fix(controller): checkpoints keep a job's exit code (bug-1867)

The capnp Job struct -- the checkpoint/entity-snapshot form of xce_job_t --
had no exit-info field, so capnp_encode_job() never wrote job.exit_info
and a job restored from a snapshot (instead of WAL replay) came back with
exit_info NULL, for DONE/EXIT and failed jobs alike. bug-1689/bug-1754
made the WAL facts carry the code; anything older than the last
checkpoint's WAL trim still lost it.

- controller.capnp: struct JobExitInfo $C.mapname("job_exitinfo_t")
  { exitcode }, and Job.exitInfo @12 $C.mapname("exit_info"). The
  generator now emits encode/decode/free for it inside encode_Job /
  decode_Job / free_Job; restore_job()'s create_job() already takes
  exit_info via MOVE_PTR. Null stays null (no fabricated exit 0). A new
  ordinal, so older snapshots/recordings decode with exit_info NULL.

New test_snapent_checkpoint_exit_info_roundtrip (RED before): exit codes
0 and 137 and a NULL survive both the raw checkpoint frames and a real
persist_load_snapshot restore. Full /xce-build all (asan): 111 meson
suites, 0 failures; all Go packages pass.

Not live-verified: the --force-snapshot HA soak cannot reach its snapshot
step because of a pre-existing checkpoint-marker replication stall,
reproduced on the baseline without this change (bug-1870).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-26 09:55:23 +00:00
parent a565779d94
commit e4bb492ae6
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
3 changed files with 93 additions and 5 deletions

View file

@ -410,11 +410,10 @@ static job_pidinfo_t* decode_pidinfo(const uint8_t* payload, size_t len)
}
/* Decode a native (non-capnp) WAL_JOB_FINISH exit_code trailer (bug-1689)
* into a freshly heap-allocated job_exitinfo_t*. job_exitinfo_t (xce/types.h)
* is a single int with NO existing Cap'n Proto mapping (confirmed -- no
* JobExitInfo struct in controller.capnp), so this mirrors wal_jobstate_t's
* own native-struct style rather than inventing a capnp wrapper for one int
* -- unlike decode_joballoc()/decode_pidinfo() above, which decode genuine
* into a freshly heap-allocated job_exitinfo_t*. The WAL trailer predates
* the JobExitInfo capnp struct (bug-1867, used for snapshots) and stays a
* native int32_t, mirroring wal_jobstate_t's own native-struct style --
* unlike decode_joballoc()/decode_pidinfo() above, which decode genuine
* capnp payloads. Returns NULL on a short/absent payload; caller owns the
* result (release via free()/FREEUP -- no nested pointers). */
static job_exitinfo_t* decode_jobexitinfo(const uint8_t* payload, size_t len)