Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs
T
Joseph Doherty b815446940 refactor(drivers): delete GetMemoryFootprint / FlushOptionalCachesAsync (#525)
Both `IDriver` members had no production call site. Their only consumer was
`MemoryTracking`, deleted with the driver-tier recycle machinery in #522, and
`GetMemoryFootprint`'s doc-comment claimed "polled every 30s by Core" — which
had not been true since v3, even before #522 removed the poller.

Owner decision on #525 was (a) delete rather than (b) build the process-wide
watchdog that would consume them. The driver-side implementations are in git
history and were mostly reusable; what was always missing is the trigger, and a
member nobody calls is how the misleading doc-comment survived this long.

Removed from `IDriver`, all 12 driver implementations, and every test stub —
plus the support code that existed only to feed them, which is the part a
member-only deletion would have left behind as dead weight:

- MTConnect's three per-entry byte-weight constants, its cached
  `ProbeModelDataItemCount`, and `CountDataItems` — a full device-tree walk run
  on every cache fill to populate a number with no remaining reader.
- OpcUaClient's `_discoveredNodeCount` field and the discovery-pass write to it.
- MQTT's `ApproxBytesPerAuthoredTag`.

Two behavioural notes, neither a change in what the software does:

- `AbCipTemplateCache` now has no production reset path. Its only caller was
  AB CIP's flush, which itself had no caller — so nothing changes except how
  visible the fact is. Recorded on the type, along with the correction that its
  comment's claim about a device `ReinitializeAsync` also flushing it was never
  true. A UDT redefined on a live controller keeps its stale shape until the
  driver instance is rebuilt.
- MTConnect's `/probe` cache is still dropped, by `InvalidateProbeModel()` on an
  Agent restart. That is now `internal` rather than `private` so the tests which
  used the flush purely to empty the cache drive the real path instead — they
  now cover a case that actually occurs in production. Several unrelated doc
  comments cited `FlushOptionalCachesAsync` as the canonical CAS example and are
  repointed to it.

Tests: the ones that existed only to assert on the removed members are deleted
(3 OpcUaClient, 3 Galaxy, 2 MQTT, 1 MTConnect, 1 AbCip). Worth noting one was
vacuous — `FlushOptionalCachesAsync_resets_footprint_counter` asserted a
footprint of 0 before the flush and 0 after with no discovery in between, so it
would have passed against a flush that did nothing at all.

Full suite green except 7 failures verified identical on master before and
after (4 AbLegacy integration with the compose profile down, 3
Host.IntegrationTests), checked by name rather than assumed.

Docs corrected where they described this as live: `DriverLifecycle.md` (which
carried the #525 placeholder), `drivers/AbCip.md`, `drivers/AbLegacy.md`, and
`v2/driver-stability.md` — whose "polls this every 30 s" + per-driver
`DriverConfig` memory budget were never built as written and are now banner-ed
as design intent rather than description.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-31 09:07:11 -04:00

76 lines
4.1 KiB
C#

using System.Collections.Concurrent;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Cache of UDT shape descriptors keyed by <c>(deviceHostAddress, templateInstanceId)</c>.
/// Populated on demand during discovery + whole-UDT reads.
/// <para><b>Nothing in production clears it.</b> Its only caller was
/// <c>AbCipDriver.FlushOptionalCachesAsync</c>, deleted in Gitea #525 as an unconsumed
/// <c>IDriver</c> member — but that member never had a caller either, so this is not a change in
/// behaviour, only in how visible the fact is. The comment here used to claim a device
/// <c>ReinitializeAsync</c> also flushed it; no such call ever existed. A UDT redefined on a live
/// controller therefore keeps its stale shape until the driver instance is rebuilt.</para>
/// </summary>
/// <remarks>
/// Templates are decoded by <see cref="CipTemplateObjectDecoder"/> (CIP Template Object
/// class 0x6C, <c>GetAttributeList</c> + <c>Read Template</c>); the live reader is
/// <see cref="LibplctagTemplateReader"/>, and <see cref="AbCipDriver.FetchUdtShapeAsync"/>
/// populates this cache on first fetch.
/// </remarks>
public sealed class AbCipTemplateCache
{
private readonly ConcurrentDictionary<(string device, uint instanceId), AbCipUdtShape> _shapes = new();
/// <summary>
/// Retrieve a cached UDT shape, or <c>null</c> if not yet read.
/// </summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <returns>The cached UDT shape, or <c>null</c> if not yet read.</returns>
public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) =>
_shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null;
/// <summary>Store a freshly-decoded UDT shape.</summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <param name="shape">The UDT shape to cache.</param>
public void Put(string deviceHostAddress, uint templateInstanceId, AbCipUdtShape shape) =>
_shapes[(deviceHostAddress, templateInstanceId)] = shape;
/// <summary>Drop every cached shape. Retained as ordinary cache API + the tests' reset seam;
/// see the type remarks for why no production path calls it.</summary>
public void Clear() => _shapes.Clear();
/// <summary>Count of cached shapes — exposed for diagnostics + tests.</summary>
public int Count => _shapes.Count;
}
/// <summary>
/// Decoded shape of one Logix UDT — member list + each member's offset + type. Populated
/// by <see cref="CipTemplateObjectDecoder.Decode"/> from a Template Object response buffer
/// read via <see cref="LibplctagTemplateReader"/>.
/// </summary>
/// <param name="TypeName">UDT name as reported by the Template Object.</param>
/// <param name="TotalSize">Bytes the UDT occupies in a whole-UDT read buffer.</param>
/// <param name="Members">Ordered list of members, each with its byte offset + type.</param>
public sealed record AbCipUdtShape(
string TypeName,
int TotalSize,
IReadOnlyList<AbCipUdtMember> Members);
/// <summary>One member of a Logix UDT.</summary>
/// <param name="Name">Member name as reported by the Template Object.</param>
/// <param name="Offset">Byte offset of the member from the struct start.</param>
/// <param name="DataType">Decoded member data type (<see cref="AbCipDataType.Structure"/> for a nested UDT).</param>
/// <param name="ArrayLength">Element count (1 for a scalar member).</param>
/// <param name="NestedTemplateId">For a nested-struct member, the nested UDT's CIP template instance id
/// (low 12 bits of the member info, same encoding as the Symbol Object) so the nested shape can be
/// fetched via <see cref="AbCipDriver.FetchUdtShapeAsync"/>; <c>null</c> for a scalar member.</param>
public sealed record AbCipUdtMember(
string Name,
int Offset,
AbCipDataType DataType,
int ArrayLength,
uint? NestedTemplateId = null);