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
|
// A cache hit bypasses the BCD pipeline, so the debug-view capture
|
||||||
// would otherwise never see cache-served reads. Replay the
|
// would otherwise never see cache-served reads. Replay the
|
||||||
// observations captured when this entry was stored — re-stamped now,
|
// observations captured when this entry was stored, preserving each
|
||||||
// since the client receives this value right now — so the detail
|
// observation's ORIGINAL timestamp — the value the client receives is
|
||||||
// page reflects what the client actually gets. Entries stored while
|
// cache-aged (up to CacheTtlMs old), so the debug view must show its
|
||||||
// no viewer was armed carry no observations; those tags self-heal on
|
// true age, not a misleading "just now". Entries stored while no
|
||||||
// the next cache miss.
|
// viewer was armed carry no observations; those tags self-heal on the
|
||||||
|
// next cache miss.
|
||||||
if (_ctx.Capture is { IsArmed: true } capture &&
|
if (_ctx.Capture is { IsArmed: true } capture &&
|
||||||
cached.CapturedTags is { Count: > 0 } cachedTags)
|
cached.CapturedTags is { Count: > 0 } cachedTags)
|
||||||
{
|
{
|
||||||
foreach (var obs in cachedTags)
|
foreach (var obs in cachedTags)
|
||||||
capture.Record(obs.Address, obs.RawLow, obs.RawHigh,
|
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);
|
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="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="rawHigh">BCD-encoded high word (0 for a 16-bit tag).</param>
|
||||||
/// <param name="decoded">Decoded binary integer the client reads/wrote.</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)
|
if (!_armed)
|
||||||
return;
|
return;
|
||||||
@@ -137,7 +145,7 @@ internal sealed class TagValueCapture
|
|||||||
ref _slots[idx],
|
ref _slots[idx],
|
||||||
new TagValueObservation(
|
new TagValueObservation(
|
||||||
_addresses[idx], _widths[idx], _names[idx], rawLow, rawHigh, decoded, direction,
|
_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
|
// 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
|
// the _armed check above and the write just made — which would strand a stale
|
||||||
|
|||||||
Reference in New Issue
Block a user