Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add merge status API to fw-headless #1294

Merged
merged 12 commits into from
Dec 6, 2024
2 changes: 2 additions & 0 deletions backend/FwHeadless/FwHeadlessKernel.cs
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ public static void AddFwHeadless(this IServiceCollection services)
.BindConfiguration("FwHeadlessConfig")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<ProjectSyncStatusService>();
services.AddScoped<SendReceiveService>();
services.AddScoped<ProjectLookupService>();
services.AddScoped<LogSanitizerService>();
@@ -26,6 +27,7 @@ public static void AddFwHeadless(this IServiceCollection services)
.AddFwDataBridge()
.AddFwLiteProjectSync();
services.AddScoped<CrdtSyncService>();
services.AddScoped<ProjectContextFromIdService>();
services.AddTransient<HttpClientAuthHandler>();
services.AddHttpClient(LexboxHttpClientName,
(provider, client) =>
41 changes: 40 additions & 1 deletion backend/FwHeadless/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FwHeadless;
using FwHeadless.Services;
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using FwLiteProjectSync;
@@ -10,6 +11,10 @@
using Microsoft.Extensions.Options;
using MiniLcm;
using Scalar.AspNetCore;
using LexCore.Utils;
using LinqToDB;
using SIL.Harmony.Core;
using SIL.Harmony;

var builder = WebApplication.CreateBuilder(args);

@@ -36,6 +41,13 @@
await next();
});

// Load project ID from request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Or just delete it. But, I believe the comment is inaccurate/out-of-date

Suggested change
// Load project ID from request
// Scope services to the relevant project

app.Use((context, next) =>
{
var renameThisService = context.RequestServices.GetRequiredService<ProjectContextFromIdService>();
return renameThisService.PopulateProjectContext(context, next);
});

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@@ -49,6 +61,7 @@
app.MapHealthChecks("/api/healthz");

app.MapPost("/api/crdt-sync", ExecuteMergeRequest);
app.MapGet("/api/crdt-sync-status", GetMergeStatus);

app.Run();

@@ -60,6 +73,7 @@ static async Task<Results<Ok<SyncResult>, NotFound, ProblemHttpResult>> ExecuteM
FwDataFactory fwDataFactory,
CrdtProjectsService projectsService,
ProjectLookupService projectLookupService,
ProjectSyncStatusService syncStatusService,
CrdtFwdataProjectSyncService syncService,
CrdtHttpSyncService crdtHttpSyncService,
IHttpClientFactory httpClientFactory,
@@ -73,6 +87,9 @@ static async Task<Results<Ok<SyncResult>, NotFound, ProblemHttpResult>> ExecuteM
return TypedResults.Ok(new SyncResult(0, 0));
}

syncStatusService.StartSyncing(projectId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It would be pretty easy to put the number of changes that are being synced in the sync status, so that we can provide that bonus status info during a sync as well.

using var stopSyncing = Defer.Action(() => syncStatusService.StopSyncing(projectId));

var projectCode = await projectLookupService.GetProjectCode(projectId);
if (projectCode is null)
{
@@ -103,7 +120,6 @@ static async Task<Results<Ok<SyncResult>, NotFound, ProblemHttpResult>> ExecuteM
var crdtSyncService = services.GetRequiredService<CrdtSyncService>();
await crdtSyncService.Sync();


var result = await syncService.Sync(miniLcmApi, fwdataApi, dryRun);
logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}", result.CrdtChanges, result.FwdataChanges);

@@ -113,6 +129,29 @@ static async Task<Results<Ok<SyncResult>, NotFound, ProblemHttpResult>> ExecuteM
return TypedResults.Ok(result);
}

static async Task<Results<Ok<ProjectSyncStatus>, NotFound>> GetMergeStatus(
ProjectContext projectContext,
ProjectLookupService projectLookupService,
ProjectSyncStatusService syncStatusService,
IServiceProvider services,
LexBoxDbContext lexBoxDb,
Guid projectId)
{
var status = syncStatusService.SyncStatus(projectId);
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
if (status is not null) return TypedResults.Ok(new ProjectSyncStatus(status.Value, 0));
var project = projectContext.Project;
if (project is null)
{
// 404 only means "project doesn't exist"; if we don't know the status, then it hasn't synced before and is therefore ready to sync
if (await projectLookupService.ProjectExists(projectId)) return TypedResults.Ok(ProjectSyncStatus.NeverSynced);
else return TypedResults.NotFound();
}
var commitsOnServer = await lexBoxDb.Set<ServerCommit>().CountAsync(c => c.ProjectId == projectId);
var lcmCrdtDbContext = services.GetRequiredService<LcmCrdtDbContext>(); // TODO: This *cannot* be right, can it?
var localCommits = await lcmCrdtDbContext.Set<Commit>().CountAsync();
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
return TypedResults.Ok(ProjectSyncStatus.ReadyToSync(localCommits - commitsOnServer));
}

static async Task<FwDataMiniLcmApi> SetupFwData(FwDataProject fwDataProject,
SendReceiveService srService,
string projectCode,
2 changes: 1 addition & 1 deletion backend/FwHeadless/Services/AppVersionService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Reflection;

namespace FwHeadless;
namespace FwHeadless.Services;

public static class AppVersionService
{
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
using LcmCrdt.RemoteSync;
using SIL.Harmony;

namespace FwHeadless;
namespace FwHeadless.Services;

public class CrdtSyncService(
CrdtHttpSyncService httpSyncService,
14 changes: 14 additions & 0 deletions backend/FwHeadless/Services/HttpHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace FwHeadless.Services;

public static class HttpHelpers
{
public static Guid? GetProjectId(this HttpContext? context)
{
if (context is null) return null;
if (context.Request.Query.TryGetValue("projectId", out var projectIds) && projectIds.FirstOrDefault() is string idStr)
{
if (Guid.TryParse(idStr, out var projectId)) return projectId;
}
return null;
}
}
28 changes: 28 additions & 0 deletions backend/FwHeadless/Services/ProjectContextFromIdService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using LcmCrdt;
using Microsoft.Extensions.Options;

namespace FwHeadless.Services;

// TODO: Pick better name
public class ProjectContextFromIdService(IOptions<FwHeadlessConfig> config, CrdtProjectsService projectsService, ProjectLookupService projectLookupService)
hahn-kev marked this conversation as resolved.
Show resolved Hide resolved
{
public async Task PopulateProjectContext(HttpContext context, Func<Task> next)
{
if (context.GetProjectId() is Guid projectId)
{
var projectCode = await projectLookupService.GetProjectCode(projectId);
if (projectCode is not null)
{
var projectFolder = Path.Join(config.Value.ProjectStorageRoot, $"{projectCode}-{projectId}");
var crdtFile = Path.Join(projectFolder, "crdt.sqlite");
if (File.Exists(crdtFile))
{
var project = new CrdtProject("crdt", crdtFile);
projectsService.SetProjectScope(project);
await context.RequestServices.GetRequiredService<CurrentProjectService>().PopulateProjectDataCache();
}
}
}
await next();
}
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
using Microsoft.EntityFrameworkCore;
using SIL.Harmony.Core;

namespace FwHeadless;
namespace FwHeadless.Services;

public class ProjectLookupService(LexBoxDbContext dbContext)
{
@@ -15,6 +15,11 @@ public class ProjectLookupService(LexBoxDbContext dbContext)
return projectCode;
}

public async Task<bool> ProjectExists(Guid projectId)
{
return await dbContext.Projects.AnyAsync(p => p.Id == projectId);
}

public async Task<bool> IsCrdtProject(Guid projectId)
{
return await dbContext.Set<ServerCommit>().AnyAsync(c => c.ProjectId == projectId);
43 changes: 43 additions & 0 deletions backend/FwHeadless/Services/ProjectSyncStatusService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Concurrent;

namespace FwHeadless.Services;

public class ProjectSyncStatusService()
{
private ConcurrentDictionary<Guid, ProjectSyncStatusEnum> Status { get; init; } = new();

public void StartSyncing(Guid projectId)
{
Status.AddOrUpdate(projectId, (_) => ProjectSyncStatusEnum.Syncing, (_, _) => ProjectSyncStatusEnum.Syncing);
}

public void StopSyncing(Guid projectId)
{
Status.Remove(projectId, out var _);
}

public ProjectSyncStatusEnum? SyncStatus(Guid projectId)
{
return Status.TryGetValue(projectId, out var status) ? status : null;
}
}

public enum ProjectSyncStatusEnum
{
NeverSynced,
ReadyToSync,
Syncing,
}

// TODO: Bikeshed this name
public record ProjectSyncStatus(
Comment on lines +38 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's perfect 👌

Suggested change
// TODO: Bikeshed this name
public record ProjectSyncStatus(
public record ProjectSyncStatus(

ProjectSyncStatusEnum status,
int ChangesAvailable)
{
public static ProjectSyncStatus NeverSynced => new(ProjectSyncStatusEnum.NeverSynced, 0);
public static ProjectSyncStatus Syncing => new(ProjectSyncStatusEnum.Syncing, 0);
public static ProjectSyncStatus ReadyToSync(int changes)
{
return new(ProjectSyncStatusEnum.ReadyToSync, changes);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using FwDataMiniLcmBridge;
using SIL.Progress;

namespace FwHeadless;
namespace FwHeadless.Services;

public static class SendReceiveHelpers
{
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using FwDataMiniLcmBridge;
using FwHeadless.Services;
using Microsoft.Extensions.Options;

namespace FwHeadless;
namespace FwHeadless.Services;

public class SendReceiveService(IOptions<FwHeadlessConfig> config, SafeLoggingProgress progress)
{
5 changes: 5 additions & 0 deletions backend/LfClassicData/LfClassicMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -20,6 +20,11 @@ public IAsyncEnumerable<ComplexFormType> GetComplexFormTypes()
return AsyncEnumerable.Empty<ComplexFormType>();
}

public Task<ComplexFormType?> GetComplexFormType(Guid id)
{
return Task.FromResult<ComplexFormType?>(null);
}

public async Task<WritingSystems> GetWritingSystems()
{
var inputSystems = await systemDbContext.Projects.AsQueryable()
Loading