# Rust metrics callback stop/drop and deferred external joining

## Reproduced baseline

Unchanged production source at `374c02ae01123c1c93bdb75aeb93237f08170085`
passes four public external-stop controls and fails twelve callback shutdown
regressions. A separate scratch crate depends on that exact checked-out engine;
all 353 tracked Rust input hashes are checked before building. The original
fixture source, Cargo manifest/lock, source manifest, pin, exit 101 and complete
failure log are retained. The sixteen cases finish in 0.63 seconds.

TCP/Unix and classic/FIXP callbacks that stop or drop their own MetricsEndpoint
panic with `failed to join thread: Resource deadlock avoided (os error 35)`.
The fixture catches that panic locally without replacing the process panic hook,
so the actual worker diagnostic remains in the log and every case finishes.
The stop path takes the JoinHandle before joining. Its failed self-join consumes
the handle: a later external stop returns while the callback is still gated.
All four deferred-join cases independently fail that completion barrier.

[Rust's JoinHandle documentation](https://doc.rust-lang.org/std/thread/struct.JoinHandle.html#method.join)
warns that joining from the same thread can panic; a dropped handle detaches the
worker. This is an actual safe public-API reproduction, without interposition,
source hooks, descriptor damage, resource exhaustion or injected kernel errors.

## Contract and implementation

MetricsEndpoint::stop first publishes a stop request as before. It now compares
the stored worker id with the current thread id before taking the handle. The
worker's own capture callback returns from stop without joining itself and keeps
the handle available for an external stop/drop. A subsequent call from another
thread joins the complete worker, including its listener/pathname teardown.
Repeated self-stop and repeated external stop remain harmless.

A callback can instead take and drop the endpoint itself. Its Drop requests stop
without self-joining; destruction then detaches the handle. The serving thread
still owns the listener, renderer and running flag until capture returns, then
performs its normal exit and cleanup. No response is emitted after that stop and
no completed scrape is counted. Worker lifetime is not the endpoint object's
lifetime in this specific drop case; the docs say so.

Only stop/drop joining and their public docs change in production. No new
worker, synchronization field, dependency or hot-path work is added. Accept retry
pacing, client deadlines, renderer/schema behavior, complete-response counting
and Unix inode/directory ownership stay unchanged. There is no FIX protocol
semantics change or other runtime edit.

## Regression coverage

Sixteen integration cases use all four combinations of TCP/Unix and classic/FIXP
metrics APIs: four ordinary external-stop controls, four repeated callback-stop
cases, four callback-drop cases and four external joins after callback stop.
Callbacks use only the public endpoint APIs through a synchronized owning slot.
Healthy controls require a real HTTP 200 scrape and exactly one scrape count.
Callback shutdown requires no panic, zero response bytes and zero completed
scrapes where the endpoint remains observable. The tests require listener
release/rebind, owned Unix-name removal and empty fixture directories.

Deferred-join callbacks report the stop outcome, then wait on a finite release
channel. A separate thread calls external stop; it must not report completion
before the test releases capture. After release the external join must complete,
with capture's completed marker visible and normal cleanup performed. Finite
fixture waits and a release-on-drop guard keep assertion failures bounded. No
test is ignored, no global panic hook is installed, and RAII directories delete
fixtures even after failure. The baseline and positive fixture sources are
identical.

## Verification

| Gate | Result |
|---|---|
| Actual unchanged baseline | 4 controls pass; 12 callback/retirement regressions fail |
| Focused metrics selection | 68 passed, 0 failed, 0 ignored |
| Complete default Rust stage | 1,877 passed, 0 failed, 1 existing ignored builders doctest |
| Complete open Rust stage | 1,690 passed, 0 failed, the same 1 ignored doctest |
| Strict Clippy, all targets, -D warnings | Passed in both editions |
| Rustfmt | Changed stop method and the entire new integration file match |
| Public-export scan | 2 owned files; 0 findings |
| API inventory / generated coverage --check | Passed; API scores unchanged |
| Integrated PGO/BOLT tuned-profile dry-run case | 1 passed, 0 skipped; no build performed |

The selected Rust conformance stages are complete Cargo suites for their editions.
Each selected stage reports one ran, zero failed and zero skipped; PARTIAL means
other monorepo stages are unselected, not that a failed Rust test was ignored.
The existing ignored builders doctest is reported separately. All new callback
cases run again within each edition's complete suite.

Owned builds/tests run serially on HP at Cargo -j1 and one test thread, offline,
with disabled development/test debug symbols and absolute session build/target
roots. Before every stage an 8 GiB available-memory floor and compiler/runtime
check apply. Foreign Java/C++ builds make the guard stop only the owned build and retry
after the host clears. Initial HP conformance attempts are deliberately
interrupted while seeking a free slot; those logs are retained separately and
never counted as a pass. Scan verifies the same 354 input hashes, but its owned
full-suite guard is canceled while waiting on other builds, before Cargo runs.
Both completed full stages run on HP after its slot clears. No heavy build/test
runs on BEELINK2 beside other sessions. Rust/Cargo 1.92.0 runs the tests; the
isolated 1.93.1 toolchain runs strict all-target Clippy in both editions.

Candidate `9ed5f4219aa4fc5bdee06a1aa65b230f005e8549` contains the fix and
all sixteen unchanged fixture cases. All 354 tracked Rust input hashes match HP
and the local publication worktree. Publication integrates two changed Rust paths: the PGO/BOLT Makefile recipes and
their Python dry-run test. The other 352 tracked Rust paths, including every
Cargo source/build/manifest and both owned metrics files, stay byte-identical.
The new tuned-profile dry-run case passes once with zero skips. The conformance
runner changes only an unrelated .NET filter, leaving the two Rust stages and
commands unchanged. No unchanged Cargo suite is repeated for these build-target
changes; final publication manifests retain all 354 path/hash pairs.
The tested candidate remains recoverable through a named Git bundle ref.

Pins, manifests, actual failure diagnostics, completed test reports, commands,
Clippy logs and candidate/publication Git bundle remain under
`~/archive/libhft/rust-metrics-callback-shutdown-20261011` on both hosts.
SHA256SUMS checks kept evidence. API inventory and generated coverage checks pass;
Rust remains 82 supported / 11 n/a API entries, 27/27 session behaviors and
43 yes / 2 partial / 1 n/a subsystem rows.

## Boundaries

An external stop still waits for capture code to return; it cannot cancel arbitrary
callback code. Self-drop cannot synchronously join the thread currently executing
the drop. It requests shutdown and leaves cleanup to that worker after capture
returns; an embedding application must retain a handle and stop externally when
it needs a synchronous completion barrier. Existing Unix directory leases and
ownership refusal/cleanup rules still apply, including a recoverable stale node
when the lease cannot be obtained. Callback panic hooks remain visible and
panic=abort still terminates the process. This slice proves kernel TCP/Unix
behavior on Linux; no live venue, acceleration deployment or cross-runtime gate
is claimed. Production endpoint and venue/FIXP schema certification stay partial.
