fix(dcl): queue a create whose actor name belongs to a still-terminating child instead of refusing it (review 2026-10-21 follow-up L2)
This commit is contained in:
+49
@@ -20,6 +20,7 @@ public class ConnectionActorNameTests : TestKit
|
||||
{
|
||||
private readonly IDataConnectionFactory _factory = Substitute.For<IDataConnectionFactory>();
|
||||
private readonly ISiteHealthCollector _health = Substitute.For<ISiteHealthCollector>();
|
||||
private volatile bool _slowDispose;
|
||||
private readonly DataConnectionOptions _options = new()
|
||||
{
|
||||
ReconnectInterval = TimeSpan.FromMilliseconds(100),
|
||||
@@ -37,6 +38,13 @@ public class ConnectionActorNameTests : TestKit
|
||||
adapter.Status.Returns(ConnectionHealth.Connected);
|
||||
adapter.SubscribeAsync(Arg.Any<string>(), Arg.Any<SubscriptionCallback>(), Arg.Any<CancellationToken>())
|
||||
.Returns("sub-1");
|
||||
// A synchronous delay inside DisposeAsync holds the stopping child in PostStop, which
|
||||
// keeps its actor name reserved — the dispose window the L2 tests exercise.
|
||||
adapter.DisposeAsync().Returns(_ =>
|
||||
{
|
||||
if (_slowDispose) Thread.Sleep(400);
|
||||
return ValueTask.CompletedTask;
|
||||
});
|
||||
return adapter;
|
||||
});
|
||||
}
|
||||
@@ -89,4 +97,45 @@ public class ConnectionActorNameTests : TestKit
|
||||
Assert.Contains("Unknown connection", Subscribe(manager, "PLC-A").ErrorMessage);
|
||||
Assert.DoesNotContain("Unknown connection", Subscribe(manager, "PLC A").ErrorMessage ?? "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follow-up L2: a Remove immediately followed by a Create of the SAME name (a redeploy that
|
||||
/// rebuilds the connection) lands while the stopped child is still terminating, so its actor
|
||||
/// name is still reserved. The create used to be refused permanently as a "collision" — and
|
||||
/// the deployment manager, having recorded the config hash, never resent it. The manager now
|
||||
/// queues the create until the old child's Terminated arrives.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RemoveThenCreateSameName_InsideDisposeWindow_IsCreatedOnceTheOldChildTerminates()
|
||||
{
|
||||
_slowDispose = true;
|
||||
var manager = CreateManager();
|
||||
manager.Tell(new CreateConnectionCommand("PLC1", "OpcUa", new Dictionary<string, string>(), null, 3));
|
||||
manager.Tell(new RemoveConnectionCommand("PLC1"));
|
||||
manager.Tell(new CreateConnectionCommand("PLC1", "OpcUa", new Dictionary<string, string>(), null, 3));
|
||||
|
||||
AwaitAssert(
|
||||
() => Assert.DoesNotContain("Unknown connection", Subscribe(manager, "PLC1").ErrorMessage ?? ""),
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follow-up L2: a Remove that arrives while a create is still queued behind a terminating
|
||||
/// child cancels the queued create — the connection stays removed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RemoveWhileCreateQueued_CancelsTheQueuedCreate()
|
||||
{
|
||||
_slowDispose = true;
|
||||
var manager = CreateManager();
|
||||
manager.Tell(new CreateConnectionCommand("PLC2", "OpcUa", new Dictionary<string, string>(), null, 3));
|
||||
manager.Tell(new RemoveConnectionCommand("PLC2"));
|
||||
manager.Tell(new CreateConnectionCommand("PLC2", "OpcUa", new Dictionary<string, string>(), null, 3));
|
||||
manager.Tell(new RemoveConnectionCommand("PLC2"));
|
||||
|
||||
// Let the old child terminate (and any wrongly-queued create run) before asserting.
|
||||
Thread.Sleep(1000);
|
||||
Assert.Contains("Unknown connection", Subscribe(manager, "PLC2").ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user