feat(db): wo_wal_next_offset — exact record offsets, proven
Task 5a of docs/superpowers/plans/2026-08-26-table-residency.md. The read path itself is NOT in this commit; see the note below. - the offset problem is far smaller than the spec feared. `w->off` is the durable tail and `w->len` the staged bytes, and wo_wal_commit pwrites the whole batch AT off before advancing it — so a record staged now lands at exactly off+len, knowable at append time with no deferral to flush - shipped as an inline accessor rather than new out-params on the three append functions, so the 156 existing WAL checks keep their signatures - correct across both awkward cases, and both are now unit-pinned: a failed commit leaves off unadvanced so the record still lands where it was promised, and wo_wal_open positions off at the end of the INTACT prefix so offsets are always relative to validated data - test_offset_capture asserts the recovered ID per record, not merely that a record parses — a wrong offset reads a NEIGHBOURING record, which passes its own CRC and returns the wrong row silently. 400 records across repeated buffer growth (stage() doubles from 4096) and uneven commit batches, so offsets are exercised mid-buffer and right after a flush The failed-commit test caught MY OWN misunderstanding: I asserted next_offset was unchanged after a failed commit. It is not, and should not be — the record is still staged, so next_offset correctly points PAST it. The invariant that matters is that the durable tail did not move, which is what it now asserts. Gates: test_wal 3428/0 (was 3426), all 18 runtime suites 0 fail under ASan+UBSan, cli_smoke OK, oop-e2e 119/0, residency 8/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b74e13d21e
commit
3290c7d117
2 changed files with 137 additions and 0 deletions
|
|
@ -53,6 +53,25 @@ typedef struct wo_wal {
|
|||
size_t len, cap;
|
||||
} wo_wal;
|
||||
|
||||
/* databasev2 2: the file offset the NEXT staged record will occupy.
|
||||
*
|
||||
* Exact, and knowable at append time — no deferral to flush is needed, which
|
||||
* is what the design spec feared. `off` is the durable tail and `len` the
|
||||
* bytes staged but not yet written, and wo_wal_commit pwrites the whole batch
|
||||
* AT `off` before advancing it, so a record staged now lands at off+len.
|
||||
*
|
||||
* Correct across the two awkward cases:
|
||||
* - a failed commit leaves `off` unadvanced and `len` intact, so the batch
|
||||
* is rewritten from the same place and previously-reported offsets stay
|
||||
* valid;
|
||||
* - a torn tail is handled by wo_wal_open, which positions `off` at the end
|
||||
* of the INTACT prefix, so offsets are always relative to validated data.
|
||||
*
|
||||
* Call it BEFORE the append whose offset you want, and only trust the value
|
||||
* after the matching wo_wal_commit returns 0 — a record whose commit failed
|
||||
* was never durable and its offset must not be recorded anywhere. */
|
||||
static inline uint64_t wo_wal_next_offset(const wo_wal *w) { return w->off + w->len; }
|
||||
|
||||
/* Open (create if missing) and preallocate [prealloc] bytes (best-effort;
|
||||
* a filesystem without fallocate still works). Positions the write offset
|
||||
* at the end of the INTACT record prefix — an existing file is scanned the
|
||||
|
|
|
|||
|
|
@ -323,12 +323,130 @@ static void test_float_bytes_replay(void) {
|
|||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
|
||||
/* databasev2 2: offset capture. wo_wal_next_offset must name exactly where a
|
||||
* record lands, so a resident:keys table can read it back by that offset
|
||||
* later. A wrong offset is the worst possible bug here: it reads a
|
||||
* NEIGHBOURING record, which passes its own CRC and returns the wrong row
|
||||
* silently. So this asserts the recovered id per record, not just that a
|
||||
* record parses.
|
||||
*
|
||||
* Covers the two awkward cases the design called out: records straddling a
|
||||
* buffer growth (stage() doubles from 4096, so 400 rows with Text payloads
|
||||
* cross it repeatedly), and a batch spanning several commits. */
|
||||
static void test_offset_capture(void) {
|
||||
char path[128];
|
||||
snprintf(path, sizeof path, "%s/offsets.wal", g_dir);
|
||||
wo_rt rt;
|
||||
T_EQ(wo_rt_init(&rt, 1 << 20, CLASSES, 1), 0);
|
||||
wo_db db;
|
||||
T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0);
|
||||
wo_wal w;
|
||||
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
|
||||
const char *msg = "";
|
||||
|
||||
enum { N = 400 };
|
||||
uint64_t ids[N], offs[N];
|
||||
|
||||
/* commit in uneven batches so offsets are exercised both mid-buffer and
|
||||
* immediately after a flush reset len to 0 */
|
||||
for (int i = 0; i < N; i++) {
|
||||
char lbl[32];
|
||||
int ln = snprintf(lbl, sizeof lbl, "label-%d-padding", i);
|
||||
wo_str *s = wo_str_new(&rt, lbl, (uint32_t)ln);
|
||||
uint64_t vals[2] = {(uint64_t)i, (uint64_t)(uintptr_t)s};
|
||||
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_CHECK(ids[i] != 0);
|
||||
/* BEFORE the append: this is the contract */
|
||||
offs[i] = wo_wal_next_offset(&w);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
||||
wo_str_free(&rt, s);
|
||||
if (i % 7 == 6) T_EQ(wo_wal_commit(&w), 0);
|
||||
}
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
|
||||
/* offsets must be strictly increasing and inside the written region */
|
||||
for (int i = 1; i < N; i++) T_CHECK(offs[i] > offs[i - 1]);
|
||||
|
||||
/* read each record back BY ITS REPORTED OFFSET and check the id matches:
|
||||
* payload is [kind u8][class u32][id u64], after the 8-byte len+crc head */
|
||||
int checked = 0;
|
||||
for (int i = 0; i < N; i++) {
|
||||
uint8_t head[8], body[13];
|
||||
T_EQ((int)pread(w.fd, head, 8, (off_t)offs[i]), 8);
|
||||
T_EQ((int)pread(w.fd, body, 13, (off_t)(offs[i] + 8)), 13);
|
||||
T_EQ(body[0], WO_WAL_INSERT);
|
||||
uint32_t cid;
|
||||
uint64_t rid;
|
||||
memcpy(&cid, body + 1, 4);
|
||||
memcpy(&rid, body + 5, 8);
|
||||
T_EQ(cid, 0u);
|
||||
T_EQ(rid, ids[i]);
|
||||
checked++;
|
||||
}
|
||||
T_EQ(checked, N);
|
||||
|
||||
wo_wal_close(&w);
|
||||
wo_db_destroy(&db);
|
||||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
/* databasev2 2: an offset reported for a record whose commit FAILED must
|
||||
* never be trusted. Simulated by closing the fd under the wal so pwrite
|
||||
* fails: the offset accessor must not have advanced past the durable tail,
|
||||
* so a later successful commit reuses the same place. */
|
||||
static void test_offset_after_failed_commit(void) {
|
||||
char path[128];
|
||||
snprintf(path, sizeof path, "%s/offfail.wal", g_dir);
|
||||
wo_rt rt;
|
||||
T_EQ(wo_rt_init(&rt, 1 << 20, CLASSES, 1), 0);
|
||||
wo_db db;
|
||||
T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0);
|
||||
wo_wal w;
|
||||
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
|
||||
const char *msg = "";
|
||||
|
||||
uint64_t vals[2] = {7u, 0u};
|
||||
uint64_t id1 = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_CHECK(id1 != 0);
|
||||
uint64_t at1 = wo_wal_next_offset(&w);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, id1), 0);
|
||||
|
||||
/* break the fd, so the commit cannot succeed */
|
||||
int saved = dup(w.fd);
|
||||
T_CHECK(saved >= 0);
|
||||
close(w.fd);
|
||||
w.fd = -1;
|
||||
T_CHECK(wo_wal_commit(&w) != 0);
|
||||
/* The DURABLE TAIL is what must not move. `next_offset` legitimately
|
||||
* points PAST the still-staged record (off unchanged, len still holding
|
||||
* it) — asserting otherwise was this test's own first mistake. The
|
||||
* invariant that matters: off is untouched, so the record still lands at
|
||||
* the offset already reported for it. */
|
||||
T_EQ(w.off, at1);
|
||||
|
||||
/* restore and commit for real: the record lands exactly where promised */
|
||||
w.fd = saved;
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
uint8_t body[13];
|
||||
T_EQ((int)pread(w.fd, body, 13, (off_t)(at1 + 8)), 13);
|
||||
uint64_t rid;
|
||||
memcpy(&rid, body + 5, 8);
|
||||
T_EQ(rid, id1);
|
||||
|
||||
wo_wal_close(&w);
|
||||
wo_db_destroy(&db);
|
||||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/wo-wal-test-XXXXXX");
|
||||
if (!mkdtemp(g_dir)) return 1;
|
||||
test_roundtrip_replay();
|
||||
test_torn_tail();
|
||||
test_float_bytes_replay();
|
||||
test_offset_capture();
|
||||
test_offset_after_failed_commit();
|
||||
test_crash_battery();
|
||||
/* leave the dir for a failed run's forensics only */
|
||||
if (!t_fail) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue