fix(communication): ReconcileSite threads the gRPC call's cancellation into central reconcile work (review 2026-10-21 03/P-1b)

This commit is contained in:
Joseph Doherty
2026-10-22 08:32:07 -04:00
parent 429ccb8d90
commit dd60e236bd
5 changed files with 94 additions and 5 deletions
@@ -81,4 +81,47 @@ public class CentralCommunicationActorReconcileTests : TestKit
Assert.Equal("dep-B", gap.DeploymentId);
Assert.False(string.IsNullOrWhiteSpace(gap.FetchToken));
}
/// <summary>Verifies that cancelling the originating gRPC call stops central reconcile work (review 2026-10-21 03/P-1b).</summary>
[Fact]
public void CancelledCall_StopsCentralReconcileWork()
{
var deploymentRepo = Substitute.For<IDeploymentManagerRepository>();
var siteRepo = Substitute.For<ISiteRepository>();
siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<Site>());
var lookupStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
CancellationToken observed = default;
siteRepo.GetSiteByIdentifierAsync("site1", Arg.Any<CancellationToken>()).Returns(async ci =>
{
observed = ci.Arg<CancellationToken>();
lookupStarted.TrySetResult();
await Task.Delay(Timeout.Infinite, observed); // a slow central DB
return (Site?)new Site("Site One", "site1") { Id = 7 };
});
var services = new ServiceCollection();
services.AddScoped(_ => deploymentRepo);
services.AddScoped(_ => siteRepo);
services.AddSingleton(Options.Create(new CommunicationOptions()));
services.AddSingleton<Microsoft.Extensions.Logging.ILogger<ReconcileService>>(
NullLogger<ReconcileService>.Instance);
services.AddScoped<ReconcileService>();
var sp = services.BuildServiceProvider();
var actor = Sys.ActorOf(Props.Create(() => new CentralCommunicationActor(
sp, Substitute.For<ISiteCommandTransport>())));
using var call = new CancellationTokenSource();
actor.Tell(new ReconcileSiteCall(
new ReconcileSiteRequest("site1", "node-b", new Dictionary<string, string>()), call.Token));
AwaitCondition(() => lookupStarted.Task.IsCompleted, TimeSpan.FromSeconds(3));
// The site gives up (deadline / disconnect).
call.Cancel();
AwaitCondition(() => observed.IsCancellationRequested, TimeSpan.FromSeconds(3));
ExpectMsg<Status.Failure>(TimeSpan.FromSeconds(3));
deploymentRepo.DidNotReceiveWithAnyArgs().GetExpectedDeploymentsForSiteAsync(default, default);
}
}