mbproxy: Wave 6 — wire ProxyCounters.AddBytes (bytes counters were always 0)
The 10-min stress test (1.46 M PDUs through the proxy) revealed that
status.json's bytes.upstreamIn / bytes.upstreamOut counters always read 0
because ProxyCounters.AddBytes was defined but never called from anywhere.
Same shape as the original review's W2.22 finding (counter wired in DTO +
HTML but no increment site), missed for the bytes counters specifically.
Wired five increment sites in PlcMultiplexer:
OnUpstreamFrameAsync (request side, parsed frame)
AddBytes(up: frame.Length, down: 0) — counted ONCE per parsed frame
regardless of subsequent routing (cache hit, coalesce, backend
round-trip, exception).
RunBackendReaderAsync fan-out (response side, after TrySendResponse=true)
AddBytes(up: 0, down: outFrame.Length) per delivered party. With
coalescing, one backend response fans out to N parties and produces
N × frame.Length bytes leaving the proxy upstream-side. Drops
(TrySendResponse=false) increment ResponseDropForFullUpstream
instead.
Cache hit path
AddBytes(up: 0, down: hitFrame.Length) for the BuildCacheHitFrame
response (no backend round-trip but still bytes leaving the proxy).
Saturation cleanup (W1.2 path, both branches)
AddBytes(up: 0, down: excFrame.Length) per delivered exception 0x04.
Non-coalescing-path saturation
AddBytes(up: 0, down: excFrame.Length) for the single exception 0x04.
Watchdog timeout exception delivery
AddBytes(up: 0, down: excFrame.Length) per delivered exception 0x0B.
Backend-side bytes (proxy ↔ PLC) are NOT counted by these counters — the
field name is `BytesUpstreamIn/Out` which is upstream-only by contract.
Tests: 387 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -690,6 +690,13 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
{
|
||||
_ctx.Counters.IncrementResponseDropForFullUpstream();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Phase 12 (W6) — count outbound bytes per delivered party.
|
||||
// With coalescing, one backend response fans out to N parties and
|
||||
// produces N × frame.Length bytes leaving the proxy upstream-side.
|
||||
_ctx.Counters.AddBytes(up: 0, down: outFrame.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,6 +730,11 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
out ushort originalTxId, out _, out _, out byte unitId))
|
||||
return;
|
||||
|
||||
// Phase 12 (W6) — count inbound bytes from the upstream client. Surfaces in
|
||||
// bytes.upstreamIn on the status page. Counted ONCE per parsed frame regardless
|
||||
// of subsequent routing (cache hit, coalesce, backend round-trip, exception).
|
||||
_ctx.Counters.AddBytes(up: frame.Length, down: 0);
|
||||
|
||||
// Parse the PDU FC + start/qty. FC03/FC04 reads use start/qty for the coalescing key
|
||||
// and (Phase 11) for the cache lookup. FC06 writes carry [addr][value]; we treat qty
|
||||
// as 1 for invalidation. FC16 carries [start][qty][byteCount]...; qty is the write
|
||||
@@ -771,6 +783,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
|
||||
byte[] hitFrame = BuildCacheHitFrame(originalTxId, unitId, cached.PduBytes);
|
||||
await pipe.SendResponseAsync(hitFrame, ct).ConfigureAwait(false);
|
||||
// Phase 12 (W6) — outbound bytes for cache-hit response.
|
||||
_ctx.Counters.AddBytes(up: 0, down: hitFrame.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -905,6 +919,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
byte[] excFrame = BuildExceptionFrame(party.OriginalTxId, unitId, fcByte, exceptionCode: 4);
|
||||
if (!party.Pipe.TrySendResponse(excFrame))
|
||||
_ctx.Counters.IncrementResponseDropForFullUpstream();
|
||||
else
|
||||
_ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -914,6 +930,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
byte[] excFrame = BuildExceptionFrame(originalTxId, unitId, fcByte, exceptionCode: 4);
|
||||
if (!pipe.TrySendResponse(excFrame))
|
||||
_ctx.Counters.IncrementResponseDropForFullUpstream();
|
||||
else
|
||||
_ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -953,6 +971,7 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
MultiplexerLogEvents.Saturated(_logger, _plc.Name, pipe.RemoteEp?.ToString() ?? "?");
|
||||
byte[] excFrame = BuildExceptionFrame(originalTxId, unitId, fcByte, exceptionCode: 4);
|
||||
await pipe.SendResponseAsync(excFrame, ct).ConfigureAwait(false);
|
||||
_ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1078,6 +1097,7 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
try
|
||||
{
|
||||
await party.Pipe.SendResponseAsync(excFrame, ct).ConfigureAwait(false);
|
||||
_ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user