docs(GWC-26): record alarm feed repairs as at-least-once; share the channel worker fake
Code-review follow-up on fix/gwc-26-27-alarm-attach. ApplyReconcile's snapshot-derived feed repairs are at-least-once, not exactly-once: a reconcile reads the worker's current state while the matching live transition may still be buffered in the monitor's lease, so both broadcast and the duplicates are indistinguishable on the alarm feed. This pre-dates the acked-state delta — the Raise/Clear presence repair has always had it, since nothing serializes a reconcile pass against the in-flight live stream — so closing it (serialization or timestamp dedup) stays out of scope for a P2 fix. Documented instead, with the consumer contract stated explicitly (apply transitions idempotently, never as an increment or toggle): - ApplyReconcile gains a "Delivery semantics" comment. - gateway.md softens the "defense in depth" prose to state the semantics. - docs/Sessions.md carries the same caveat on the alarm-feed description. - Tracker change-log records it as a known pre-existing characteristic and a candidate finding for the next review cycle. Also hoists the ChannelWorkerClient fake — duplicated across the three alarm test files — into TestSupport/, dropping the usings it took with it.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Channels;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Workers;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||
|
||||
/// <summary>
|
||||
/// Always-<see cref="WorkerClientState.Ready"/> <see cref="IWorkerClient"/> whose event
|
||||
/// stream is a channel the test writes to. Lets a test attach a real
|
||||
/// <c>GatewaySession</c> to a worker it drives by hand — the session can be marked Ready,
|
||||
/// its <c>SessionEventDistributor</c> pump then drains this channel, and the test controls
|
||||
/// exactly when each event is fanned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Commands are not scripted here: <see cref="InvokeAsync"/> returns an empty reply, because
|
||||
/// the consumers of this fake route commands through their own <c>ISessionManager</c> double
|
||||
/// rather than through the worker client. Use a purpose-built worker client instead when a
|
||||
/// test needs command behavior.
|
||||
/// </remarks>
|
||||
/// <param name="sessionId">Session identifier the client reports.</param>
|
||||
/// <param name="events">Channel whose events <see cref="ReadEventsAsync"/> yields, in order.</param>
|
||||
public sealed class ChannelWorkerClient(string sessionId, ChannelReader<WorkerEvent> events) : IWorkerClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string SessionId { get; } = sessionId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? ProcessId { get; } = 4321;
|
||||
|
||||
/// <inheritdoc />
|
||||
public WorkerClientState State { get; } = WorkerClientState.Ready;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
WorkerCommand command,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken) => Task.FromResult(new WorkerCommandReply());
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (WorkerEvent workerEvent in events.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return workerEvent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Kill(string reason)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user