# Transport lifecycle review — 2026-10-10

Reviewed from `feffa5bddb7024f7fa782d3d1560134a901d5103` in the isolated `codex-review-transport-1010` worktree.
Publication base: `3b55559efde54c24d830d82d5cf60d71eb08c4ff`. Parallel agents reviewed C++, Java and Rust;
the root serialized every heavy build/test through `tools/host-cpu-slot`, with
at most 16 jobs and `-flto=1` where LTO was enabled. No .NET source was changed.

## Changes and reproduced failures

* **C++ journal/audit startup:** failure of the real `pthread_create` underneath
  `std::thread` previously left a journal open before its owner published recovered
  counters. Cleanup could checkpoint inbound sequence 1 over recovered sequence 17,
  and the failed object retained the writer lock. Audit startup leaked its file
  descriptor and retained buffers. Failed starts now release ownership and buffers
  without a journal checkpoint; callers receive a normal refusal and can retry.
* **C++ acceptor compaction:** removing another connection called protocol `Init`
  on the surviving connection. That cleared its learned peer/store binding, open
  send batch and protocol counters. Copied outbound builder pointers also retained
  addresses in the vacated slot, allowing its next Logon to overwrite a prepared
  application message. Slot movement now rebinds endpoints and all five owned-buffer
  cursors while preserving the live protocol state and caller-owned buffers.
* **C++ build compatibility:** an unconditional `catch` in the metrics publisher
  prevented exception-disabled acceptor compilation. Compiler exception guards
  retain the normal exception-enabled cleanup and permit the disabled build.
* **Java FIXP address admission:** an unresolved address threw after opening an
  unowned channel; runner close could not release it. A bounded 32-attempt Linux
  regression observed all 32 sockets retained. Admission now returns `CONFIG_ERROR`
  before opening a socket or spending reconnect attempts; valid reinitialization
  still establishes and sends application traffic.
* **Java FIXP callback lifecycle:** an outer multi-acceptor step overwrote the
  result of a callback that reinitialized the owner and received a configuration
  refusal. Both peer-pump orders now retain the replacement lifecycle's result
  and avoid cleaning up its replacement children.
* **Rust TLS retry:** appending queued frames changed the length passed to
  `SSL_write` after WANT_READ/WANT_WRITE. Retained handles now share the original
  retry length; subsequent queued bytes follow after that write completes.
  [OpenSSL requires the retry data and length to remain identical](https://docs.openssl.org/3.0/man3/SSL_write/),
  even with moving-buffer mode. Real TLS 1.2 and 1.3 loops observed the old retry
  growing from 1,095 to 2,190 bytes. OpenSSL 3.5.5 delivered the frames anyway:
  these real-loopback failures proved a contract violation, not observed wire loss
  on this library. Transparent observation delegates the actual OpenSSL call;
  deterministic strict retry cases additionally exercise WANT_READ and batching.
* **Rust graceful stop:** an acceptor whose only sessions were already
  `LOGOUT_SENT` closed them immediately when graceful stop began. Existing exchanges
  now wait for peer replies or their original timeout, without duplicate Logout
  or timer restart. Forced cancellation and zero timeout remain immediate.

The C++ fixes act during setup/removal. Compaction also avoids reinitializing the
rendered header. The Rust TLS change stores one retry length and adds no copying
or allocation. This pass makes no measured latency claim.

## Verification

All rows below are checked against preserved logs, named test inventories and
source hashes by `scratch/summarize-verification.py` in the archive.

| Gate | Result |
|---|---|
| Background-start original target | 2 controls passed; 10 regressions failed |
| Background-start final / ASan+UBSan | 12 passed in each |
| Acceptor-compaction original target | 32 controls passed; 64 regressions failed |
| Compaction final / ASan+UBSan / exceptions disabled | 96 passed in each |
| Exceptions-disabled healthy journal, audit and metrics probe | Passed |
| C++ open edition | 157 test binaries passed; no reported skips |
| C++ affected private integrations | 6 binaries passed; no reported skips |
| Java focused original | 74 passed; 9 assertion failures; 2 unresolved-address errors; no skips |
| Java focused final | 85 passed; no failures/errors/skips; native required |
| Java development | 6322 unique cases passed; native required |
| Java open edition | 5854 unique cases passed; native required |
| Rust focused original | 4 controls passed; 7 regressions failed |
| Rust focused default / no-default / open with service packs | 11 passed in each; no ignored cases |
| Rust full default / no-default | 1,601 passed in each; one existing ignored documentation example |
| Rust Clippy default / service packs | All targets passed with warnings denied |
| Export scan | All 14 changed engine files; 0 findings |

Compaction uses real sockets with FAST/SAFE modes, polled/pollless service,
MemoryStore and an owning handle around a heap-stable real FileStore. The handle
moves ownership without moving a journal worker's address. Tests verify wire bytes,
live replay, durable reopen, four changed-identity forms, batches, metrics, completed
and unfinished composition, caller buffers, same-peer reset, partial-frame recovery
and vacant-slot reuse. The 32 control cases pass before the fix.

Private C++ checks cover session reliability, store selection, mmap store,
acceptor HA store, standby store and the fake transport lifecycle tests. Native
hardware transport operation is not claimed. Java's 80 callback cases cover four
owner/pump roles, two formats, both keepalive settings and five lifecycle actions.
Its descriptor check ran on Linux. Java broad XML inventories retain the preceding
published pass's complete coverage and add 44
development and 44 open-edition cases. Three initially
skipped counter-disabled tests run in the explicit disabled-counter execution;
the final inventories contain no skips. JNI stayed in its normal build-root
location, and `libhft.require.native=true` was required throughout.

Runtime verification was on Linux x86-64 with GCC 15.2, Rust 1.93.1, OpenSSL 3.5.5
and JDK 21. Full toolchain output and commands are retained. Thread-create refusal
injection is Linux/exception-enabled; the exception-disabled checks cover healthy
operation and compilation, not graceful recovery from an uncatchable thread-start
failure. Windows runtime behavior was not tested in this pass.

## Evidence and publication

Local archive: `/home/yann/archive/libhft/reviews/2026-10-10-codex-review-transport-1010`.
It is mirrored at the same path on `hp`; both manifests are verified before worktree
cleanup. The archive contains original and final source, complete reports, a review
git bundle and `MANIFEST.sha256`. `scratch/evidence/tested-source.sha256` identifies
every changed engine file; source hashes are rechecked after the publication rebase.

Reproduction drivers are `scratch/root/thread-start-{baseline,final}.sh`,
`scratch/root/compaction-baseline.sh`, `scratch/root/cpp-focused-final.sh`,
`scratch/rust/{baseline.sh,final-gates.py}`, `scratch/java/{baseline.sh,focused-final.sh}`
and `scratch/full-main.sh`. Run from a worktree with a session-specific build root
through the host slot/guard. Java's exact baseline/final and coverage-floor checker
is `scratch/java/check-reports.py`.
The compaction, Java and Rust baseline drivers expect their snapshots restored
first in a disposable session worktree: `scratch/cpp/compaction-baseline-source`,
`scratch/java/baseline-source` and `scratch/rust/baseline-source`, respectively.
They do not rewrite production source themselves.
The thread-start baseline instead selects its saved original headers through the
compiler include path. Retained baseline hashes identify those source states;
the tests used for the documented before/after comparisons are identical.

Earlier attempts are preserved separately: the startup fixture initially assumed
capacity metadata cleared on refusal; the first compaction fixture tried to move
FileStore directly; a partial-frame control initially counted only unparsed bytes;
and one redundant original-code build followed an incorrect patch strip count.
The documented final counts use corrected, identical before/after test sources.
The exception-disabled compiler failure is retained under `scratch/cpp/noexcept-original`.

Code commits in this batch:

```
1490992bc fix(rust): retain TLS write boundaries across queued appends
ddee71e65 fix(rust): finish existing Logout exchanges during graceful stop
56e4f530c fix(java): refuse unresolved FIXP addresses before opening sockets
d0acf96f4 fix(java): preserve FIXP owner lifecycle changes from callbacks
b82b43c57 fix(cpp): preserve session state across startup failures and slot moves
ed9c380a9 fix(cpp): allow metrics publisher builds with exceptions disabled
```
