swactor/docs/datastore/actors.md
Zachery Aaron Shores-Chmielewski 8412d01393 feat: content addressed datastore (#41)
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>
2026-02-15 17:03:31 +00:00

13 KiB

Datastore Actor Reference

Overview

The datastore is built from four actors within the swactor runtime. DatastoreNode is the public facade — all external requests (HTTP API, network protocol) enter through it and are routed to two long-lived worker actors: BlobStoreActor (content-addressed chunk/manifest I/O) and MetadataActor (object index, DHT replication, GC). A fourth actor, TransferActor, is spawned ephemerally for each remote download and self-terminates on completion or failure.

                        ┌─────────────────────────┐
                        │     store_node (main)    │
                        │  spawns all 3 long-lived │
                        │  actors, drives ticks    │
                        └────┬──────┬──────┬───────┘
                             │      │      │
                    spawn    │      │      │   spawn
              ┌──────────────┘      │      └──────────────┐
              ▼                     │ spawn                ▼
  ┌───────────────────┐             │          ┌───────────────────┐
  │  BlobStoreActor   │             │          │  MetadataActor    │
  │  (chunks, manifests)│           │          │  (index, DHT, GC) │
  └─────────▲─────────┘            │          └──▲────────┬───────┘
            │                       ▼             │        │
            │            ┌───────────────────┐    │        │
            │            │  DatastoreNode    │────┘        │
            │            │  (facade/router)  │─────────────┘
            └────────────│                   │
                         └────────┬──────────┘
                                  │ spawns (per download)
                                  ▼
                         ┌───────────────────┐
                         │  TransferActor    │
                         │  (ephemeral)      │
                         └───────────────────┘

  Arrows: ──▶ sends messages to

Actors

DatastoreNode

Role Top-level coordinator/facade. Accepts user-facing commands and incoming network protocol messages, delegates all work to BlobStoreActor and MetadataActor.
Source crates/datastore/src/actors/datastore_node.rs
Spawned by store_node binary (crates/datastore/src/bin/store_node.rs:188)
Lifecycle Long-lived — runs for the lifetime of the process

Inbound messages (DatastoreNodeMsg — 11 variants):

User-facing commands:

  • Put { data, name, tags, reply_to } — chunk a blob, write chunks + manifest to BlobStoreActor, register in MetadataActor
  • Get { content_hash, reply_to } — retrieve object metadata + manifest via MetadataActor
  • Delete { content_hash, reply_to } — remove object via MetadataActor
  • List { name_filter, all, reply_to } — list objects (local or swarm-wide) via MetadataActor
  • Status { reply_to } — return this node's NodeId
  • ReadChunk { hash, reply_to } — read a single chunk via BlobStoreActor

Protocol routing (incoming network messages):

  • IncomingGetChunk — forwards to BlobStoreActor::ReadChunk
  • IncomingGetManifest — forwards to BlobStoreActor::ReadManifest
  • IncomingStoreObject — forwards to MetadataActor::HandleStoreObject
  • IncomingFindObject — forwards to MetadataActor::HandleFindObject
  • IncomingListObjects — forwards to MetadataActor::ListLocal

Key outbound messages:

  • BlobStoreMsg::WriteChunk, WriteManifest, ReadChunk, ReadManifest — to BlobStoreActor
  • MetadataMsg::PutObject, GetObject, DeleteObject, ListLocal, ListSwarm, HandleStoreObject, HandleFindObject — to MetadataActor
  • DatastoreResponse::NodeStatus — directly to caller for Status

BlobStoreActor

Role Content-addressed storage for chunks and manifests. All I/O goes through a pluggable StorageBackend (filesystem or in-memory).
Source crates/datastore/src/actors/blob_store.rs
Spawned by store_node binary (store_node.rs:178)
Lifecycle Long-lived — runs for the lifetime of the process

Inbound messages (BlobStoreMsg — 8 variants):

Chunk operations:

  • WriteChunk { hash, data, reply_to } — persist a chunk, reply ChunkStored
  • ReadChunk { hash, reply_to } — read a chunk, reply ChunkOk or NotFound
  • DeleteChunk { hash } — remove a chunk (fire-and-forget)
  • HasChunk { hash, reply_to } — existence check, reply Bool
  • ListChunks { reply_to } — list all chunk hashes, reply ChunkList
  • GcUnreferenced { referenced } — delete chunks not in the referenced set (fire-and-forget)

Manifest operations:

  • WriteManifest { manifest, reply_to } — persist a manifest, reply ManifestStored
  • ReadManifest { hash, reply_to } — read a manifest, reply ManifestOk or NotFound

Key outbound messages:

  • DatastoreResponse variants (ChunkStored, ChunkOk, ManifestStored, ManifestOk, NotFound, Error, Bool, ChunkList) — always back to the reply_to address

MetadataActor

Role Object metadata index. Maintains a HashMap<ContentHash, ObjectEntry> and a manifest cache. Handles DHT-style find/store operations, epidemic dissemination of entries to peers, and periodic garbage collection.
Source crates/datastore/src/actors/metadata.rs
Spawned by store_node binary (store_node.rs:185)
Lifecycle Long-lived — runs for the lifetime of the process

Inbound messages (MetadataMsg — 11 variants):

Object operations:

  • PutObject { entry, manifest, reply_to } — store metadata + manifest locally, enqueue for dissemination, reply PutOk
  • GetObject { content_hash, reply_to } — local lookup, reply GetOk or NotFound
  • DeleteObject { content_hash, reply_to } — remove from local index, reply DeleteOk or NotFound
  • ListLocal { name_filter, reply_to } — list local entries with optional name filter, reply ListOk
  • ListSwarm { name_filter, reply_to } — swarm-wide list (currently delegates to ListLocal)

DHT protocol:

  • HandleFindObject { from, content_hash, reply_to } — answer an incoming FIND_VALUE from a peer
  • HandleStoreObject { entry, manifest } — accept an incoming STORE from a peer (fire-and-forget)

Peer management:

  • SetPeers { peers } — update the list of peer MetadataActor addresses for dissemination

Periodic ticks (driven by the store_node main loop):

  • DisseminateTick — send pending entries to all known peers
  • GcTick — collect referenced chunks from all manifests, send BlobStoreMsg::GcUnreferenced to BlobStoreActor

Key outbound messages:

  • DatastoreResponse variants (PutOk, GetOk, DeleteOk, ListOk, NotFound, Error) — to caller
  • MetadataMsg::HandleStoreObject — to peer MetadataActor addresses during dissemination
  • BlobStoreMsg::GcUnreferenced — to local BlobStoreActor during GC

TransferActor

Role Manages a single object download from a remote node. Tracks pending/received chunks, forwards received data to the local BlobStoreActor, and reports completion or failure to the original requester.
Source crates/datastore/src/actors/transfer.rs
Spawned by DatastoreNode (one per remote download)
Lifecycle Ephemeral — self-terminates via ctx.stop_self() on completion, failure, or cancel

Inbound messages (TransferMsg — 4 variants):

  • StartDownload { manifest, source_node, reply_to } — initialize the download with a manifest and source
  • ChunkReceived { hash, data } — a chunk arrived from the remote node
  • ChunkFailed { hash, reason } — a chunk fetch failed (retries up to max_retries, then fails the whole transfer)
  • Cancel — abort the transfer immediately

Key outbound messages:

  • BlobStoreMsg::WriteChunk — to local BlobStoreActor for each received chunk
  • DatastoreResponse::TransferComplete — to reply_to when all chunks received
  • DatastoreResponse::TransferFailed — to reply_to when retries are exhausted

Message Reference

All message types are defined in crates/datastore/src/messages.rs.

Intra-node actor messages

Enum Variants Handled by
DatastoreNodeMsg 11 (6 user-facing + 5 protocol routing) DatastoreNode
BlobStoreMsg 8 (5 chunk ops + 1 GC + 2 manifest ops) BlobStoreActor
MetadataMsg 11 (5 object ops + 2 DHT + 1 peer mgmt + 2 ticks) MetadataActor
TransferMsg 4 (start + chunk received + chunk failed + cancel) TransferActor

Shared response enum

DatastoreResponse — 15 variants used as the return type for all four actors:

Variant Meaning
PutOk { content_hash } Object stored successfully
GetOk { entry, manifest } Object found
DeleteOk { content_hash } Object deleted
ListOk { entries } List result
ChunkOk { hash, data } Chunk data retrieved
ChunkStored { hash } Chunk written to storage
ManifestStored { hash } Manifest written to storage
ManifestOk { manifest } Manifest retrieved
TransferComplete { content_hash } All chunks downloaded
TransferFailed { reason } Transfer failed
NodeStatus { node_id } Node identity
NotFound Resource not found
Error { reason } Generic error
Bool(bool) Boolean result (e.g. HasChunk)
ChunkList { hashes } List of chunk hashes

Inter-node wire messages (NetworkMessage)

Struct Direction Purpose
GetChunkRequest requester → holder Fetch a chunk by hash
GetChunkResponse holder → requester Return chunk data (or None)
StoreObjectRequest origin → DHT peer Kademlia STORE for object metadata
FindObjectRequest requester → DHT peer Kademlia FIND_VALUE for object metadata
FindObjectResponse DHT peer → requester Return Found(entry) or Closer(nodes)
GetManifestRequest requester → holder Fetch a manifest by content hash
GetManifestResponse holder → requester Return manifest (or None)
ListObjectsRequest requester → peer List objects with optional name filter
ListObjectsResponse peer → requester Return matching entries

Wire messages are distinguished from intra-node messages by implementing the NetworkMessage trait with a stable type_tag() string. They are serialized with serde for transport over iroh/QUIC.


Key Flows

Put (store a blob)

Client → DatastoreNode::Put
  → chunk_blob() splits data into chunks
  → BlobStoreActor::WriteChunk  (for each chunk, fire-and-forget)
  → BlobStoreActor::WriteManifest
  → MetadataActor::PutObject
    → stores entry + manifest locally
    → enqueues for dissemination
    → replies DatastoreResponse::PutOk

Get (retrieve metadata)

Client → DatastoreNode::Get
  → MetadataActor::GetObject
    → local index lookup
    → replies DatastoreResponse::GetOk  (or NotFound)

Data (reassemble from chunks)

See streaming.md for the full transfer protocol. In summary:

API server → DatastoreNode::Get → MetadataActor (local miss)
  → iterate peers:
    → FindObjectRequest  (wire) → peer MetadataActor
    → GetManifestRequest (wire) → peer BlobStoreActor
    → GetChunkRequest    (wire) → peer BlobStoreActor  (per chunk)
    → BlobStoreActor::WriteChunk (store locally)
  → BlobStoreActor::WriteManifest
  → MetadataActor::PutObject
  → reassemble_blob() → verify blake3 → respond

Dissemination (epidemic replication)

store_node main loop (every disseminate_interval ticks)
  → MetadataActor::DisseminateTick
    → take_pending() selects entries with remaining budget
    → for each peer: MetadataActor::HandleStoreObject
      → peer inserts if absent, re-enqueues for further dissemination

Budget per entry = Λ * ceil(log2(cluster_size)) (SWIM-style, Λ=3).

GC (garbage collection)

store_node main loop (every gc_interval ticks)
  → MetadataActor::GcTick
    → scans all manifests → builds referenced chunk set
    → BlobStoreActor::GcUnreferenced { referenced }
      → deletes any chunk not in the referenced set

  • streaming.md — chunking, transfer protocol, reassembly, and progress tracking