988046cbe4
PerPlatformProbeWatcher records a platform's handle and publishes the probe binding into the driver registry as one step under _syncLock, but WatchedPlatforms read the handle map lock-free - so it could report a platform one step ahead of the registry Upsert. A ScanState event raised in that window was dispatched by the EventPump, missed ResolveSubscribers and was dropped as "stale after unsubscribe", leaving the host status empty. That is the #856 flake: A_fetch_completing_after_init_populates_host_statuses waited for WatchedPlatforms, emitted ScanState, and timed out at 10 s on the final Running wait. Reproduced 11/700 (28 parallel runners x 25 + 14 CPU burners), every failure at that step with the binding present by the time of diagnosis and hostStatuses=[] - i.e. the event was dropped. 0/1400 after. WatchedPlatforms now reads under _syncLock (test seam only; the dispatch path already checks the map after the registry, the safe order). New PerPlatformProbeWatcherVisibilityTests spins a reader on another thread and asserts the registry entry exists the moment the platform is visible (old code: torn in 1691/2000 iterations). WaitUntilAsync gains a describe overload; the #856 test's waits now report elapsed time and the observed seed/binding/pump/host-status chain on timeout. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
56 lines
2.7 KiB
C#
56 lines
2.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
using static ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime.GalaxyDriverFixRound1Tests;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Health;
|
|
|
|
/// <summary>
|
|
/// #856: a platform must never be observable in <see cref="PerPlatformProbeWatcher.WatchedPlatforms"/>
|
|
/// before its probe binding resolves through the driver registry. The watcher records the handle
|
|
/// and then publishes the binding inside one lock, but <c>WatchedPlatforms</c> used to read the
|
|
/// handle map lock-free — so a caller could see the platform, raise a ScanState event, and have the
|
|
/// pump drop it as "stale after unsubscribe" (host status stuck at Unknown). That is what made
|
|
/// <c>A_fetch_completing_after_init_populates_host_statuses</c> time out under load.
|
|
/// </summary>
|
|
public sealed class PerPlatformProbeWatcherVisibilityTests
|
|
{
|
|
/// <summary>
|
|
/// A reader spinning on <c>WatchedPlatforms</c> from another thread checks, at the first moment
|
|
/// it sees the platform, that the registry already holds the probe entry. Before the fix this
|
|
/// caught the torn state in ~85% of iterations; after it, the publish is part of what the
|
|
/// reader's lock waits for, so the torn state is unobservable.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task A_platform_visible_as_watched_already_resolves_through_the_registry()
|
|
{
|
|
const int iterations = 500;
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var torn = 0;
|
|
for (var i = 0; i < iterations; i++)
|
|
{
|
|
var registry = new SubscriptionRegistry();
|
|
var sub = new ParkableSubscriber();
|
|
using var watcher = new PerPlatformProbeWatcher(sub, new HostStatusAggregator(), registry);
|
|
using var readerRunning = new ManualResetEventSlim();
|
|
var observed = Task.Run(() =>
|
|
{
|
|
readerRunning.Set();
|
|
var deadline = DateTime.UtcNow.AddMilliseconds(WaitMs);
|
|
while (watcher.WatchedPlatforms.Count == 0)
|
|
{
|
|
if (DateTime.UtcNow > deadline) throw new ShouldAssertException("the sync never recorded the platform");
|
|
}
|
|
return registry.Contains(SubscriptionRegistry.ProbeSubscriptionId);
|
|
}, ct);
|
|
readerRunning.Wait(ct);
|
|
await Task.Run(() => watcher.SyncPlatformsAsync(["P"], CancellationToken.None), ct);
|
|
if (!await observed) torn++;
|
|
}
|
|
|
|
torn.ShouldBe(0,
|
|
$"{torn}/{iterations} iterations saw a watched platform whose probe binding was not yet in the registry");
|
|
}
|
|
}
|