# C++ sparse replay and callback lifetime review — 2026-10-04

The C++ session resend path retained decoded fields across the outbound GapFill
application callback. A callback that reads another QuickFIX record overwrites
that store's shared read buffer; replacing, clearing or closing a store can also
invalidate the pending replay. The session now reacquires and decodes the pending
record after the GapFill. An absent, corrupt or administrative replacement starts
a new gap at the pending sequence. The already-sent gap remains unchanged.

Built-in replay previously probed every integer in a requested range, including
missing records. Optional `NextReplaySeq` now searches retained ring slots or the
QuickFIX ordered map. Session replay and the store-neutral replay walker use it
without retaining iterators or borrowed views across callbacks. Journaled and
runtime QuickFIX wrappers forward the lookup. Save paths and positive-capacity
memory-store layout are unchanged.

Legacy stores remain scalar. A custom store overriding `FindOutbound` must not
inherit an index that describes different storage, so member-owner detection
rejects incompatible inherited hooks. Exact NullStore alone has an empty shortcut;
subclasses supplying payloads do not inherit it. Overloaded and static methods
fall back rather than failing to compile. C++11 remains supported.

For ring intervals no wider than capacity, the original scalar loop already has
bounded work and avoids index overhead. Wider intervals use sparse lookup. This
choice followed a dense-replay gate: an initial always-indexed walker increased
short dense replay from about 96 ns to 154 ns. The final capacity-based path restores
the dense result. The indexed helper is outlined so runtime-supplied bounds do
not pull its larger loop into the scalar caller. Ring index work is bounded per gap, not asserted linear over all
gaps; alternating far-apart retained records can require repeated ring scans.

## Regression evidence

Baseline: `925b9e988cfd40134b0b8c24ff2aecfa75885dd9`.
Owned worktree: `codex-review-cpp-replay-1004`, locally and on hp.

- New public-session callback tests against the baseline SessionProtocol fail 24
  checks for clear, replacement, corrupt/admin replacement and empty markers.
  The same tests pass with the candidate.
- A baseline QuickFIX session callback that reads a different record fails its
  field-identity assertion. Both runtime profiles pass read-other and close-store
  callback cases with the fix. Closing the store also causes the existing inbound
  checkpoint fault; tests require that fault and the correct two GapFills.
- Differential event tests cover nonmonotonic saves, ring overwrite, empty markers,
  reopen, unreadable indexed records, callback mutations and cancellation at each
  event. Unsigned endpoints include UINT64_MAX. Finite preflight and hard lookup
  budgets precede huge-range cases, so a traversal regression cannot hang a test.
- Public session tests include a 2^40 span, corrupt/admin filtering, unchanged
  outbound frontier, custom-store inheritance and an unattached store. Runtime
  QuickFIX wrapper tests include a sequence above UINT32_MAX for the C++ profile.

Validation includes 14 focused development suites, seven open-edition suites,
and three AddressSanitizer/UndefinedBehaviorSanitizer suites, with no reported
skips. Development suites include the pro mmap variants, store selection, real
sockets, application sends and backpressure. Sanitizers cover replay callbacks,
QuickFIX session callbacks and session reliability. The 14 development suites were
rebuilt after the upstream C++ build-option rename. The final indexed-helper
layout also passes the seven affected store/callback suites, two open-edition
suites and the two affected sanitizer suites, with no reported skips. C++11/C++17 compatibility probes and a no-exceptions/no-RTTI
session reliability build are also retained. Optional external QuickFIX engines,
Windows and managed-language suites are outside this batch.

## Measurements

hp, GCC 15, `-std=c++17 -O3 -march=native`, exclusive host CPU lease. Sparse probe:
one warmup, seven measured samples, four retained application records at the end of
ranges 1,000 / 100,000 / 10,000,000. Times below are medians for the last range.
This is an in-process replay benchmark, not a network or production latency claim.

| Path | Before | After |
| --- | ---: | ---: |
| MemoryStore replay | 9,755,289 ns | 70 ns |
| FileStore replay | 85,473,713 ns | 117 ns |
| MmapStore replay | 6,973,335 ns | 73 ns |
| QuickFIX C++ store replay | 45,530,803 ns | 1,574 ns |
| QuickFIX/J store replay | 35,778,309 ns | 1,596 ns |
| Memory-backed public session | 19,517,897 ns | 1,364 ns |
| Unattached public session | 2,942,616 ns | 173 ns |

Before/after event counts, payload checksums and the entire emitted FIX wire match
at all three spans. The ten-million memory session emits five frames / 632 bytes;
the unattached session emits one frame / 128 bytes.

Three paired dense/alternating runs with compile-time bounds also match their checksums. Capacity/span 64:
dense ~96 ns before / ~95 ns after; alternating ~74.3 / ~74.8 ns. Capacity/span
4096: dense ~6.26 us both; alternating usually ~7.30 us both (one final sample
~6.64 us). A separate runtime-bound probe keeps the endpoint volatile and passes
it to a non-inlined replay call. Final runtime dense medians are ~134.5 ns for
64 sequences and ~7.96 us for 4096 sequences, both matching baseline. Runtime
alternation at 4096 is also ~7.96 us. At 64 sequences it retains a small cost:
~115.8 ns before versus ~126.5 ns after, about 11 ns per complete replay. No
blanket no-regression claim or broad dense-performance gain is made.

## Retained evidence

Archive (mirrored and hash-verified on both hosts after publication):
`~/archive/libhft/reviews/2026-10-04-codex-review-cpp-replay-1004/`.
Includes exact probe sources/scripts, baseline/candidate timings and wire output,
independent source review, failed initial attempts, final build/test logs, source
diffs and final revision/publication records. Build products stay outside worktrees;
heavy jobs use host leases and process/memory checks. Final make jobs use `-j8`
and one LTO worker per linker to stay below the machine's concurrency cap.
