fix(communication): debug-stream teardown/failover unsubscribes via TryGet, never creates or disposes shared channels
This commit is contained in:
@@ -400,6 +400,60 @@ public class DebugStreamBridgeActorTests : TestKit
|
||||
Assert.Equal("corr-1", factory.ClientFor(GrpcNodeB).SubscribeCalls[0].CorrelationId);
|
||||
}
|
||||
|
||||
// ── Task 6 (arch review 02, High): teardown/failover unsubscribe is endpoint-safe ──
|
||||
// Both paths use TryGet, never GetOrCreate, so cleanup can never open a fresh
|
||||
// channel or (with (site,endpoint) keying) touch another session's channel.
|
||||
|
||||
private (IActorRef Actor, EndpointTrackingGrpcClientFactory Factory) CreateBridgeWithTrackingFactory()
|
||||
{
|
||||
var commProbe = CreateTestProbe();
|
||||
var factory = new EndpointTrackingGrpcClientFactory();
|
||||
var props = Props.Create(typeof(DebugStreamBridgeActor),
|
||||
SiteId, InstanceName, "corr-1", commProbe.Ref,
|
||||
(Action<object>)(_ => { }), (Action)(() => { }),
|
||||
factory, GrpcNodeA, GrpcNodeB);
|
||||
var actor = Sys.ActorOf(props);
|
||||
commProbe.ExpectMsg<SiteEnvelope>(); // PreStart subscribe handshake
|
||||
return (actor, factory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CleanupGrpc_UnsubscribesViaTryGet_WithoutOpeningAChannel()
|
||||
{
|
||||
var (actor, factory) = CreateBridgeWithTrackingFactory();
|
||||
AwaitCondition(() => factory.ClientFor(GrpcNodeA).SubscribeCalls.Count == 1,
|
||||
TimeSpan.FromSeconds(3));
|
||||
var createdBefore = factory.CreatedCount; // node-a only
|
||||
|
||||
actor.Tell(new StopDebugStream());
|
||||
|
||||
AwaitCondition(() => factory.ClientFor(GrpcNodeA).UnsubscribedCorrelationIds.Contains("corr-1"),
|
||||
TimeSpan.FromSeconds(3));
|
||||
// Pre-fix CleanupGrpc called GetOrCreate → could open a fresh channel here.
|
||||
Assert.Equal(createdBefore, factory.CreatedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionFailover_UnsubscribesFailedEndpointViaTryGet_OpensOnlyTheReconnectChannel()
|
||||
{
|
||||
var (_, factory) = CreateBridgeWithTrackingFactory();
|
||||
AwaitCondition(() => factory.ClientFor(GrpcNodeA).SubscribeCalls.Count == 1,
|
||||
TimeSpan.FromSeconds(3));
|
||||
|
||||
// Fail NodeA: the bridge unsubscribes the failed NodeA stream via TryGet
|
||||
// (if that path had used GetOrCreate on the post-flip cache it could have
|
||||
// disposed/created the wrong client), then reconnects to NodeB.
|
||||
factory.ClientFor(GrpcNodeA).SubscribeCalls[0].OnError(new Exception("NodeA down"));
|
||||
|
||||
AwaitCondition(() => factory.ClientFor(GrpcNodeA).UnsubscribedCorrelationIds.Contains("corr-1"),
|
||||
TimeSpan.FromSeconds(5));
|
||||
AwaitCondition(() => factory.ClientFor(GrpcNodeB).SubscribeCalls.Count == 1,
|
||||
TimeSpan.FromSeconds(5));
|
||||
// Only the two real node endpoints were ever opened — the unsubscribe
|
||||
// itself created nothing.
|
||||
Assert.Equal(2, factory.CreatedCount);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// M2.18 (#26) — stream-first + replay/dedup
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -883,6 +937,12 @@ internal class MockSiteStreamGrpcClientFactory : SiteStreamGrpcClientFactory
|
||||
RequestedEndpoints.Add(grpcEndpoint);
|
||||
return _mockClient;
|
||||
}
|
||||
|
||||
// Mirrors the real factory's TryGet: returns the client only for an endpoint
|
||||
// already opened via GetOrCreate, never creating one. Lets teardown/failover
|
||||
// unsubscribe paths (now TryGet-based, Task 6) resolve the mock client.
|
||||
public override SiteStreamGrpcClient? TryGet(string siteIdentifier, string grpcEndpoint)
|
||||
=> RequestedEndpoints.Contains(grpcEndpoint) ? _mockClient : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -902,6 +962,13 @@ internal class EndpointTrackingGrpcClientFactory : SiteStreamGrpcClientFactory
|
||||
public MockSiteStreamGrpcClient ClientFor(string endpoint) =>
|
||||
_byEndpoint.GetOrAdd(endpoint, _ => new MockSiteStreamGrpcClient());
|
||||
|
||||
/// <summary>Number of distinct endpoint clients created so far (channels opened).</summary>
|
||||
public int CreatedCount => _byEndpoint.Count;
|
||||
|
||||
public override SiteStreamGrpcClient GetOrCreate(string siteIdentifier, string grpcEndpoint)
|
||||
=> ClientFor(grpcEndpoint);
|
||||
|
||||
// TryGet never creates: returns the client only for an already-opened endpoint.
|
||||
public override SiteStreamGrpcClient? TryGet(string siteIdentifier, string grpcEndpoint)
|
||||
=> _byEndpoint.TryGetValue(grpcEndpoint, out var client) ? client : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user