# .NET metrics worker lifetime: reproduced and independently closed

The correction in `963f8bb1f` passes all four independent public-API probes:
callback Dispose returns in 3 ms on both TCP and Unix, and restart refuses
the old worker until its capture exits. Maximum concurrent captures remain
one. This closes the reproduced endpoint lifetime defects; it does not
qualify unrelated native or protocol paths.

Before the correction, the .NET metrics endpoint permitted overlapping endpoint
lifetimes when a source callback outlived Dispose's five-second join. Starting
it again created a new worker while the previous worker was still executing
that callback. Separate self-close probes showed a callback waited five seconds
joining its own thread. All four failures were reproduced over the public API,
with no native session, reflection, injected I/O failure or production hook.

## Reproductions

At `7de226832`, TCP and Unix `self-close` probes each measure 5004 ms inside
`TelemetryHttpEndpoint.Dispose()` called by `ITelemetrySource.TelemetryInto`.
The endpoint joins `Thread.CurrentThread` for its full timeout; the two-second
client deadline cannot prevent that wait inside user capture code.

For TCP and Unix `restart`, the first source capture waits on an explicit
test barrier. An external Dispose returns after 5004 ms, but the capture has
not returned. Start succeeds instead of refusing this unretired worker. A real
request to the newly bound endpoint invokes a second capture before the
first barrier is released: the maximum concurrent capture count is two.
The probe releases the first capture, verifies the replacement response and
ordinary later reuse, and exits 1 because the premature restart was accepted.
This directly drives overlapping captures; it is not just an inference from
the missing worker-liveness check.

Dispose clears `_worker` before the timed join, while Start checks only
`_running`. Serve reads the endpoint's mutable listener fields and running flag,
so reactivating those fields can also make an old worker continue into a new
lifetime. The reproduced failure is simultaneous old/new captures; further
continuations depend on scheduling and are not needed to establish it.

## Required correction

Retain the old worker until it exits; refuse restart while it is alive. A
callback Dispose must request shutdown without joining itself, and an external
Dispose must still be able to join after that request clears the running flag.
Do not hold a lifecycle gate across join if a worker callback can acquire that
same gate to stop. Keep any synchronization on endpoint lifecycle operations,
not session sends, receives or snapshot publication.

The structure session implemented the correction in `963f8bb1f`; this review
did not edit its source or tests. The original reservation and release request
are no longer outstanding. The corrected C++ counterpart is `c8395eb93`;
Java already rejects restart while its worker
is still alive and avoids callback self-join.

## Independent closure

At `963f8bb1fe15b1e35914f8b6bb76807897d17478`, the unchanged driver runs
without the expected-failure override. All four probes exit 0:

- TCP and Unix callback Dispose take 3 ms and retain one capture at a time.
- TCP and Unix external Dispose take 5004 ms while the explicit test barrier
  keeps capture blocked beyond the join timeout. Restart is refused while
  that worker remains alive; after release, ordinary reuse succeeds. The
  maximum capture concurrency before and after release is one.

The endpoint retains its worker across a timed-out join, checks its liveness
before restarting, and avoids joining the calling worker. A lifecycle gate
serializes start and stop requests but is never held across a join. No gate
was added to the FIX send/receive paths.

`~/archive/libhft/dotnet-metrics-callback-20261011/closure/` retains the four
logs, source pin, toolchain, normal ProjectReference build output and unchanged
source/probe manifests. The build reports zero warnings and errors. This was
a functional run under a shared CPU lease, not a latency measurement.

## Evidence

`~/archive/libhft/dotnet-metrics-callback-20261011/baseline/` retains all four
logs, exact .NET toolchain output, normal ProjectReference build output,
source pin and unchanged .NET and probe manifests. The build has zero warnings
and errors. Outputs are under the session's external build root and all
processes run under a shared CPU lease on hp.

The reusable driver defaults to corrected exit 0. This baseline explicitly
uses `LIBHFT_METRICS_CALLBACK_EXPECT_EXIT=1`: its successful outer status
means all four expected failures were reproduced, not that lifecycle checks
passed. Each probe prints its actual pass result and exit. This focused
managed endpoint test neither loads hftnet nor qualifies a native session,
an official .NET suite, NIC latency or the complete all-flavours review.
