79d6b0a4b2
- DriverHostActor.StopChild removes the driver's _lastReprovisionUtc stamp (and its deferred-reprovision timer): the map grew per removed id and a re-added driver inherited a backoff it never earned (02/L2). - Stale now registers explicit drops for AttributeValueBatchPublished, AttributeAlarmPublished, ArtifactForApply and ApplyWedged instead of letting them hit Unhandled (02/H5). - DbHealthProbeActor runs its SELECT on the thread pool and pipes the outcome back (single-flight), with a 2 s command timeout on relational providers, so a hung SQL Server no longer pins the actor thread and the cached GetStatus answer stays reachable (02/P7). - TelemetryLocalHub eviction tombstones are timestamped and swept past a 10 min TTL on the next Evict; a TimeProvider ctor is the test seam (02/L4). - Tests: ValuePublishes.Single helper replaces the per-value AttributeValuePublished in CalculationDependencyFlowTests (02/U4, test half; the production receive is owned by Lane E). - R05/8 audit: PollGroupEngine.Unsubscribe has no production caller; every driver already uses UnsubscribeAsync (no change). Refs #731 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
70 lines
2.8 KiB
C#
70 lines
2.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Telemetry;
|
|
|
|
/// <summary>
|
|
/// Review 02 L4: <see cref="TelemetryLocalHub.Evict"/>'s tombstone set only ever shrank on a re-spawn of the
|
|
/// SAME id, so a driver removed for good kept its id forever. The tombstone is load-bearing (#609 — it holds
|
|
/// back the removed child's late writes), so it cannot simply be cleared; it is now timestamped and swept
|
|
/// once older than <see cref="TelemetryLocalHub.EvictionTombstoneTtl"/>, which outlives any in-flight
|
|
/// capability call by a wide margin.
|
|
/// </summary>
|
|
public sealed class TelemetryLocalHubTombstoneSweepTests
|
|
{
|
|
private static DriverHealthChanged Health(string instanceId) =>
|
|
new("cluster-a", instanceId, "Healthy", null, null, 0, DateTime.UtcNow);
|
|
|
|
private static List<TelemetryItem> Drain(ITelemetrySubscription sub)
|
|
{
|
|
var items = new List<TelemetryItem>();
|
|
while (sub.Reader.TryRead(out var item)) items.Add(item);
|
|
return items;
|
|
}
|
|
|
|
[Fact]
|
|
public void A_tombstone_holds_late_writes_until_the_ttl_then_is_swept()
|
|
{
|
|
var clock = new ManualTime { Now = new DateTimeOffset(2026, 10, 22, 12, 0, 0, TimeSpan.Zero) };
|
|
var hub = new TelemetryLocalHub(clock);
|
|
hub.Emit(new TelemetryItem.Health(Health("drv-a")));
|
|
hub.Evict("drv-a");
|
|
|
|
// Inside the TTL: the late write is dropped (the #609 contract) — and another eviction's sweep does
|
|
// not lift a tombstone that is still young.
|
|
clock.Now += TelemetryLocalHub.EvictionTombstoneTtl / 2;
|
|
hub.Evict("drv-b");
|
|
hub.Emit(new TelemetryItem.Health(Health("drv-a")));
|
|
using (var sub = hub.Subscribe(16))
|
|
Drain(sub).ShouldBeEmpty("a late write inside the TTL must still be held back");
|
|
|
|
// Past the TTL: the next eviction sweeps drv-a's tombstone and its writes land again.
|
|
clock.Now += TelemetryLocalHub.EvictionTombstoneTtl;
|
|
hub.Evict("drv-c");
|
|
hub.Emit(new TelemetryItem.Health(Health("drv-a")));
|
|
using (var sub = hub.Subscribe(16))
|
|
Drain(sub).OfType<TelemetryItem.Health>().Single().E.DriverInstanceId.ShouldBe("drv-a");
|
|
}
|
|
|
|
[Fact]
|
|
public void Readmit_lifts_a_tombstone_before_its_ttl()
|
|
{
|
|
var clock = new ManualTime { Now = DateTimeOffset.UtcNow };
|
|
var hub = new TelemetryLocalHub(clock);
|
|
hub.Evict("drv-a");
|
|
hub.Readmit("drv-a");
|
|
|
|
hub.Emit(new TelemetryItem.Health(Health("drv-a")));
|
|
using var sub = hub.Subscribe(16);
|
|
Drain(sub).Count.ShouldBe(1);
|
|
}
|
|
|
|
private sealed class ManualTime : TimeProvider
|
|
{
|
|
public DateTimeOffset Now { get; set; }
|
|
public override DateTimeOffset GetUtcNow() => Now;
|
|
}
|
|
}
|