feat(db2-7): WO_DATA=<file> — the store as one file, two refusals
- `wo_wal_resolve_data_path` (database/src/wal.c|h): an existing directory or a trailing `/` → `<dir>/shard-0.wal` byte for byte (the `//` after a trailing slash included); otherwise the path IS the log — opened if a regular file, created by `wo_wal_open` if absent under an existing parent. - Refusals as codes for main.c: WO_WAL_PATH_NO_PARENT (out = the parent, so the line names it), WO_WAL_PATH_NOT_A_FILE (fifo/socket/device), WO_WAL_PATH_TOO_LONG (today's snprintf truncated silently). - `parent_dir_of` shared by the resolver and `sync_parent_dir`: the parent checked at boot IS the parent fsync'd after a compaction/migration rename. - runtime/src/main.c: the resolver replaces the unconditional `"%s/shard-0.wal"`; each refusal is one stderr line, exit 2 through the 6a destroy sequence; never mkdir -p. The 6a block is untouched. - Failing first: test_resolve_data_path — 4× -Werror (implicit declaration + three undeclared codes); green after: 21 assertions (dir, trailing slash, absent file, regular file, bare name, missing parent, parent is a file, fifo, two too-long). - `make -C runtime test`: 21 suites, 8374 pass / 0 fail (was 8353/0). `make -C runtime wovm-asan` clean; smoke: file form seeds + replays with `app.db` the only artifact; missing parent and fifo refuse rc 2 naming path + parent; dir and trailing slash unchanged; WO_EPHEMERAL conflict inherited from 6a. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit b31bd4052624be460de1c18cf208661539397e09)
This commit is contained in:
parent
82efb498d4
commit
c19a01564f
4 changed files with 147 additions and 8 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
/* ---- crc32 (poly 0xEDB88320) — ported from runtime/wo-rt.c ------------- */
|
/* ---- crc32 (poly 0xEDB88320) — ported from runtime/wo-rt.c ------------- */
|
||||||
|
|
@ -366,6 +367,45 @@ int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* dirname(3) without its allocation and locale baggage: "a/b" → "a", "/b" →
|
||||||
|
* "/", "b" → ".". The boot-time parent check (wo_wal_resolve_data_path) and
|
||||||
|
* the post-rename fsync (sync_parent_dir) MUST agree on what the parent is;
|
||||||
|
* this is the one place that decides. 0 ok, -1 when [path] does not fit. */
|
||||||
|
static int parent_dir_of(const char *path, char *dir, size_t cap) {
|
||||||
|
size_t n = strlen(path);
|
||||||
|
if (n >= cap) return -1;
|
||||||
|
memcpy(dir, path, n + 1);
|
||||||
|
char *slash = strrchr(dir, '/');
|
||||||
|
if (slash == dir) dir[1] = '\0';
|
||||||
|
else if (slash) *slash = '\0';
|
||||||
|
else memcpy(dir, ".", 2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_wal_resolve_data_path(const char *wo_data, char *out, size_t cap) {
|
||||||
|
size_t n = strlen(wo_data);
|
||||||
|
struct stat st;
|
||||||
|
int have = stat(wo_data, &st) == 0;
|
||||||
|
if ((n && wo_data[n - 1] == '/') || (have && S_ISDIR(st.st_mode))) {
|
||||||
|
/* the directory form: byte for byte what main.c produced before this
|
||||||
|
* function existed, doubled slash after a trailing '/' included */
|
||||||
|
if ((size_t)snprintf(out, cap, "%s/shard-0.wal", wo_data) >= cap)
|
||||||
|
return WO_WAL_PATH_TOO_LONG;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (have && !S_ISREG(st.st_mode)) return WO_WAL_PATH_NOT_A_FILE;
|
||||||
|
if (!have) {
|
||||||
|
/* absent: wo_wal_open creates it, but only where a directory already
|
||||||
|
* is — the parent is handed back so the refusal can name it */
|
||||||
|
if (parent_dir_of(wo_data, out, cap) != 0) return WO_WAL_PATH_TOO_LONG;
|
||||||
|
struct stat pst;
|
||||||
|
if (stat(out, &pst) != 0 || !S_ISDIR(pst.st_mode)) return WO_WAL_PATH_NO_PARENT;
|
||||||
|
}
|
||||||
|
if (n >= cap) return WO_WAL_PATH_TOO_LONG;
|
||||||
|
memcpy(out, wo_data, n + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int wo_wal_pend_drop(wo_wal *w, uint32_t cid, uint64_t id, uint64_t off) {
|
int wo_wal_pend_drop(wo_wal *w, uint32_t cid, uint64_t id, uint64_t off) {
|
||||||
if (w->pend_len == w->pend_cap) {
|
if (w->pend_len == w->pend_cap) {
|
||||||
size_t nc = w->pend_cap ? w->pend_cap * 2 : 16;
|
size_t nc = w->pend_cap ? w->pend_cap * 2 : 16;
|
||||||
|
|
@ -1015,13 +1055,7 @@ int wo_wal_should_compact(uint64_t used, uint64_t last, uint64_t floor, uint32_t
|
||||||
* cut. */
|
* cut. */
|
||||||
static void sync_parent_dir(const char *path) {
|
static void sync_parent_dir(const char *path) {
|
||||||
char dir[4096];
|
char dir[4096];
|
||||||
size_t n = strlen(path);
|
if (parent_dir_of(path, dir, sizeof dir) != 0) return;
|
||||||
if (n >= sizeof dir) return;
|
|
||||||
memcpy(dir, path, n + 1);
|
|
||||||
char *slash = strrchr(dir, '/');
|
|
||||||
if (slash == dir) dir[1] = '\0';
|
|
||||||
else if (slash) *slash = '\0';
|
|
||||||
else memcpy(dir, ".", 2);
|
|
||||||
int fd = open(dir, O_RDONLY);
|
int fd = open(dir, O_RDONLY);
|
||||||
if (fd < 0) return;
|
if (fd < 0) return;
|
||||||
(void)fsync(fd);
|
(void)fsync(fd);
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,22 @@ static inline uint64_t wo_wal_next_offset(const wo_wal *w) { return w->off + w->
|
||||||
int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc);
|
int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc);
|
||||||
void wo_wal_close(wo_wal *w);
|
void wo_wal_close(wo_wal *w);
|
||||||
|
|
||||||
|
/* databasev2 7: WO_DATA names the store as EITHER a directory or the log file.
|
||||||
|
* An existing directory or a trailing '/' resolves to "<dir>/shard-0.wal" —
|
||||||
|
* the bytes every deployment before this iteration used, unchanged. Anything
|
||||||
|
* else IS the log: an existing regular file is opened, an absent path is
|
||||||
|
* created by wo_wal_open — but only under a parent directory that exists NOW.
|
||||||
|
* The resolver never mkdirs: a typo must not plant a store somewhere
|
||||||
|
* unexpected. Pure — stats, creates nothing. 0 ok, [out] = the log path;
|
||||||
|
* WO_WAL_PATH_NO_PARENT, [out] = the parent that is not an existing directory
|
||||||
|
* (for the refusal line); WO_WAL_PATH_NOT_A_FILE: exists, neither a regular
|
||||||
|
* file nor a directory (fifo, socket, device); WO_WAL_PATH_TOO_LONG: the
|
||||||
|
* result would not fit [cap] — refused, never truncated. */
|
||||||
|
#define WO_WAL_PATH_NO_PARENT -1
|
||||||
|
#define WO_WAL_PATH_NOT_A_FILE -2
|
||||||
|
#define WO_WAL_PATH_TOO_LONG -3
|
||||||
|
int wo_wal_resolve_data_path(const char *wo_data, char *out, size_t cap);
|
||||||
|
|
||||||
/* Stage a record for the row that MUST already be applied to RAM (the
|
/* Stage a record for the row that MUST already be applied to RAM (the
|
||||||
* commit-order doctrine). Insert/update read the row via wo_row_ptr.
|
* commit-order doctrine). Insert/update read the row via wo_row_ptr.
|
||||||
* 0 ok, -1 OOM / no such row. */
|
* 0 ok, -1 OOM / no such row. */
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,29 @@ int main(int argc, char **argv) {
|
||||||
int have_schema = 0;
|
int have_schema = 0;
|
||||||
if (data_dir && data_dir[0]) {
|
if (data_dir && data_dir[0]) {
|
||||||
char wal_path[512];
|
char wal_path[512];
|
||||||
snprintf(wal_path, sizeof wal_path, "%s/shard-0.wal", data_dir);
|
/* databasev2 7: WO_DATA is a directory (→ <dir>/shard-0.wal, as it
|
||||||
|
* always was) or THE log file, created if absent. Refuse rather than
|
||||||
|
* guess: a missing parent is never created, and a path that is
|
||||||
|
* neither a regular file nor a directory is not a store. */
|
||||||
|
int prc = wo_wal_resolve_data_path(data_dir, wal_path, sizeof wal_path);
|
||||||
|
if (prc != 0) {
|
||||||
|
if (prc == WO_WAL_PATH_NO_PARENT)
|
||||||
|
fprintf(stderr,
|
||||||
|
"wovm: WO_DATA=%s — its parent %s is not an existing "
|
||||||
|
"directory; create it first (wovm never runs mkdir -p).\n",
|
||||||
|
data_dir, wal_path);
|
||||||
|
else if (prc == WO_WAL_PATH_NOT_A_FILE)
|
||||||
|
fprintf(stderr,
|
||||||
|
"wovm: WO_DATA=%s exists but is neither a regular file "
|
||||||
|
"nor a directory.\n",
|
||||||
|
data_dir);
|
||||||
|
else
|
||||||
|
fprintf(stderr, "wovm: WO_DATA=%s is too long for a path.\n", data_dir);
|
||||||
|
wo_db_destroy(&DB);
|
||||||
|
wo_vm_destroy(&VM);
|
||||||
|
wo_module_free(&mod);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
/* databasev2 12: the log's head record states the shape that wrote
|
/* databasev2 12: the log's head record states the shape that wrote
|
||||||
* it. Diff it against the compiled classes BEFORE replay: a matching
|
* it. Diff it against the compiled classes BEFORE replay: a matching
|
||||||
* shape replays as-is, add/delete migrates the log in place through
|
* shape replays as-is, add/delete migrates the log in place through
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
@ -297,6 +298,71 @@ static void test_stale_compact_temp_is_removed(void) {
|
||||||
wo_rt_destroy(&rt);
|
wo_rt_destroy(&rt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* databasev2 7: WO_DATA names EITHER a directory (today's form, <dir>/shard-0.wal
|
||||||
|
* byte for byte) or THE log file. Resolution is a pure function so main.c's
|
||||||
|
* only branch is "print the refusal, exit 2" — and so every arm of the rule is
|
||||||
|
* checkable here rather than by booting wovm against the filesystem. */
|
||||||
|
static void test_resolve_data_path(void) {
|
||||||
|
char in[192], out[256], want[256];
|
||||||
|
|
||||||
|
/* an existing directory: exactly the bytes main.c always produced */
|
||||||
|
T_EQ(wo_wal_resolve_data_path(g_dir, out, sizeof out), 0);
|
||||||
|
snprintf(want, sizeof want, "%s/shard-0.wal", g_dir);
|
||||||
|
T_STREQ(out, want);
|
||||||
|
|
||||||
|
/* a trailing slash keeps the directory form even when nothing exists
|
||||||
|
there — including the doubled slash today's snprintf produced */
|
||||||
|
snprintf(in, sizeof in, "%s/nodir/", g_dir);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), 0);
|
||||||
|
snprintf(want, sizeof want, "%s/nodir//shard-0.wal", g_dir);
|
||||||
|
T_STREQ(out, want);
|
||||||
|
|
||||||
|
/* absent file under an existing parent: the path IS the log; resolution
|
||||||
|
itself creates nothing (wo_wal_open's O_CREAT does, later) */
|
||||||
|
snprintf(in, sizeof in, "%s/app.db", g_dir);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), 0);
|
||||||
|
T_STREQ(out, in);
|
||||||
|
T_CHECK(access(in, F_OK) != 0);
|
||||||
|
|
||||||
|
/* an existing regular file: opened as the log */
|
||||||
|
{
|
||||||
|
int fd = open(in, O_WRONLY | O_CREAT, 0644);
|
||||||
|
T_CHECK(fd >= 0);
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), 0);
|
||||||
|
T_STREQ(out, in);
|
||||||
|
|
||||||
|
/* a bare relative name: its parent is ".", which always exists */
|
||||||
|
T_EQ(wo_wal_resolve_data_path("app.db", out, sizeof out), 0);
|
||||||
|
T_STREQ(out, "app.db");
|
||||||
|
|
||||||
|
/* missing parent: refused, the PARENT is handed back for the message,
|
||||||
|
and nothing was mkdir'd on the way */
|
||||||
|
snprintf(in, sizeof in, "%s/nodir/app.db", g_dir);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), WO_WAL_PATH_NO_PARENT);
|
||||||
|
snprintf(want, sizeof want, "%s/nodir", g_dir);
|
||||||
|
T_STREQ(out, want);
|
||||||
|
T_CHECK(access(want, F_OK) != 0);
|
||||||
|
|
||||||
|
/* the parent exists but is a FILE (ENOTDIR): same refusal, same subject */
|
||||||
|
snprintf(in, sizeof in, "%s/app.db/x.db", g_dir);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), WO_WAL_PATH_NO_PARENT);
|
||||||
|
snprintf(want, sizeof want, "%s/app.db", g_dir);
|
||||||
|
T_STREQ(out, want);
|
||||||
|
|
||||||
|
/* exists, but neither a regular file nor a directory */
|
||||||
|
snprintf(in, sizeof in, "%s/fifo.db", g_dir);
|
||||||
|
T_EQ(mkfifo(in, 0600), 0);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, sizeof out), WO_WAL_PATH_NOT_A_FILE);
|
||||||
|
|
||||||
|
/* a result that would not fit is refused, never truncated (today's
|
||||||
|
snprintf into main.c's 512-byte buffer truncated silently) */
|
||||||
|
T_EQ(wo_wal_resolve_data_path(g_dir, out, 8), WO_WAL_PATH_TOO_LONG);
|
||||||
|
snprintf(in, sizeof in, "%s/app.db", g_dir);
|
||||||
|
T_EQ(wo_wal_resolve_data_path(in, out, strlen(in)), WO_WAL_PATH_TOO_LONG);
|
||||||
|
}
|
||||||
|
|
||||||
/* databasev2 3 Task 3: the trigger, tested as a pure decision. Kept pure
|
/* databasev2 3 Task 3: the trigger, tested as a pure decision. Kept pure
|
||||||
* precisely so it CAN be tested — a policy only observable by writing megabytes
|
* precisely so it CAN be tested — a policy only observable by writing megabytes
|
||||||
* and waiting is a policy nobody checks. */
|
* and waiting is a policy nobody checks. */
|
||||||
|
|
@ -3445,6 +3511,7 @@ int main(void) {
|
||||||
test_keys_resident_delete();
|
test_keys_resident_delete();
|
||||||
test_keys_resident_delete_then_replay();
|
test_keys_resident_delete_then_replay();
|
||||||
test_stale_compact_temp_is_removed();
|
test_stale_compact_temp_is_removed();
|
||||||
|
test_resolve_data_path();
|
||||||
test_should_compact_policy();
|
test_should_compact_policy();
|
||||||
test_compact_refuses_with_staged_records();
|
test_compact_refuses_with_staged_records();
|
||||||
test_torn_tail();
|
test_torn_tail();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue