← Documentation

Garbage Collector

How Xudanu reclaims disk space without losing data

Contents
Overview Algorithm Checkpoint & GC Lifecycle Chunk Write Path The v0.7.5 Bug Fix Three Layers of Defense Code Reference

Overview

Every 30 seconds, Xudanu runs a checkpoint that serializes all in-memory state to disk. After the checkpoint completes, the garbage collector (GC) scans the chunk store and deletes any chunk that is no longer referenced by live data.

Chunks are content-addressed blocks of serialized data. Over time, as works are edited, new chunks are written and old chunks become unreachable. The GC reclaims this space.

CHECKPOINT Serialize all state to manifest.json + chunks BUILD REFERENCED SET Collect all chunk hashes from live data structures SWEEP Delete chunks not in the referenced set Referenced chunks are collected from: Works self.works Clubs self.clubs Manifest 6 hash fields Editions standalone_editions Backups manifest_v*.json Chunk Store on Disk a1 b2 c3 d4 e5 f6 g7 h8 i9 ✓ Referenced — kept ✗ Orphaned — deleted

The GC marks each chunk as referenced (green) or orphaned (red dashed), then deletes the orphans.

Algorithm

Collect work chunk hashes

For each work in self.works, walk the edition's chunk reference tree and add all reachable chunk hashes to the referenced set.

Read manifest hash fields

Read manifest.json from disk and add 6 hash references: links_hash, annotations_hash, historical_authors_hash, blob_metas_hash, content_address_hash, fossil_snapshots_hash.

Collect club and edition hashes

Walk chunk reference trees for all clubs (self.clubs) and standalone editions (self.standalone_editions).

Protect backup manifest references

Scan for manifest_v*.json backup files and collect their chunk references. This prevents GC from deleting chunks that a rollback might need.

List all chunks on disk

Call chunk_store.all_chunk_hashes() to enumerate every chunk file in the chunks/ directory.

Delete unreferenced chunks

For each chunk on disk that is not in the referenced set, call chunk_store.delete_chunk().

Safety gate: If the manifest cannot be read in Step 2, the GC returns Ok(0) immediately — it does not proceed. This prevents deleting valid chunks that the GC failed to protect.

Checkpoint & GC Lifecycle

The GC runs as part of the checkpoint cycle. Understanding the full lifecycle is critical for understanding how data survives crashes.

~30 second cycle User creates link / edits work wal.append_create_link(...) WAL entry written wal.log (appended, fsync'd) auto_checkpoint() fires if 30s elapsed since last CHECKPOINT 1. Write chunks (fsync + rename) 2. Write manifest (dual-slot) 3. GC orphaned chunks 4. Truncate WAL Data is now durable on disk manifest.json + chunks/ + wal.log (empty) CRASH RECOVERY (restart) 1. Read manifest → restore works, links, editions 2. Replay WAL → recover un-checkpointed operations

The full lifecycle: user operation → WAL → checkpoint → GC → WAL truncate. On crash, both manifest and WAL provide recovery.

Chunk Write Path

Chunk writes use the gold-standard durable write pattern. This is why actual data corruption is vanishingly rare:

Write .tmp file hash.tmp f.sync_all() fsync data fs::rename() tmp → final dir.sync_all() fsync directory entry A crash at ANY point leaves either the old state or the new state — never partial data write_chunk_durable() in chunk_store.rs:265

The v0.7.5 Bug Fix

On the AWS production instance, transclusion links were disappearing after server restarts. The root cause was NOT data corruption — it was the GC deleting valid chunks.

✗ Before (buggy)
// GC reads manifest to protect chunks
let manifest = read_manifest(path);
if let Ok(m) = manifest {
    // add hashes to referenced set...
}
// GC proceeds even if manifest
// read failed! Chunks that should
// be protected are seen as orphans.
// → links_hash chunk gets DELETED
✓ After (safe)
// GC reads manifest to protect chunks
let manifest = read_manifest(path);
match manifest {
    Ok(m) => m,
    Err(e) => {
        // GC does NOT run. Stale chunks
        // may accumulate temporarily,
        // but valid data is never deleted.
        return Ok(0);
    }
}

The fix is conservative: if the GC can't verify what's safe to delete, it doesn't delete anything. The cost is temporarily accumulated stale chunks, which will be cleaned up on the next successful GC cycle.

Three Layers of Defense

Transclusion links now have three independent persistence mechanisms:

LAYER 1: WAL Crash within 30s window wal.append_create_link() Replayed on restart LAYER 2: MANIFEST INLINE Chunk deleted or corrupt manifest.links: Vec<LinkEntry> Fallback in manifest JSON LAYER 3: MANIFEST CHUNK Normal operation manifest.links_hash Primary storage path RESTORE PRIORITY ON RESTART 1. Try links_hash chunk 2. Fall back to inline links 3. Replay WAL for links created since last checkpoint

Three independent layers ensure link data survives crashes, corruption, and GC.

Code Reference

Component File:Line Function
GC server.rs:10193 gc_orphaned_chunks()
Checkpoint server.rs:9731 checkpoint_to_store()
Auto-checkpoint server.rs:5157 auto_checkpoint() — 30s timer
Chunk write chunk_store.rs:265 write_chunk_durable()
Chunk read chunk_store.rs:299 read_chunk() — verifies hash
Manifest write manifest.rs write_manifest() — dual-slot
Manifest read manifest.rs read_manifest_dual() — tries both slots
WAL wal.rs WalLog, replay_entries()
Restore server.rs:4373 restore_from_data_dir()
Link WAL append server.rs:5338 in create_link()
Link WAL replay server.rs:3338 wal_replay_create_link()
Bottom line: The GC is now conservative by default. If it can't prove a chunk is safe to delete, it leaves it alone. Stale chunks accumulate harmlessly until the next successful GC cycle cleans them up.