Driver health is published AFTER teardown, so GetHealth() can report Healthy with a disposed client #517
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the MTConnect Task 9 code review (PR #506), deliberately not fixed there because MTConnect follows the existing fleet convention — fixing one driver alone would create inconsistency.
Symptom
GetHealth()can report Healthy while the driver's client is already disposed.Cause
ShutdownAsyncand theInitializeAsyncfailure path bothawait TeardownCoreAsync()— disposing the client and clearing state — before callingWriteHealth(...). AGetHealth()landing in that window observes the stale, pre-teardown health value.This is a fleet-wide ordering pattern, not an MTConnect regression:
ModbusDriver.cs:247-249has the identical teardown-then-write sequence.Suggested fix
A fleet-wide pass: write a transitional /
Degradedhealth value before disposing, so the race window leans conservative rather than optimistic. Audit every driver for the same ordering while doing it.Resolved — master
3207089f(commit2d20aeb7).The symptom was real; the mechanism in the issue was not
ShutdownAsynccannot race.DriverInstanceActorcalls it fromPostStop— actor thread, after the mailbox has drained — so the health poll is serialised against it. The one class with a lock-protected concurrentGetHealth,Core/Hosting/DriverHost, has no production call site at all (it is a "Phase 1 scaffold" for the Tier C isolation deleted in #522), and it removes the driver from its registry before calling shutdown anyway.InitializeAsynccan. It runs on a fire-and-forgetTask.Run(DriverInstanceActor.cs:567), so the health-poll tick keeps callingGetHealth()throughout.MTConnectDriverwritesHealthy, then starts its sample stream and probe loop — either can throw into acatchthat disposes the client and only then writesFaulted. That is this issue's claim, made concrete, and it is the only reachable window in the fleet.The suggested fix would have made it worse
Applied as a move, that inverts the terminal state. Teardown drains in-flight poll/probe loops, and those loops write health as they unwind — so a late loop can overwrite the shutdown value with
Healthyand leave that at rest, with every connection closed. The trailing write is load-bearing, not an oversight.The correct shape is a bracket: conservative value before, terminal value after. Applied to MTConnect's init-failure path. The shutdown paths keep their single trailing write, since the window is not reachable there — and the rule plus the reachability table are now documented on
IDriver.ShutdownAsyncso the ordering is not "fixed" on the wrong premise later.One unambiguous defect, deterministic rather than racy
CalculationDriver.GetHealth()returned a hard-codednew(DriverState.Healthy, …)that consulted nothing — Healthy beforeInitializeAsync, afterShutdownAsync, and afterDispose. No race required. Its existing test was namedHealth_is_always_Connectedand asserted exactly that; removed and replaced with a lifecycle test.Verification
The ordering test samples
GetHealth()from inside the client'sDispose— every other test in that file asserts state onceInitializeAsynchas returned, by which time the window has closed, which is why the defect survived: the terminal value was always right and only the ordering was wrong. Falsified by deleting the pre-teardown write and confirming red.Audited and not changed
The fleet-wide pass found the teardown-then-write shape in ~9 drivers'
ShutdownAsyncand in exactly oneInitializeAsynccatch block (MTConnect). The nine were left alone deliberately, per the reachability table above.