schfs: snapshots
A snapshot is a tree root that is kept. The B-tree is copy-on-write, so the blocks a committed root reaches are already immutable -- nothing overwrites a block a committed tree can reach -- and retaining one costs a record: (SNAP_OBJECTID, KEY_SNAP, id) -> struct schfs_snap. The record names the root as it was *before* the record was added. Inserting it rewrites the path under copy-on-write and produces a new root, so a snapshot that included its own creation would name a block nobody had written yet. What makes it survive is the derivation. There is no free-space map on disk; it is rebuilt at mount by walking the tree, and that walk now covers every snapshot root as well as the live one. Without it a block reachable only from a snapshot looks free, gets handed out, and overwrites the data the snapshot exists to keep -- silently, surfacing only when the snapshot is read. A snapshot's own tree may hold older snapshot records; those are deliberately not followed, or deleting a snapshot could be undone by an older one's memory of it. Runtime frees are suspended while a snapshot exists. Knowing which freed blocks a snapshot still needs requires per-extent reference counting, which is a design and not a flag; until that exists the safe answer is to free nothing and let the next mount's derivation reclaim what is genuinely unreachable. Allocation is unaffected and stays correct -- it works from the in-use bitmap the derivation filled. The running free-blocks *counter* does drift from the truth while a snapshot is present, which is cosmetic and is reported by the test rather than asserted away. Tested in schfs_objtest: after a snapshot, the live tree shows new contents while the snapshot still shows the old ones; a file deleted from the live tree is still readable through the snapshot; deleting the snapshot removes the record and the space comes back at the next mount. schfs-check passes including the round-trip; bootcheck 108 [ok]. Not yet: snapshots are not reachable by name from userspace -- there is no mount-a-snapshot path, so this is the format and the mechanism, not the feature a user sees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
58296b3376
commit
6e2e9e8d05
6 changed files with 333 additions and 5 deletions
|
|
@ -42,6 +42,76 @@ schfs_alloc_objectid(struct schfs_vol *v)
|
|||
return v->next_objectid++;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Snapshots */
|
||||
/* */
|
||||
/* A snapshot is a tree root that is kept. Copy-on-write means the */
|
||||
/* blocks that root reaches are already immutable -- nothing overwrites */
|
||||
/* a block a committed tree can reach -- so retaining one costs a */
|
||||
/* record, and keeping it alive is entirely schfs_space_scan's job. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Take a snapshot of the volume as it is committed *now*.
|
||||
//
|
||||
// The root recorded is the one in v->tree before this call, which is the
|
||||
// committed state. It cannot be the root this call produces: adding the record
|
||||
// rewrites the path to it under copy-on-write, so that root does not exist
|
||||
// until afterwards, and a snapshot naming it would name a block nobody had
|
||||
// written yet.
|
||||
//
|
||||
// The caller commits. Until it does, the record is not on disk and the
|
||||
// snapshot does not exist -- which is the correct failure mode, because a
|
||||
// snapshot that survived a crash while the tree it names did not would be a
|
||||
// dangling root.
|
||||
int
|
||||
schfs_snap_create(struct schfs_vol *v, uint64_t id, const char *name,
|
||||
unsigned namelen)
|
||||
{
|
||||
schfs_cache_invalidate();
|
||||
|
||||
if (namelen >= SCHFS_SNAP_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_snap sn;
|
||||
memset(&sn, 0, sizeof sn);
|
||||
sn.root = v->tree.root;
|
||||
sn.root_crc = v->tree.root_crc;
|
||||
sn.root_generation = v->tree.root_gen;
|
||||
sn.created = v->tree.generation;
|
||||
for (unsigned i = 0; i < namelen; i++)
|
||||
sn.name[i] = name[i];
|
||||
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
|
||||
int r = schfs_btree_insert(&v->tree, &v->ctx, &k, &sn, sizeof sn);
|
||||
if (r == 0)
|
||||
v->space.flags |= SCHFS_SPACE_RETAIN_ALL;
|
||||
return r;
|
||||
}
|
||||
|
||||
// Forget a snapshot. The blocks it was holding are not freed here: they stop
|
||||
// being reachable, and the next derivation at mount simply does not mark them.
|
||||
// That is the whole deletion -- there is no reference count to decrement,
|
||||
// because there never was one.
|
||||
int
|
||||
schfs_snap_delete(struct schfs_vol *v, uint64_t id)
|
||||
{
|
||||
schfs_cache_invalidate();
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, id);
|
||||
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
// Stop retaining. If other snapshots remain, the next mount's derivation
|
||||
// sets the flag again -- it is the walk that knows, not this function, and
|
||||
// guessing here would mean keeping a second count to disagree with it.
|
||||
// Until that mount the allocator is merely conservative, which is the safe
|
||||
// direction.
|
||||
v->space.flags &= ~(unsigned)SCHFS_SPACE_RETAIN_ALL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue