feat(ha-entity-snapshots): entity-bearing checkpoint write path for jobs (task 2.2)

write_snapshot now Cap'n-Proto-encodes every retained job under nested
table+per-entity RTTI_LOCK_READ (table lock alone does not exclude a
concurrent per-entity writer -- design.md's central correctness finding),
appends length-prefixed job frames after a new format_version marker, and
fixes a previously-unchecked fdatasync return. Any failure anywhere in the
path (encode, write, sync) discards the whole checkpoint -- no partial
file at the final path, proven by a real mid-encode-loop fault-injection
test (forces one specific job's encode to fail among N, asserts nothing
survives).

Also fixed two pre-existing bugs in task 1.1's test fixture: terminal-state
seeded jobs need a non-zero finish_time or job_should_retain's retention
window silently excludes them from any checkpoint; the fixture needs
xstring_table_init or every seeded job's submit/start_info silently
decodes as NULL.

Round 1 of this task also touched load_snapshot/persist.h to make the
round-trip tests pass end-to-end, but review caught a live hazard: it
advanced current_lsn/checkpoint_lsn BEFORE the entity-decode loop ran, so
a decode failure still left the position advanced -- reachable today
through the unmodified ha_handle_snapshot_chunk production path, not
hypothetical. Remediated by fully reverting load_snapshot/persist.h to
byte-identical pre-task-2.2 state and rewriting the round-trip tests to
parse write_snapshot's raw on-disk bytes directly (read_Job/decode_Job)
instead of round-tripping through persist_load_snapshot -- proves the
write side without depending on load_snapshot, which stays task 5.2's
scope entirely, not started here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
This commit is contained in:
Rongsong Shen 2026-07-01 06:30:28 +00:00
parent 0f344da89a
commit 5929af1078
4 changed files with 453 additions and 54 deletions

View file

@ -273,6 +273,15 @@ struct persist_s {
* production. */
pthread_t test_last_fdatasync_thread;
atomic_int test_last_fdatasync_thread_set;
/* Fault-injection seam (ha-entity-snapshots task 2.2): force the Nth
* (0-based, in retained-job walk order) job's checkpoint encode to fail
* inside write_snapshot, proving a genuine mid-loop encode failure
* discards the WHOLE checkpoint rather than skipping just one job.
* Stored as index+1 so the zero-initialized default (0) means
* "disabled" — mirrors test_force_fsync_fail's zero-init-safe
* convention (no explicit persist_init reset needed). Compiled out of
* production. */
atomic_int test_force_job_encode_fail_at;
#endif
pthread_t fsync_thread;
pthread_mutex_t fsync_mutex; /* guards the fsync timer wait */
@ -584,6 +593,11 @@ pthread_t persist_test_last_fdatasync_thread(persist_t* p);
* compaction hazard where a committed marker's segment is unlinked. Returns 0
* on success, negative errno on I/O failure. Compiled out of production. */
int persist_test_force_new_segment(persist_t* p);
/* Arm/disarm the job-encode fault-injection seam (see the struct field
* comment above). Pass a negative index to disable. Lock-free atomic
* write; compiled out of production. */
void persist_test_set_force_job_encode_fail_at(persist_t* p, int index);
#endif /* XCE_TESTING */
/* Checkpoint */