fix(waldump): decode WAL_CONFIG_MARKER, distinguish trimmed gaps from real loss
wal_type_name() never had a case for WAL_CONFIG_MARKER (type 50) -- every config-change marker entry printed as UNKNOWN instead of a real, defined type. Added the case plus payload decode (tag/sha256) matching config_repl.c's config_marker_encode/_decode byte layout, hand-decoded here rather than linking config_repl.c into xce_test's link graph. Gap detection also had no way to tell "committed prefix missing!" (real loss) apart from a normal checkpoint-triggered WAL trim (persist.c's trim_wal_segments, floor-guarded so it never removes a segment a standby still needs) -- every long-running node's WAL, once checkpointed, would false-alarm on every inspection. Cross-checks the sibling data/snap/ directory (snapshot LSN is literally in the filename, snap-<hex>.capnp) and downgrades the message when a snapshot covers the gap. Adds waldump_stats_t.leading_gap_explained and 3 new tests plus one assertion on the existing gap test.
This commit is contained in:
parent
0ca96359f5
commit
a86de77f91
3 changed files with 228 additions and 5 deletions
|
|
@ -46,6 +46,7 @@ static const char* wal_type_name(uint32_t t)
|
|||
case WAL_QUEUE_CTRL: return "QUEUE_CTRL";
|
||||
case WAL_WORKER_CTRL: return "WORKER_CTRL";
|
||||
case WAL_NOOP: return "NOOP";
|
||||
case WAL_CONFIG_MARKER: return "CONFIG_MARKER";
|
||||
case WAL_CHECKPOINT: return "CHECKPOINT";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
|
|
@ -127,6 +128,63 @@ static int decode_jobstate_info(const uint8_t* payload, uint32_t len,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* Decode a WAL_CONFIG_MARKER payload -- fixed 40-byte form (8-byte
|
||||
* big-endian version_tag + 32-byte raw sha256), never capnp-encoded
|
||||
* (config_repl.c's config_marker_encode/config_marker_decode). Hand-decoded
|
||||
* here rather than linking config_repl.c, which would pull controller-
|
||||
* internal dependencies into xce_test's link graph (meson.build's
|
||||
* xce_test_src is deliberately limited to read-only dump/compare tools).
|
||||
* Mirrors config_marker_decode's byte layout exactly. */
|
||||
static int decode_config_marker_info(const uint8_t* payload, uint32_t len,
|
||||
uint64_t* out_tag, uint8_t sha256_out[32])
|
||||
{
|
||||
if (!payload || len < WAL_CONFIG_MARKER_PAYLOAD_LEN)
|
||||
return -1;
|
||||
uint64_t tag = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
tag = (tag << 8) | payload[i];
|
||||
*out_tag = tag;
|
||||
memcpy(sha256_out, payload + 8, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Highest LSN among sibling data/snap/snap-<hex>.capnp files next to
|
||||
* `wal_dir` (persist.c hardcodes both as siblings under a node's data/,
|
||||
* e.g. .../data/wal and .../data/snap). A leading WAL gap up to this LSN is
|
||||
* explained by normal checkpoint-triggered trim (persist.c's
|
||||
* trim_wal_segments only removes segments below a replication-safe floor,
|
||||
* never a segment a standby still needs to catch up) rather than genuine
|
||||
* loss -- without this check, EVERY long-running node's WAL (which will
|
||||
* have been checkpointed at least once) prints the same alarming
|
||||
* "(committed prefix missing!)" wording a real, unexplained loss would,
|
||||
* making the message a standing false alarm. Returns 0 if `wal_dir` isn't
|
||||
* named "wal", or no sibling snap dir/files exist (unknown -- callers keep
|
||||
* today's un-downgraded wording, the conservative default). */
|
||||
static uint64_t highest_snapshot_lsn(const char* wal_dir)
|
||||
{
|
||||
char snap_dir[4096];
|
||||
snprintf(snap_dir, sizeof(snap_dir), "%s", wal_dir);
|
||||
char* slash = strrchr(snap_dir, '/');
|
||||
if (!slash || strcmp(slash + 1, "wal") != 0)
|
||||
return 0;
|
||||
snprintf(slash + 1, sizeof(snap_dir) - (size_t)(slash + 1 - snap_dir),
|
||||
"snap");
|
||||
|
||||
DIR* d = opendir(snap_dir);
|
||||
if (!d)
|
||||
return 0;
|
||||
uint64_t max_lsn = 0;
|
||||
struct dirent* e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
uint64_t lsn;
|
||||
if (sscanf(e->d_name, "snap-%" SCNx64 ".capnp", &lsn) == 1 &&
|
||||
lsn > max_lsn)
|
||||
max_lsn = lsn;
|
||||
}
|
||||
closedir(d);
|
||||
return max_lsn;
|
||||
}
|
||||
|
||||
/* True if `dir` directly contains at least one seg-*.wal file. */
|
||||
static int dir_has_segments(const char* dir)
|
||||
{
|
||||
|
|
@ -241,7 +299,7 @@ static int collect_segments(const char* wal_dir, uint64_t* seg_lsns, int max)
|
|||
static void scan_segment(const char* seg_path, FILE* out,
|
||||
uint32_t* last_crc, uint64_t* prev_lsn,
|
||||
uint64_t* prev_submit_jobid, uint32_t* prev_submit_pcrc,
|
||||
waldump_stats_t* st,
|
||||
waldump_stats_t* st, uint64_t snap_lsn,
|
||||
waldump_visit_fn visit, void* visit_ctx,
|
||||
int* visit_stop)
|
||||
{
|
||||
|
|
@ -309,10 +367,15 @@ static void scan_segment(const char* seg_path, FILE* out,
|
|||
/* Gap detection against the running LSN sequence. */
|
||||
if (*prev_lsn == 0) {
|
||||
if (hdr.lsn > 1) {
|
||||
int explained = snap_lsn >= hdr.lsn - 1;
|
||||
fprintf(out, " *** GAP: missing prefix LSN 1..%" PRIu64
|
||||
" (%" PRIu64 " entries absent) ***\n",
|
||||
hdr.lsn - 1, hdr.lsn - 1);
|
||||
" (%" PRIu64 " entries absent)%s ***\n",
|
||||
hdr.lsn - 1, hdr.lsn - 1,
|
||||
explained ? " -- covered by snapshot, expected "
|
||||
"post-checkpoint trim"
|
||||
: "");
|
||||
st->n_gaps++;
|
||||
st->leading_gap_explained = explained;
|
||||
}
|
||||
} else if (hdr.lsn > *prev_lsn + 1) {
|
||||
fprintf(out, " *** GAP: missing LSN %" PRIu64 "..%" PRIu64 " ***\n",
|
||||
|
|
@ -404,6 +467,20 @@ static void scan_segment(const char* seg_path, FILE* out,
|
|||
}
|
||||
/* a non-submit entry breaks the adjacency for dup detection */
|
||||
*prev_submit_jobid = 0;
|
||||
} else if (hdr.type == WAL_CONFIG_MARKER) {
|
||||
uint64_t tag = 0;
|
||||
uint8_t sha[32];
|
||||
if (decode_config_marker_info(payload, hdr.payload_len, &tag,
|
||||
sha) == 0) {
|
||||
char hex[65];
|
||||
for (int i = 0; i < 32; i++)
|
||||
snprintf(hex + i * 2, 3, "%02x", sha[i]);
|
||||
snprintf(info, sizeof(info), "tag=%016" PRIx64 " sha256=%s",
|
||||
tag, hex);
|
||||
} else {
|
||||
snprintf(info, sizeof(info), "tag=?");
|
||||
}
|
||||
*prev_submit_jobid = 0;
|
||||
} else {
|
||||
/* a non-submit entry breaks the adjacency for dup detection */
|
||||
*prev_submit_jobid = 0;
|
||||
|
|
@ -505,6 +582,7 @@ int waldump_path_visit(const char* path, FILE* out, waldump_stats_t* stats,
|
|||
}
|
||||
|
||||
st.n_segments = (uint32_t)nseg;
|
||||
uint64_t snap_lsn = highest_snapshot_lsn(wal_dir);
|
||||
|
||||
fprintf(out, "== WAL: %s (%d segment%s) ==\n", wal_dir, nseg,
|
||||
nseg == 1 ? "" : "s");
|
||||
|
|
@ -521,7 +599,7 @@ int waldump_path_visit(const char* path, FILE* out, waldump_stats_t* stats,
|
|||
snprintf(seg_path, sizeof(seg_path), WAL_SEG_FMT, wal_dir,
|
||||
seg_lsns[i]);
|
||||
scan_segment(seg_path, out, &last_crc, &prev_lsn,
|
||||
&prev_submit_jobid, &prev_submit_pcrc, &st,
|
||||
&prev_submit_jobid, &prev_submit_pcrc, &st, snap_lsn,
|
||||
visit, ctx, &visit_stop);
|
||||
}
|
||||
|
||||
|
|
@ -532,7 +610,11 @@ int waldump_path_visit(const char* path, FILE* out, waldump_stats_t* stats,
|
|||
" recovered tip: %" PRIu64 "\n",
|
||||
st.first_lsn, st.last_lsn, st.recovered_tip);
|
||||
fprintf(out, " gaps: %u%s crc chain: %s\n", st.n_gaps,
|
||||
(st.n_gaps && st.first_lsn > 1) ? " (committed prefix missing!)" : "",
|
||||
(st.n_gaps && st.first_lsn > 1)
|
||||
? (st.leading_gap_explained
|
||||
? " (explained by checkpoint/snapshot -- not a loss)"
|
||||
: " (committed prefix missing!)")
|
||||
: "",
|
||||
st.crc_broken ? "BROKEN" : "intact");
|
||||
if (st.n_dup_submits > 0)
|
||||
fprintf(out, " duplicate submits: %u (byte-identical: %u) — bug-845\n",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue