fix(comm): site delete disposes the site's cached gRPC channels via RemoveSiteAsync (plan R2-02 T13)
This commit is contained in:
@@ -1583,6 +1583,18 @@ public class ManagementActor : ReceiveActor
|
|||||||
await repo.SaveChangesAsync();
|
await repo.SaveChangesAsync();
|
||||||
var commService = sp.GetService<CommunicationService>();
|
var commService = sp.GetService<CommunicationService>();
|
||||||
commService?.RefreshSiteAddresses();
|
commService?.RefreshSiteAddresses();
|
||||||
|
|
||||||
|
// Dispose the deleted site's cached gRPC channels — RemoveSiteAsync is the
|
||||||
|
// factory's designed site-deletion disposal path and previously had no
|
||||||
|
// production caller (arch review 02 round 2, N8): a deleted site's channels
|
||||||
|
// (both node endpoints) otherwise persist until process restart. Null-safe:
|
||||||
|
// test/composition roots without the factory skip it. Any live-alarm
|
||||||
|
// aggregator for the site reconciles to an empty snapshot and is reaped by
|
||||||
|
// the viewer linger (documented acceptance — see Component-Communication.md).
|
||||||
|
var grpcFactory = sp.GetService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcClientFactory>();
|
||||||
|
if (grpcFactory is not null && site is not null)
|
||||||
|
await grpcFactory.RemoveSiteAsync(site.SiteIdentifier);
|
||||||
|
|
||||||
await AuditAsync(sp, user, "Delete", "Site", cmd.SiteId.ToString(), site?.Name ?? cmd.SiteId.ToString(), null);
|
await AuditAsync(sp, user, "Delete", "Site", cmd.SiteId.ToString(), site?.Name ?? cmd.SiteId.ToString(), null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||||
using NSubstitute.ExceptionExtensions;
|
using NSubstitute.ExceptionExtensions;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
|
||||||
@@ -3599,6 +3601,51 @@ public class ManagementActorTests : TestKit, IDisposable
|
|||||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── R2 T13: site delete disposes the site's cached gRPC channels (N8) ──
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteSite_DisposesTheSitesCachedGrpcChannels()
|
||||||
|
{
|
||||||
|
var siteRepo = Substitute.For<ISiteRepository>();
|
||||||
|
siteRepo.GetSiteByIdAsync(7, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Site("Seven", "site-7") { Id = 7 });
|
||||||
|
siteRepo.GetInstancesBySiteIdAsync(7, Arg.Any<CancellationToken>())
|
||||||
|
.Returns((IReadOnlyList<Instance>)new List<Instance>());
|
||||||
|
_services.AddScoped(_ => siteRepo);
|
||||||
|
|
||||||
|
var factory = new TrackingGrpcFactory();
|
||||||
|
var cached = (TrackingClient)factory.GetOrCreate("site-7", "http://node-a:8083");
|
||||||
|
_services.AddSingleton<SiteStreamGrpcClientFactory>(factory);
|
||||||
|
|
||||||
|
var actor = CreateActor();
|
||||||
|
actor.Tell(Envelope(new DeleteSiteCommand(7), "Administrator"));
|
||||||
|
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
AwaitAssert(() =>
|
||||||
|
{
|
||||||
|
Assert.True(cached.Disposed);
|
||||||
|
Assert.Null(factory.TryGet("site-7", "http://node-a:8083"));
|
||||||
|
}, TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackingClient : SiteStreamGrpcClient
|
||||||
|
{
|
||||||
|
public TrackingClient(string endpoint) : base(endpoint) { }
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
public override void Dispose() => Disposed = true;
|
||||||
|
public override ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Disposed = true;
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackingGrpcFactory : SiteStreamGrpcClientFactory
|
||||||
|
{
|
||||||
|
public TrackingGrpcFactory() : base(NullLoggerFactory.Instance) { }
|
||||||
|
protected override SiteStreamGrpcClient CreateClient(string grpcEndpoint) => new TrackingClient(grpcEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records remote-query relays for the actor-dispatch tests. The browse /
|
/// Records remote-query relays for the actor-dispatch tests. The browse /
|
||||||
/// search / verify / cert-trust methods on <see cref="CommunicationService"/>
|
/// search / verify / cert-trust methods on <see cref="CommunicationService"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user