mbproxy: fix Multiplexing N6 — cache-hit replay preserves observation age
The response-cache hit path replayed captured tag observations into the debug-view capture stamped 'now', making a cache-served read look freshly read when its value is actually up to CacheTtlMs old. Record() now takes an optional observedAtUtc; the cache-hit replay passes each observation's original timestamp so the debug view shows the true age. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -913,17 +913,18 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi
|
||||
|
||||
// A cache hit bypasses the BCD pipeline, so the debug-view capture
|
||||
// would otherwise never see cache-served reads. Replay the
|
||||
// observations captured when this entry was stored — re-stamped now,
|
||||
// since the client receives this value right now — so the detail
|
||||
// page reflects what the client actually gets. Entries stored while
|
||||
// no viewer was armed carry no observations; those tags self-heal on
|
||||
// the next cache miss.
|
||||
// observations captured when this entry was stored, preserving each
|
||||
// observation's ORIGINAL timestamp — the value the client receives is
|
||||
// cache-aged (up to CacheTtlMs old), so the debug view must show its
|
||||
// true age, not a misleading "just now". Entries stored while no
|
||||
// viewer was armed carry no observations; those tags self-heal on the
|
||||
// next cache miss.
|
||||
if (_ctx.Capture is { IsArmed: true } capture &&
|
||||
cached.CapturedTags is { Count: > 0 } cachedTags)
|
||||
{
|
||||
foreach (var obs in cachedTags)
|
||||
capture.Record(obs.Address, obs.RawLow, obs.RawHigh,
|
||||
obs.DecodedValue, CaptureDirection.Read);
|
||||
obs.DecodedValue, CaptureDirection.Read, obs.UpdatedAtUtc);
|
||||
}
|
||||
|
||||
byte[] hitFrame = BuildCacheHitFrame(originalTxId, unitId, cached.PduBytes);
|
||||
|
||||
@@ -126,7 +126,15 @@ internal sealed class TagValueCapture
|
||||
/// <param name="rawLow">BCD-encoded low word as it sits on the PLC wire.</param>
|
||||
/// <param name="rawHigh">BCD-encoded high word (0 for a 16-bit tag).</param>
|
||||
/// <param name="decoded">Decoded binary integer the client reads/wrote.</param>
|
||||
public void Record(ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction)
|
||||
/// <param name="observedAtUtc">
|
||||
/// When the value was actually observed from the PLC. Defaults to "now" for a live
|
||||
/// pipeline observation. The response-cache hit path passes the cached observation's
|
||||
/// original timestamp so a cache-served read shows its true age in the debug view
|
||||
/// rather than appearing freshly read.
|
||||
/// </param>
|
||||
public void Record(
|
||||
ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction,
|
||||
DateTimeOffset? observedAtUtc = null)
|
||||
{
|
||||
if (!_armed)
|
||||
return;
|
||||
@@ -137,7 +145,7 @@ internal sealed class TagValueCapture
|
||||
ref _slots[idx],
|
||||
new TagValueObservation(
|
||||
_addresses[idx], _widths[idx], _names[idx], rawLow, rawHigh, decoded, direction,
|
||||
DateTimeOffset.UtcNow));
|
||||
observedAtUtc ?? DateTimeOffset.UtcNow));
|
||||
|
||||
// A concurrent Disarm() may have flipped _armed (and cleared the slots) between
|
||||
// the _armed check above and the write just made — which would strand a stale
|
||||
|
||||
Reference in New Issue
Block a user