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
This commit is contained in:
Joseph Doherty
2026-07-31 09:07:11 -04:00
parent 803f4e6efe
commit b815446940
42 changed files with 130 additions and 523 deletions
@@ -590,31 +590,21 @@ public sealed class MTConnectDriverLifecycleTests
driver.GetHealth().State.ShouldBe(DriverState.Unknown);
}
// ---- footprint + flush ----
// ---- dropping the probe cache ----
//
// These used to drive FlushOptionalCachesAsync. Gitea #525 deleted it as an unconsumed IDriver
// member, along with GetMemoryFootprint and the footprint test that only existed to assert on
// it. What survives is InvalidateProbeModel — the Agent-restart drop, which is the only thing
// that empties this cache in production, and which has the same retired-session guard and the
// same "browse must not be left permanently empty" obligation.
[Fact]
public async Task Memory_footprint_grows_when_initialize_populates_the_caches()
public void Dropping_the_probe_cache_keeps_the_observation_index()
{
var (driver, _) = NewDriver();
var (driver, _) = InitializedDriverAsync().GetAwaiter().GetResult();
var before = driver.GetMemoryFootprint();
await driver.InitializeAsync("{}", CancellationToken.None);
var after = driver.GetMemoryFootprint();
driver.InvalidateProbeModel();
// Before init the only config-attributable state is the two authored tags.
before.ShouldBeLessThan(after);
after.ShouldBeGreaterThan(0);
}
[Fact]
public async Task Flush_drops_the_probe_cache_and_keeps_the_observation_index()
{
var (driver, _) = await InitializedDriverAsync();
var before = driver.GetMemoryFootprint();
await driver.FlushOptionalCachesAsync(CancellationToken.None);
driver.GetMemoryFootprint().ShouldBeLessThan(before);
driver.CachedProbeModel.ShouldBeNull();
// The index is required for correctness (it IS the read surface) — never flushable.
@@ -624,12 +614,12 @@ public sealed class MTConnectDriverLifecycleTests
}
[Fact]
public async Task Flush_does_not_wedge_the_driver_the_probe_model_is_refetched_on_demand()
public async Task Dropping_the_cache_does_not_wedge_the_driver_the_probe_model_is_refetched_on_demand()
{
// Task 12's DiscoverAsync reads the probe model. If flushing left no way back to it, a
// cache-budget breach would permanently disable browse on that driver instance.
// Task 12's DiscoverAsync reads the probe model. If dropping it left no way back, an Agent
// restart would permanently disable browse on that driver instance.
var (driver, client) = await InitializedDriverAsync();
await driver.FlushOptionalCachesAsync(CancellationToken.None);
driver.InvalidateProbeModel();
client.ProbeCallCount.ShouldBe(1);
@@ -679,7 +669,7 @@ public sealed class MTConnectDriverLifecycleTests
// it captured. What must not happen is a raw ObjectDisposedException escaping into browse —
// the caller already handles "not connected" and should have exactly one failure mode.
var (driver, client) = await InitializedDriverAsync();
await driver.FlushOptionalCachesAsync(CancellationToken.None);
driver.InvalidateProbeModel();
// Held locally: the gate is one-shot, so the client has already cleared its own reference by
// the time the request is parked on it.
@@ -712,7 +702,7 @@ public sealed class MTConnectDriverLifecycleTests
});
await driver.InitializeAsync("{}", CancellationToken.None);
await driver.FlushOptionalCachesAsync(CancellationToken.None);
driver.InvalidateProbeModel();
var client = built[0];
var staleModel = client.Probe;
@@ -742,22 +732,22 @@ public sealed class MTConnectDriverLifecycleTests
}
[Fact]
public async Task Flush_after_shutdown_is_a_no_op_and_does_not_resurrect_the_session()
public async Task Dropping_the_cache_after_shutdown_is_a_no_op_and_does_not_resurrect_the_session()
{
// Core polls the footprint and asks for a flush on its own schedule, so a flush arriving
// after the driver was torn down is ordinary, not exotic. It must observe the retired
// session and do nothing — publishing a "flushed" copy of it would reinstate a session
// holding an already-disposed client for every later caller.
// The /sample pump can see an Agent restart at any moment, so a drop arriving after the
// driver was torn down is ordinary, not exotic. It must observe the retired session and do
// nothing — publishing a "dropped" copy of it would reinstate a session holding an
// already-disposed client for every later caller.
//
// NOTE: this pins the retired-session guard, NOT the CompareExchange beneath it. Flush has
// no await point, so nothing can be interleaved between its read and its publish from a
// NOTE: this pins the retired-session guard, NOT the CompareExchange beneath it. The drop
// has no await point, so nothing can be interleaved between its read and its publish from a
// single-threaded test; the CAS is defence-in-depth against a genuinely concurrent
// lifecycle call and is deliberately claimed as such rather than as tested behaviour. The
// fetch-side CAS, which does have an await point, is covered by the test above.
var (driver, client) = await InitializedDriverAsync();
await driver.ShutdownAsync(CancellationToken.None);
await driver.FlushOptionalCachesAsync(CancellationToken.None);
driver.InvalidateProbeModel();
driver.CachedProbeModel.ShouldBeNull();
driver.GetHealth().State.ShouldBe(DriverState.Unknown);