# Append journal recovery checks positioning before repair

Java, Rust and C++ now finish their append-position seek before trimming a torn
append-journal suffix. Previously, that seek could refuse the open after the
file had already changed. The correction is confined to recovery: no added
locks, send-path work, sequence policy, disk-format or ABI change. This record
retains the fault reproductions and verification; it does not close the full
all-flavour review or establish a latency improvement.

## Reproduced refusals

Baseline source is 73a3dd06e. Rust's new Linux x86-64 integration executable
interposes lseek and lseek64, matches only the current fixture's descriptor and
injects EIO at the final append position. Both public stores are exercised:
classic FileStore and FIXP FileReplayStore. A 13-byte payload record with a
one-byte tail shrinks from 14 to 13 bytes despite refusing the open; a 28-byte
legacy checkpoint with the same tail shrinks from 29 to 28. Healthy 13-byte
controls remain unchanged. The baseline has four failures and two controls
that pass, with exactly one injected error required per case.

Java's six JUnit cases drive loadIndex with a delegating RandomAccessFile.
The delegate forwards reads and repairs to the original journal descriptor,
so it cannot disturb the writer's process-scoped lock by opening and closing
the journal again. Four torn inputs fail byte-preservation expectations:
payload plus tail, legacy checkpoint plus tail, incomplete header and
incomplete payload. Healthy and empty controls pass.

An independent Linux LD_PRELOAD probe drives the real Java public constructor
with ordinary files and no reflected field or substituted descriptor. It
reproduces those same four changed-file refusals, with lengths 14 to 13,
29 to 28, 1 to 0 and 13 to 0. Each subprocess requires exactly one injected
positioning EIO and the resulting IOException. Healthy and empty controls
also drive the error, then successfully retry, append and reopen.

These ordinary-file probes show premature removal of torn bytes, not loss of
an accepted payload or checkpoint. No physical device failure or power loss
was induced. The first Rust fixture incorrectly expected inbound sequence 1
without a checkpoint; the contract is 0 until one is persisted. That initial
fixture failure and its source are retained separately. The corrected healthy
controls pass against the unchanged runtime.

A follow-up portability check found a test-harness defect: with a symlinked
TMPDIR, the test's logical filename did not match the resolved path reported
by /proc/self/fd. The already-corrected store opened successfully because no
fault was injected, so all six old regression cases failed. The test now
resolves its existing fixture before arming the fault. All six cases pass with
both physical and symlinked temporary roots in both editions, and strict
all-target Clippy stays green. This changes only the test's path matching,
not store behavior; the old executable and failing alias run are retained.

## Correction and regression coverage

Java seeks to the accepted prefix length before setLength. Rust likewise
seeks to that absolute offset before set_len, rather than seeking to the
file's end after repair. These operations preserve the append position when
trimming succeeds. Record validation, incompatible-range refusal and payload
reads still precede any repair, and neither implementation adds a seek.

The checked-in tests require the failed open, the injected error and exact
original bytes. With the fault disabled, they reopen, repair to the accepted
prefix, check recovered counters and payloads, append a new payload and reopen
again. Java also checks the repaired length and append cursor directly through
the already-owned descriptor. Retrying through the public constructors proves
that failed recovery releases the writer fences.

Strict Clippy exposed two missing Safety sections in recently landed test-only
flock and msync interposers. Their documentation now states the descriptor/ABI
and readable-mapping requirements; their implementation and assertions are
unchanged. Both original lint failures are retained.

## Verification on hp

All foreground builds and suites use host-cpu-slot. Functional fault tests
used isolated CPUs; broader suites used shared CPUs. Other functional work
could overlap, so these are correctness checks, not latency measurements.
Outputs and temporary files remain under the session build root on /home.

Rust 1.92.0, offline, default features:

| Check | Development edition | Open edition |
| --- | --- | --- |
| New recovery faults with six test threads | 6 passed | 6 passed |
| Full crate suite | 1,070 passed | 926 passed |
| Strict Clippy 1.93.1 with all targets and warnings denied | passed | passed |

Both full suites retain one existing ignored builders doctest, with no failed
tests. Totals include the focused tests and must not be added as unique cases.
The development count is 1,014 library cases plus 56 others; open is 886 plus 40.

After integrating the other session's audit ownership repair (cf0622267),
both full suites pass again: 1,074 development cases and 930 open cases.
Each adds three audit-ownership integration cases and one compile-fail
doctest. The same builders doctest is ignored, and no test fails. Final logs
and a complete Rust build-input hash manifest are retained separately.

OpenJDK 17.0.20.1 compiles Java for release 8. The controlled JUnit regression
and the external constructor probes each pass all six cases after correction.
Normal Maven builds use separate fresh development/open target directories and
their own JNI library through the supported libhft.build.dir property; native
availability is required rather than waived. Each edition passes all
181 selected store/session cases in each of the three configured Surefire
executions, with zero failures, errors or skips. These are the same selected
cases repeated by Maven's test-selector override, not 543 distinct tests.

## C++ correction and verification

The independent C++ public-API probe confirms the same defect in FileJournal.
For both classic FIX and FIXP, four torn inputs change before refusal and the
two healthy/empty controls preserve bytes: eight failures and four controls
overall. The new checked-in class-level regression also fails against the old
helper at its first torn-payload case, with 14 bytes reduced to 13. The exact
old class-test executable is retained; core output was disabled for that
deliberate assertion failure.

Previously, recovery called TruncateFile, whose own seek followed truncation,
then had a further final seek. Fixing only the latter would be insufficient.
TruncateFile now positions first, before ftruncate or the Windows CRT resize;
recovery does not seek again after a successful trim. The helper's successful
append-cursor contract is retained for recovery, reset and write-fault rollback.
There is one fewer explicit seek when recovery repairs a tail, but this is a
cold-path simplification, not a measured latency gain.

On Linux, all six checked-in refusal cases now preserve the bytes and retry,
append and reopen successfully. The independent public-API probe passes all
12 classic/FIXP cases with exactly one injected seek error each. GCC 14 normal
Makefile builds pass nine relevant binaries in each edition: file-store class,
async-file runner, FIXP file replay, native FIXP store runtimes, replay callback,
both QuickFIX file layouts, QuickFIX file sessions and real-socket initiation.
These cover reset, partial-write rollback, inherited writer-lock lifetime and
normal recovery as well as the new failure ordering.

Clang 21.1.8 builds the complete file-store class test with address/undefined
behavior sanitizers and leak detection in both editions. Both pass with six
injected seeks and no sanitizer reports. The Java JNI libraries are rebuilt
against the corrected header through normal Maven in both editions; all 181
selected cases pass again in each of the three executions, with no skips.
The two changed C++ files pass the public export scrub with zero findings.

## Follow-up across flavours

The .NET structure owner has a request in docs/WIP_AGENT_CLAIMS.md:
FileStore.LoadRecords in MemoryStore.cs also calls SetLength(recordStart)
before querying Length and setting Position in its torn-record branches.
A later ordinary-file public-constructor probe now reproduces the .NET failure
in both editions: EIO from fstat64 after a successful ftruncate64 refuses open
after already removing the torn bytes. It drives the post-repair Length query,
not a Position setter assumed to issue lseek. The four torn inputs change
14 to 13, 29 to 28, 1 to 0 and 13 to 0 bytes; healthy/empty controls open normally.
The source is still reserved and not changed here. Sources, SDK/runtime details
and logs are in `~/archive/libhft/dotnet-journal-recovery-io-20261004/`; the
reset follow-up record retains the cross-runtime status.

## Retained evidence and limits

`~/archive/libhft/journal-recovery-io-20261004/` holds the baseline source pin,
exact old Rust executable, old Java constructor classes, the C++ constructor
executable, injector/probe sources, run scripts, compiler versions, full logs,
Surefire reports, sanitizer output and verified source/JNI hashes. It survives session build
cleanup. Probe-created fixtures are unique and removed only by their probes.

This patch does not make recovery transactional across later truncation,
checkpoint-write, synchronization or writer-start failures. Optional Rust
venue/IB verbs features, Windows fault injection, real device failures, NIC
transports and the entire monorepo conformance matrix were not tested here.
Durable FIXP empty-replacement semantics and the older missing-record
observation remain distinct findings.
