feat(r2-04): meter unconfirmed + advise-failed Galaxy writes
This commit is contained in:
+91
@@ -81,8 +81,99 @@ public sealed class GatewayGalaxyDataWriterFailClosedTests
|
||||
writer.CachedSupervisedHandleCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---------------- T7: counters ----------------
|
||||
|
||||
/// <summary>A fail-closed advise increments galaxy.writes.advise_failed (and not unconfirmed).</summary>
|
||||
[Fact]
|
||||
public async Task WriteOne_AdviseFails_Increments_AdviseFailed_Meter()
|
||||
{
|
||||
using var advise = new MeterRecorder("galaxy.writes.advise_failed");
|
||||
using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
|
||||
var ops = new FakeMxWriteOps
|
||||
{
|
||||
InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.MxaccessFailure } },
|
||||
};
|
||||
var writer = NewWriter();
|
||||
|
||||
await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.A", true), SecurityClassification.FreeAccess, CancellationToken.None);
|
||||
|
||||
advise.Total.ShouldBe(1);
|
||||
unconfirmed.Total.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>An advised write whose reply carries an EMPTY statuses array returns Good but increments
|
||||
/// galaxy.writes.unconfirmed (the commit is unconfirmed pending gateway WriteComplete correlation).</summary>
|
||||
[Fact]
|
||||
public async Task WriteOne_EmptyStatuses_ReturnsGood_And_Increments_Unconfirmed_Meter()
|
||||
{
|
||||
using var advise = new MeterRecorder("galaxy.writes.advise_failed");
|
||||
using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
|
||||
var ops = new FakeMxWriteOps
|
||||
{
|
||||
InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } },
|
||||
RawWriteResponder = () => new MxCommandReply(), // no ProtocolStatus, EMPTY statuses ⇒ Good
|
||||
};
|
||||
var writer = NewWriter();
|
||||
|
||||
var result = await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.B", 1), SecurityClassification.FreeAccess, CancellationToken.None);
|
||||
|
||||
result.StatusCode.ShouldBe(StatusCodeMap.Good);
|
||||
unconfirmed.Total.ShouldBe(1);
|
||||
advise.Total.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>A reply carrying an MX status ROW increments neither meter (not empty-statuses, advise OK).</summary>
|
||||
[Fact]
|
||||
public async Task WriteOne_StatusRowReply_Increments_Neither_Meter()
|
||||
{
|
||||
using var advise = new MeterRecorder("galaxy.writes.advise_failed");
|
||||
using var unconfirmed = new MeterRecorder("galaxy.writes.unconfirmed");
|
||||
var okStatusReply = new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } };
|
||||
okStatusReply.Statuses.Add(new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok });
|
||||
var ops = new FakeMxWriteOps
|
||||
{
|
||||
InvokeResponder = _ => new MxCommandReply { ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok } },
|
||||
RawWriteResponder = () => okStatusReply,
|
||||
};
|
||||
var writer = NewWriter();
|
||||
|
||||
var result = await writer.WriteOneForTestAsync(ops, new WriteRequest("Tag.C", 2), SecurityClassification.FreeAccess, CancellationToken.None);
|
||||
|
||||
result.StatusCode.ShouldBe(StatusCodeMap.Good); // Success/Ok status row ⇒ Good
|
||||
advise.Total.ShouldBe(0);
|
||||
unconfirmed.Total.ShouldBe(0); // NOT empty-statuses ⇒ not metered unconfirmed
|
||||
}
|
||||
|
||||
// ---------------- fakes ----------------
|
||||
|
||||
/// <summary>Listens to a single instrument by name on the Galaxy driver meter and sums the values.</summary>
|
||||
private sealed class MeterRecorder : IDisposable
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly System.Diagnostics.Metrics.MeterListener _listener;
|
||||
private long _total;
|
||||
private readonly object _gate = new();
|
||||
|
||||
public MeterRecorder(string instrumentName)
|
||||
{
|
||||
_name = instrumentName;
|
||||
_listener = new System.Diagnostics.Metrics.MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == EventPump.MeterName && instrument.Name == _name)
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
_listener.SetMeasurementEventCallback<long>((_, value, _, _) => { lock (_gate) _total += value; });
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public long Total { get { lock (_gate) return _total; } }
|
||||
|
||||
public void Dispose() => _listener.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>A fake <see cref="IMxWriteOps"/> that records every call and returns canned replies.</summary>
|
||||
private sealed class FakeMxWriteOps : IMxWriteOps
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user