Content addressable datastore. Allows you to configure a node to store and stream large blobs of data, and retrieve them from any swactor-connected node. Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
4.7 KiB
Datastore Streaming Architecture
Overview
"Streaming" in the swactor datastore refers to progressive chunk-based transfer, not byte-level streaming. When an object is stored, it is split into fixed-size chunks, each content-addressed with blake3. When retrieved from a remote peer, chunks are fetched individually and reassembled — enabling progress tracking and partial recovery.
This design trades a small amount of per-chunk overhead for:
- Progress visibility: the dashboard shows
chunks_received / chunks_totalin real time - Resumability: a failed transfer can (in principle) restart from the last chunk
- Deduplication: identical chunks across objects are stored once
Content-Addressed Chunking
The chunk_blob() function (chunking.rs) splits raw bytes into fixed-size pieces:
- Compute
ContentHash = blake3(entire_blob)— this is the object's identity - Split the blob into
ceil(total_size / chunk_size)pieces (default chunk size: 1 MB) - For each piece, compute
chunk_hash = blake3(piece_bytes) - Build a
ChunkRef { hash, offset, size }for each piece - Return an
ObjectManifestcontaining the full list ofChunkRefs
Blob (5.2 MB, chunk_size=1MB)
├── Chunk 0: hash=abc1…, offset=0, size=1048576
├── Chunk 1: hash=def2…, offset=1048576, size=1048576
├── Chunk 2: hash=789a…, offset=2097152, size=1048576
├── Chunk 3: hash=bcd3…, offset=3145728, size=1048576
└── Chunk 4: hash=ef45…, offset=4194304, size=1048576 (last: 209920 bytes)
The object's identity (ContentHash) is the hash of the entire blob, not of the manifest. This means the same data always produces the same hash regardless of chunk size.
Transfer Protocol
When a client requests an object via GET /api/data?hash=..., the API server:
- Sends a
DatastoreNodeMsg::Getto the localDatastoreNodeactor - If found locally, reads all chunks from the local
BlobStoreand reassembles - If not found locally, enters
try_remote_get():
Remote GET step-by-step
- Iterate peers: for each known peer node:
- FindObject: send
MetadataMsg::HandleFindObjectto the peer'sMetadataActor - Read manifest: send
BlobStoreMsg::ReadManifestto the peer'sBlobStore - Fetch chunks: for each
ChunkRefin the manifest:- Send
BlobStoreMsg::ReadChunkto the peer - Receive
DatastoreResponse::ChunkOk { hash, data } - Store locally via
BlobStoreMsg::WriteChunk - Update
DatastoreMetrics::advance_transfer()for dashboard progress
- Send
- Store manifest locally:
BlobStoreMsg::WriteManifest - Store metadata locally:
MetadataMsg::PutObject - Reassemble and respond:
reassemble_blob()concatenates chunks and verifies integrity
If a peer doesn't have the object (or any step fails), the loop continues to the next peer.
Reassembly
reassemble_blob() (chunking.rs) takes a manifest and a set of (hash, data) pairs:
- For each
ChunkRefin manifest order, find the matching(hash, data)pair - Concatenate all chunk data into a single buffer
- Compute
blake3(result)and verify it matchesmanifest.content_hash - Return the reassembled blob (or a
ChunkingErroron mismatch)
This integrity check ensures that even if individual chunks are corrupted or swapped, the final result is always verified against the original content hash.
Progress Tracking
The DatastoreMetrics struct provides thread-safe transfer tracking:
begin_transfer(hash, chunks_total) // called when remote GET starts
advance_transfer(hash) // called after each chunk is stored locally
end_transfer(hash) // called on completion or failure
The dashboard SSE stream includes a datastore event every ~200ms with a DatastoreSnapshot containing active_transfers: Vec<TransferProgress>. The web UI renders these as animated progress bars.
TransferProgress {
hash: "abc123...",
chunks_received: 3,
chunks_total: 5,
}
GC Integration
When an object is deleted, its ObjectEntry and ObjectManifest are removed from the MetadataActor. However, the underlying chunks are not immediately deleted — they may be referenced by other manifests (deduplication).
Instead, garbage collection runs periodically:
MetadataMsg::GcTicktriggers a scan- The
MetadataActorcollects all chunk hashes referenced by any live manifest - Sends
BlobStoreMsg::GcUnreferencedwith the referenced set - The
BlobStoredeletes any chunks not in the referenced set
This two-phase approach prevents data loss when chunks are shared between objects.