schfs: the commit barrier, reflink, and attributes with an index over them
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f4d5c671a7
commit
03cc316001
39 changed files with 3942 additions and 550 deletions
|
|
@ -69,6 +69,7 @@ static long readlink(const char *p, char *b, unsigned long n)
|
|||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
#include "../kernel/fs/schfs/objw.c"
|
||||
#include "../kernel/fs/schfs/attr.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* File-backed block device */
|
||||
|
|
@ -111,6 +112,23 @@ img_write(void *ctx, uint64_t b, const void *buf)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Make what has been written actually reach the file.
|
||||
//
|
||||
// The kernel's version of this hook drains the buffer cache and then tells the
|
||||
// drive to commit its own cache; here the equivalent stages are stdio's buffer
|
||||
// and the operating system's page cache. The image mkschfs is building is
|
||||
// normally consumed by something else the moment it exits, so this is mostly
|
||||
// belt and braces -- but the commit path calls it, and a hook that lies about
|
||||
// having flushed is exactly the bug this whole change set is about.
|
||||
static int
|
||||
img_flush(void *ctx)
|
||||
{
|
||||
struct imgdev *d = ctx;
|
||||
if (fflush(d->f) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Allocation */
|
||||
/* */
|
||||
|
|
@ -661,12 +679,14 @@ main(int argc, char **argv)
|
|||
IMG.nblocks = nblocks;
|
||||
|
||||
struct schfs_bdev bd;
|
||||
memset(&bd, 0, sizeof(bd)); /* optional hooks must read as absent */
|
||||
bd.ctx = &IMG;
|
||||
bd.bread = img_read;
|
||||
bd.bwrite = img_write;
|
||||
bd.balloc = img_alloc;
|
||||
bd.bfree = img_free;
|
||||
bd.balloc_run = img_alloc_run;
|
||||
bd.bflush = img_flush;
|
||||
|
||||
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc);
|
||||
if (r != 0) die("formatting", r);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ static int link(const char *from, const char *to)
|
|||
#include "../kernel/fs/schfs/space.c"
|
||||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
// Included for schfs_index_reload, which schfs_vol_open calls at mount. This
|
||||
// tool never writes, so the rest of attr.c is dead weight here -- but linking
|
||||
// against a filesystem whose mount path is missing a function is worse.
|
||||
#include "../kernel/fs/schfs/attr.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
|
|
@ -771,6 +775,7 @@ main(int argc, char **argv)
|
|||
IMG.nblocks = (uint64_t)sz / SCHFS_BLKSIZE;
|
||||
|
||||
struct schfs_bdev bd;
|
||||
memset(&bd, 0, sizeof(bd)); /* optional hooks must read as absent */
|
||||
bd.ctx = &IMG;
|
||||
bd.bread = img_read;
|
||||
bd.bwrite = img_write;
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <setjmp.h> /* the crash in section 10 is a longjmp out of a write */
|
||||
|
||||
#include "../kernel/fs/schfs/btree.c"
|
||||
#include "../kernel/fs/schfs/space.c"
|
||||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
#include "../kernel/fs/schfs/objw.c"
|
||||
#include "../kernel/fs/schfs/attr.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Memory-backed device */
|
||||
|
|
@ -50,11 +52,28 @@ md_read(void *ctx, uint64_t b, void *buf)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Simulated power loss.
|
||||
//
|
||||
// md_fail_after counts writes down; when it reaches zero the write does not
|
||||
// happen and control leaves the filesystem entirely, through the longjmp. That
|
||||
// is what a power cut is: the disk keeps whatever reached it and *nothing else
|
||||
// runs*. The obvious cheaper models are both wrong in ways that test the wrong
|
||||
// thing -- returning an error tests error handling, and silently swallowing the
|
||||
// write while letting execution continue leaves the code reading back blocks it
|
||||
// believes it just wrote, which no real failure does.
|
||||
static long md_fail_after = -1;
|
||||
static int md_crash_armed;
|
||||
static jmp_buf md_crash_jmp;
|
||||
static long md_writes;
|
||||
|
||||
static int
|
||||
md_write(void *ctx, uint64_t b, const void *buf)
|
||||
{
|
||||
(void)ctx;
|
||||
if (b >= NBLOCKS) return -1;
|
||||
md_writes++;
|
||||
if (md_crash_armed && md_fail_after >= 0 && md_fail_after-- == 0)
|
||||
longjmp(md_crash_jmp, 1);
|
||||
memcpy(DISK + b * SCHFS_BLKSIZE, buf, SCHFS_BLKSIZE);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -67,6 +86,11 @@ static void *md_palloc(void) { return calloc(1, SCHFS_BLKSIZE
|
|||
static void
|
||||
mkbdev(struct schfs_bdev *bd)
|
||||
{
|
||||
// Zero first. Every field has to be set even when the answer is "no such
|
||||
// hook", because callers declare these on the stack: leaving the optional
|
||||
// bflush uninitialised handed schfs_vol_commit a garbage function pointer
|
||||
// to test and call, which is a segfault at the first commit.
|
||||
memset(bd, 0, sizeof(*bd));
|
||||
bd->ctx = 0;
|
||||
bd->bread = md_read;
|
||||
bd->bwrite = md_write;
|
||||
|
|
@ -663,6 +687,420 @@ main(void)
|
|||
(unsigned long long)VOL.free_blocks);
|
||||
}
|
||||
|
||||
/* ---- 9b. reflink ---- */
|
||||
//
|
||||
// The thing that makes a reflink worth having is that it costs no space,
|
||||
// and the thing that makes it dangerous is that both files are writable
|
||||
// afterwards. So both are checked: the clone must be free, and a write to
|
||||
// either side must not be visible on the other.
|
||||
printf("\n reflink\n");
|
||||
{
|
||||
uint64_t before = VOL.free_blocks;
|
||||
|
||||
uint64_t a = mkfile(ROOT, "clone-src");
|
||||
char big[16 * 1024];
|
||||
for (unsigned i = 0; i < sizeof(big); i++)
|
||||
big[i] = (char)(i * 7 + 1);
|
||||
schfs_file_write(&VOL, a, 0, big, sizeof(big), DATABLK);
|
||||
remount();
|
||||
|
||||
uint64_t used_by_a = before - VOL.free_blocks;
|
||||
ok(used_by_a > 0, "the source file occupies space");
|
||||
|
||||
uint64_t b = mkfile(ROOT, "clone-dst");
|
||||
uint64_t pre = VOL.free_blocks;
|
||||
ok(schfs_file_reflink(&VOL, a, b) == 0, "reflink succeeds");
|
||||
remount();
|
||||
|
||||
// A handful of metadata blocks is expected; a full copy is not.
|
||||
uint64_t cost = pre - VOL.free_blocks;
|
||||
ok(cost * 4 < used_by_a,
|
||||
"the clone costs far less than the data it shares");
|
||||
if (cost * 4 >= used_by_a)
|
||||
printf(" (clone cost %llu blocks, data is %llu)\n",
|
||||
(unsigned long long)cost, (unsigned long long)used_by_a);
|
||||
|
||||
ok(size_of(b) == sizeof(big), "the clone has the source's size");
|
||||
|
||||
char rb[sizeof(big)];
|
||||
ok(readall(b, rb, sizeof(rb)) == (long)sizeof(big) &&
|
||||
memcmp(rb, big, sizeof(big)) == 0,
|
||||
"the clone reads back exactly what the source held");
|
||||
|
||||
// Writing to the clone must not disturb the source. This is the check
|
||||
// that fails if the write path rewrites a shared run in place.
|
||||
const char *poke = "CLONE-WAS-WRITTEN";
|
||||
schfs_file_write(&VOL, b, 0, poke, 17, DATABLK);
|
||||
remount();
|
||||
|
||||
char ra[sizeof(big)];
|
||||
ok(readall(a, ra, sizeof(ra)) == (long)sizeof(big) &&
|
||||
memcmp(ra, big, sizeof(big)) == 0,
|
||||
"writing the clone leaves the source untouched");
|
||||
|
||||
ok(readall(b, rb, sizeof(rb)) == (long)sizeof(big) &&
|
||||
memcmp(rb, poke, 17) == 0 &&
|
||||
memcmp(rb + 17, big + 17, sizeof(big) - 17) == 0,
|
||||
"and the clone holds the new bytes with the rest unchanged");
|
||||
|
||||
// And the other direction, on a fresh pair: writing the *source*.
|
||||
uint64_t c2 = mkfile(ROOT, "clone-dst2");
|
||||
ok(schfs_file_reflink(&VOL, a, c2) == 0, "a second clone of the same file");
|
||||
remount();
|
||||
schfs_file_write(&VOL, a, 0, "SOURCE-WAS-WRITTEN", 18, DATABLK);
|
||||
remount();
|
||||
ok(readall(c2, rb, sizeof(rb)) == (long)sizeof(big) &&
|
||||
memcmp(rb, big, sizeof(big)) == 0,
|
||||
"writing the source leaves the clone untouched");
|
||||
|
||||
// Deleting every holder must give all the space back. A refcount that
|
||||
// is decremented once too often frees blocks another file still uses;
|
||||
// one decremented too rarely leaks them. Only this catches either.
|
||||
schfs_unlink(&VOL, ROOT, "clone-src", 9);
|
||||
schfs_unlink(&VOL, ROOT, "clone-dst", 9);
|
||||
schfs_unlink(&VOL, ROOT, "clone-dst2", 10);
|
||||
remount();
|
||||
ok(VOL.free_blocks == before,
|
||||
"deleting every holder returns exactly the space they shared");
|
||||
if (VOL.free_blocks != before)
|
||||
printf(" (started %llu, ended %llu)\n",
|
||||
(unsigned long long)before,
|
||||
(unsigned long long)VOL.free_blocks);
|
||||
|
||||
ok(schfs_btree_check(&VOL.tree) == 0, "the tree is sound after all that");
|
||||
}
|
||||
|
||||
/* ---- 9c. attributes, and the index over them ---- */
|
||||
//
|
||||
// Two things are being checked, and only the second is interesting. That
|
||||
// attributes store and reload is table stakes. That a *query* returns
|
||||
// exactly the right objects -- including after a value changes, which has
|
||||
// to remove the old index entry as well as add the new one -- is the part
|
||||
// that makes the feature worth having and the part that quietly breaks.
|
||||
printf("\n attributes and query\n");
|
||||
{
|
||||
uint64_t d = mkdirent(ROOT, "photos");
|
||||
uint64_t f1 = mkfile(d, "beach.jpg");
|
||||
uint64_t f2 = mkfile(d, "hills.jpg");
|
||||
uint64_t f3 = mkfile(d, "notes.txt");
|
||||
|
||||
ok(schfs_attr_set(&VOL, f1, "tag", 3, "holiday", 7,
|
||||
SCHFS_ATTR_STRING) == 0, "set an attribute");
|
||||
|
||||
char buf[64];
|
||||
unsigned vl = 0;
|
||||
uint16_t ty = 0;
|
||||
ok(schfs_attr_get(&VOL, f1, "tag", 3, buf, sizeof(buf), &vl, &ty) == 1 &&
|
||||
vl == 7 && memcmp(buf, "holiday", 7) == 0 &&
|
||||
ty == SCHFS_ATTR_STRING,
|
||||
"it reads back with its value and type");
|
||||
|
||||
ok(schfs_attr_get(&VOL, f2, "tag", 3, buf, sizeof(buf), &vl, &ty) == 0,
|
||||
"an object without it says so");
|
||||
|
||||
remount();
|
||||
ok(schfs_attr_get(&VOL, f1, "tag", 3, buf, sizeof(buf), &vl, &ty) == 1 &&
|
||||
memcmp(buf, "holiday", 7) == 0,
|
||||
"and it survives a remount");
|
||||
|
||||
// Two attributes on one object, and one replaced.
|
||||
schfs_attr_set(&VOL, f1, "rating", 6, "5", 1, SCHFS_ATTR_INT);
|
||||
schfs_attr_set(&VOL, f1, "tag", 3, "work", 4, SCHFS_ATTR_STRING);
|
||||
remount();
|
||||
ok(schfs_attr_get(&VOL, f1, "tag", 3, buf, sizeof(buf), &vl, &ty) == 1 &&
|
||||
vl == 4 && memcmp(buf, "work", 4) == 0,
|
||||
"replacing a value replaces it rather than appending");
|
||||
ok(schfs_attr_get(&VOL, f1, "rating", 6, buf, sizeof(buf), &vl, &ty) == 1 &&
|
||||
vl == 1 && buf[0] == '5',
|
||||
"and the other attribute is untouched");
|
||||
|
||||
ok(schfs_attr_del(&VOL, f1, "rating", 6) == 0 &&
|
||||
schfs_attr_get(&VOL, f1, "rating", 6, buf, sizeof(buf), &vl, &ty) == 0,
|
||||
"removing one leaves the rest");
|
||||
ok(schfs_attr_get(&VOL, f1, "tag", 3, buf, sizeof(buf), &vl, &ty) == 1,
|
||||
"specifically, the one not removed");
|
||||
|
||||
/* --- the index --- */
|
||||
uint64_t hits[8];
|
||||
ok(schfs_query_equal(&VOL, "tag", 3, "holiday", 7, hits, 8) < 0,
|
||||
"querying an attribute with no index is refused, not answered slowly");
|
||||
|
||||
ok(schfs_index_create(&VOL, "tag", 3) == 0, "declare an index on `tag`");
|
||||
|
||||
// Written *after* the index exists, which is the only thing it covers.
|
||||
schfs_attr_set(&VOL, f1, "tag", 3, "holiday", 7, SCHFS_ATTR_STRING);
|
||||
schfs_attr_set(&VOL, f2, "tag", 3, "holiday", 7, SCHFS_ATTR_STRING);
|
||||
schfs_attr_set(&VOL, f3, "tag", 3, "work", 4, SCHFS_ATTR_STRING);
|
||||
remount();
|
||||
|
||||
int n = schfs_query_equal(&VOL, "tag", 3, "holiday", 7, hits, 8);
|
||||
ok(n == 2, "a query returns exactly the objects carrying that value");
|
||||
if (n != 2)
|
||||
printf(" (got %d)\n", n);
|
||||
ok(n == 2 && ((hits[0] == f1 && hits[1] == f2) ||
|
||||
(hits[0] == f2 && hits[1] == f1)),
|
||||
"and they are the right two");
|
||||
|
||||
n = schfs_query_equal(&VOL, "tag", 3, "work", 4, hits, 8);
|
||||
ok(n == 1 && hits[0] == f3, "a different value returns a different set");
|
||||
|
||||
n = schfs_query_equal(&VOL, "tag", 3, "nonesuch", 8, hits, 8);
|
||||
ok(n == 0, "a value nothing carries returns nothing");
|
||||
|
||||
// Changing a value must take the object *out* of the old set. An index
|
||||
// that only ever gains entries answers confidently and wrongly.
|
||||
schfs_attr_set(&VOL, f2, "tag", 3, "work", 4, SCHFS_ATTR_STRING);
|
||||
remount();
|
||||
n = schfs_query_equal(&VOL, "tag", 3, "holiday", 7, hits, 8);
|
||||
ok(n == 1 && hits[0] == f1,
|
||||
"changing a value removes the object from the old value's set");
|
||||
n = schfs_query_equal(&VOL, "tag", 3, "work", 4, hits, 8);
|
||||
ok(n == 2, "and adds it to the new one");
|
||||
|
||||
// And removing the attribute entirely.
|
||||
schfs_attr_del(&VOL, f1, "tag", 3);
|
||||
remount();
|
||||
n = schfs_query_equal(&VOL, "tag", 3, "holiday", 7, hits, 8);
|
||||
ok(n == 0, "removing the attribute removes it from the index too");
|
||||
|
||||
ok(schfs_btree_check(&VOL.tree) == 0, "the tree is sound after all that");
|
||||
}
|
||||
|
||||
/* ---- 10. crash recovery ---- */
|
||||
//
|
||||
// The whole consistency argument for this filesystem is one sentence:
|
||||
// mount takes the newest uberblock that validates, so a crash lands on
|
||||
// either the old tree or the new one and never between them. Every test
|
||||
// above this point runs on a volume that was never interrupted, so not one
|
||||
// of them touches that sentence -- schfs_test proves it for the B-tree in
|
||||
// isolation, and nothing proved it for a whole volume until here.
|
||||
printf("\n crash recovery\n");
|
||||
{
|
||||
// A known-good starting point, and a note of where it is.
|
||||
{
|
||||
uint64_t d = mkdirent(ROOT, "survivor");
|
||||
uint64_t f = mkfile(d, "keep");
|
||||
schfs_file_write(&VOL, f, 0, "before the crash", 16, DATABLK);
|
||||
}
|
||||
remount();
|
||||
uint64_t good_gen = VOL.generation;
|
||||
|
||||
unsigned char *saved = malloc(NBLOCKS * SCHFS_BLKSIZE);
|
||||
memcpy(saved, DISK, NBLOCKS * SCHFS_BLKSIZE);
|
||||
|
||||
/* --- 10a. cut the power in the middle of a transaction --- */
|
||||
//
|
||||
// Enough writes to be well into the tree and nowhere near the commit,
|
||||
// which is the case that matters: the uberblock never lands, so the
|
||||
// previous one is still newest and every block this group touched is
|
||||
// one no committed root points at.
|
||||
md_writes = 0;
|
||||
md_writes = 0;
|
||||
if (setjmp(md_crash_jmp) == 0) {
|
||||
md_crash_armed = 1;
|
||||
md_fail_after = 12;
|
||||
|
||||
uint64_t d = mkdirent(ROOT, "doomed");
|
||||
for (int i = 0; i < 40; i++) {
|
||||
char nm[16];
|
||||
snprintf(nm, sizeof(nm), "f%d", i);
|
||||
uint64_t f = mkfile(d, nm);
|
||||
if (f) schfs_file_write(&VOL, f, 0, "junk", 4, DATABLK);
|
||||
}
|
||||
schfs_vol_commit(&VOL);
|
||||
|
||||
printf(" FAIL: the simulated crash never fired\n");
|
||||
failures++;
|
||||
}
|
||||
md_crash_armed = 0;
|
||||
md_fail_after = -1;
|
||||
|
||||
struct schfs_bdev bd3;
|
||||
mkbdev(&bd3);
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
int r = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc);
|
||||
ok(r == 0, "the volume mounts after a crash mid-transaction");
|
||||
ok(VOL.generation == good_gen,
|
||||
"it comes back at the last committed generation, not a partial one");
|
||||
ok(VOL.recovered == 0,
|
||||
"and needs no fallback to do it: the newest uberblock is the good one");
|
||||
|
||||
/* --- 10b. the pre-crash filesystem is exactly as it was --- */
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
uint64_t d = 0, f = 0;
|
||||
uint16_t ty = 0;
|
||||
ok(schfs_dir_lookup(&VOL, &c, ROOT, "survivor", 8, &d, &ty) == 0,
|
||||
"what was committed before the crash is still there");
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
ok(d && schfs_dir_lookup(&VOL, &c, d, "keep", 4, &f, &ty) == 0,
|
||||
"and so is its content");
|
||||
char buf[32];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
long got = f ? schfs_file_read(&VOL, &c, f, 0, buf, 16, DATABLK) : -1;
|
||||
ok(got == 16 && memcmp(buf, "before the crash", 16) == 0,
|
||||
"the file reads back byte for byte");
|
||||
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
uint64_t gone = 0;
|
||||
ok(schfs_dir_lookup(&VOL, &c, ROOT, "doomed", 6, &gone, &ty) != 0,
|
||||
"nothing the interrupted transaction did is visible");
|
||||
}
|
||||
ok(schfs_btree_check(&VOL.tree) == 0,
|
||||
"the tree is structurally sound after the crash");
|
||||
|
||||
/* --- 10b'. crash at every write point, not one --- */
|
||||
//
|
||||
// One crash point proves the mechanism exists. It does not prove there
|
||||
// is no window -- and a window is exactly what this kind of bug is: a
|
||||
// handful of writes somewhere in the middle after which the volume
|
||||
// comes back subtly wrong rather than obviously broken. So: run the
|
||||
// same transaction again and again, cutting the power one write later
|
||||
// each time, and check the whole story every time.
|
||||
{
|
||||
int points = 0, bad = 0;
|
||||
for (long cut = 1; cut < 120; cut++) {
|
||||
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
|
||||
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) {
|
||||
bad++; break;
|
||||
}
|
||||
|
||||
int fired = 0;
|
||||
if (setjmp(md_crash_jmp) == 0) {
|
||||
md_crash_armed = 1;
|
||||
md_fail_after = cut;
|
||||
uint64_t d = mkdirent(ROOT, "doomed");
|
||||
for (int i = 0; i < 40; i++) {
|
||||
char nm[16];
|
||||
snprintf(nm, sizeof(nm), "f%d", i);
|
||||
uint64_t f = mkfile(d, nm);
|
||||
if (f) schfs_file_write(&VOL, f, 0, "junk", 4, DATABLK);
|
||||
}
|
||||
schfs_vol_commit(&VOL);
|
||||
} else {
|
||||
fired = 1;
|
||||
}
|
||||
md_crash_armed = 0;
|
||||
md_fail_after = -1;
|
||||
if (!fired)
|
||||
break; /* ran out of writes to cut at */
|
||||
points++;
|
||||
|
||||
// Whatever the disk looks like now, all three of these must
|
||||
// hold: it mounts, it is a whole tree, and the data committed
|
||||
// before the crash is exactly as it was.
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) {
|
||||
printf(" cut %ld: does not mount\n", cut);
|
||||
bad++; continue;
|
||||
}
|
||||
if (schfs_btree_check(&VOL.tree) != 0) {
|
||||
printf(" cut %ld: tree is not sound\n", cut);
|
||||
bad++; continue;
|
||||
}
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
uint64_t d = 0, f = 0;
|
||||
uint16_t ty = 0;
|
||||
if (schfs_dir_lookup(&VOL, &c, ROOT, "survivor", 8, &d, &ty) != 0) {
|
||||
printf(" cut %ld: lost a committed directory\n", cut);
|
||||
bad++; continue;
|
||||
}
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
if (schfs_dir_lookup(&VOL, &c, d, "keep", 4, &f, &ty) != 0) {
|
||||
printf(" cut %ld: lost a committed file\n", cut);
|
||||
bad++; continue;
|
||||
}
|
||||
char buf[32];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
if (schfs_file_read(&VOL, &c, f, 0, buf, 16, DATABLK) != 16 ||
|
||||
memcmp(buf, "before the crash", 16) != 0) {
|
||||
printf(" cut %ld: committed file came back changed\n", cut);
|
||||
bad++; continue;
|
||||
}
|
||||
}
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"%d crash points, every one mounts intact with its "
|
||||
"committed data", points);
|
||||
ok(bad == 0 && points > 20, msg);
|
||||
}
|
||||
|
||||
/* --- 10c. a newest uberblock naming a tree that never landed --- */
|
||||
//
|
||||
// This is the failure the commit barrier exists to prevent, and it is
|
||||
// still reachable through a misdirected write or a bad sector under
|
||||
// the root. Before the fallback existed the mount simply refused, with
|
||||
// 127 intact older roots sitting on the disk untouched.
|
||||
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
if (schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0) {
|
||||
printf(" FAIL: reopen for the misdirected-write case\n");
|
||||
failures++;
|
||||
} else {
|
||||
uint64_t base_gen = VOL.generation;
|
||||
|
||||
// Two more commits, then destroy the block each of their roots
|
||||
// points at -- so the two newest uberblocks are both intact and
|
||||
// both name rubbish.
|
||||
uint64_t roots[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char nm[16];
|
||||
snprintf(nm, sizeof(nm), "later%d", i);
|
||||
mkfile(ROOT, nm);
|
||||
schfs_vol_commit(&VOL);
|
||||
roots[i] = VOL.tree.root;
|
||||
}
|
||||
for (int i = 0; i < 2; i++)
|
||||
memset(DISK + roots[i] * SCHFS_BLKSIZE, 0xA5, SCHFS_BLKSIZE);
|
||||
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
int rr = schfs_vol_open(&VOL, &bd3, &CTX, md_palloc);
|
||||
ok(rr == 0, "a volume whose two newest roots are rubbish still mounts");
|
||||
// Strictly older, not "exactly two older". Copy-on-write shares
|
||||
// every unchanged node between generations, so a block that is part
|
||||
// of the newest tree is very often part of several older ones too --
|
||||
// destroying it takes them all with it. How far back the ring has to
|
||||
// reach is therefore not something a test can pin down, and an
|
||||
// earlier version of this asserted exactly two and was simply wrong
|
||||
// once the filesystem had enough history to share nodes.
|
||||
//
|
||||
// What must hold is that it landed on neither corrupted generation
|
||||
// and said so.
|
||||
ok(VOL.generation < base_gen,
|
||||
"it falls back past both of them to an older readable root");
|
||||
ok(VOL.recovered >= 2,
|
||||
"and says that at least those two groups were lost");
|
||||
ok(schfs_btree_check(&VOL.tree) == 0,
|
||||
"what it recovered is structurally sound");
|
||||
}
|
||||
|
||||
/* --- 10d. every uberblock destroyed is a failure, not a guess --- */
|
||||
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
|
||||
memset(DISK + SCHFS_UBER_START * SCHFS_BLKSIZE, 0,
|
||||
SCHFS_UBER_COUNT * SCHFS_BLKSIZE);
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
ok(schfs_vol_open(&VOL, &bd3, &CTX, md_palloc) != 0,
|
||||
"a volume with no valid uberblock at all is refused, not invented");
|
||||
|
||||
// Put the good image back so anything after this runs on a sane volume.
|
||||
memcpy(DISK, saved, NBLOCKS * SCHFS_BLKSIZE);
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
mkbdev(&bd3);
|
||||
schfs_vol_open(&VOL, &bd3, &CTX, md_palloc);
|
||||
free(saved);
|
||||
}
|
||||
|
||||
printf("\n%s\n", failures ? "FAILED" : "all checks passed");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,7 @@ static void
|
|||
mkdev(struct schfs_bdev *bd, struct memdev *d)
|
||||
{
|
||||
memset(d, 0, sizeof(*d));
|
||||
memset(bd, 0, sizeof(*bd)); /* optional hooks must read as absent */
|
||||
d->next = SCHFS_UBER_START + 1;
|
||||
d->fail_after = -1;
|
||||
bd->ctx = d;
|
||||
|
|
|
|||
Loading…
Reference in a new issue