Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
actordb
A Rust actor-based distributed database with pluggable, opt-in storage
Crate installation
The 0.4 release series requires Rust 1.88+:
[]
= "0.4"
The default crate includes no concrete storage backend. Its actor cache and
KvStore contracts work without a database; NodeConfig.store remains None
unless you supply a custom store. RocksDB is an independent opt-in:
= { = "0.4", = ["rocksdb"] }
The separate memory feature exposes the no-op MemoryStore test double; it
is not persistent storage. Neither backend feature enables the other. Protocol
messages ship as generated Rust in actordb-protocol; ordinary builds do not
compile protobuf tooling. RocksDB requires its native library and libclang
bindings. default-features = false is not
necessary to avoid RocksDB: the default feature set is empty.
This remains pre-1.0 software. The 0.2, 0.3, and 0.4 releases include API,
wire-protocol, and legacy-store compatibility changes. Read the release notes
and release procedure before upgrading or publishing.
Working prototype
A working, tested prototype of the actor-first sharded database vision:
The workspace requires Rust 1.88 or newer; CI pins 1.88 as the minimum supported toolchain.
This library workspace does not commit Cargo.lock; fresh checkouts resolve
compatible dependency versions, preferring the declared Rust minimum. Cargo
can generate an ignored local lockfile. Run cargo update to refresh it and
rerun tests; dependency resolution is not frozen across fresh builds.
shard: deterministic key → fragment mapping (fragment_ofvia seahash). The keyspace is partitioned intofragment_countfragments; every node agrees on the mapping.actor: per fragment, aKvActordispatcher (built on ractor) routes ops to a private managed worker pool byhash(key) % worker_count. Sticky dispatch preserves per-key ordering within a priority pool while different keys execute concurrently. Workers share the fragment'sFragmentState: one in-memory cache with a lock-coherent ordered key index, one ordered persistence buffer, and oneKvStore; cross-pool LWW merges are serialized there.- Supervisor actors (in
actor): every fragment runs under aFragmentSupervisorowning the dispatcher pool plus background actors — aCleaner(durable TTL-expiry markers) and aSnapshotter(periodic durable flush). Dead workers restart in place; dead children restart without touching sibling fragments.Node::health()reports per-fragment health. Thesupervisionmodule preserves older imports as compatibility reexports; implementations live alongside the other actors. node: aNodepairs the per-fragment supervisors with a Tokio TCP mesh. Static placement (fragment_map) with caller-side one-hop routing: a node either applies a write locally and enqueues it for async replication, or forwards it to a fragment owner. Inbound ops merge with last-writer-wins on hybrid-logical-clock timestamps (tie-broken bynode_id), which is idempotent and commutative, so replicas converge without coordination. Deletes are tombstones that propagate like writes. Joining and restarted nodes converge through correlated, byte-bounded reconciliation after the peer handshake.store: pluggable durable storage. In-memory by default; enable therocksdbfeature for RocksDB (per-fragment key namespacing viaPrefixedStore, a fail-closed identity/layout manifest, exclusive live attachment, validated physical routing, and synchronous WAL writes by default). Shared contracts and options stay instore; implementations live in itsmemory,prefixed, and feature-gatedrocksdbchild modules, withstore::*imports available through reexports when the corresponding backend feature is enabled. No bundled backend is enabled by default.proto: the wire protocol — prost-encodedEnvelopeframes, length-delimited over TCP.
Run the unit and real-localhost-TCP integration tests:
Run the three-node demo (writes from each node, prints every node's converged local copy):
Documentation
Comprehensive guides live in docs/:
- Architecture — what actordb is, crate layout, the fragment/supervisor/worker-pool hierarchy
- Public API guide —
Node,NodeConfig, puts/gets/deletes,put_multi/get_multi, TTLs, priorities, health APIs, with examples - Persistence — RocksDB backend, on-disk format,
PrefixedStore, flush batching, crash semantics - Routing & placement — key→fragment→owner,
PlacementController, migration protocol,placement_report() - Replication — sender/receiver state machine, sessions, acks, byte-bounded reconciliation, and the protocol-v3 handshake
- Tombstones & deletes — delete lifecycle, conflict rules, retention, and requirements for safe future reclamation
- Consistency model — LWW via HLC, exact local apply outcomes, read-your-write, partitions
- Multi-key batching — fan-out, failover, partial failures
- Scanning — single-shard filter + transform scans, spec semantics, scheduling, and cost model
- SQL queries — fragment filters, global pagination,
byte projections, and exact aggregates over the virtual
kvtable - Rust client — remote application connections, operation coverage, credentials, and mutation retry semantics
- Traffic classes & backpressure —
OpPriority, pool config, shed behavior and observability - Configuration reference — every
NodeConfigfield with defaults and guidance - Operations — crash/restart, rolling restart, partitions, overload, what to monitor
- Test strategy — unit vs integration vs Docker scenarios
- Releases — package checks, version tags, and crates.io publication
Docker integration tests
Modeled on
ractor's cluster integration tests:
three nodes on a private compose network (test-net), each container
running one scripted role from the actordb-docker-test binary
(docker-tests/ crate). These exercise process boundaries, container-network
DNS peer addresses, container death, and
true SIGKILL crashes.
Scenarios live in docker/envs/:
- replication — node-a writes one key; node-b and node-c must converge on it.
- restart —
SIGKILLnode-a mid-cluster (a real kill -9, delivered by the driver script), restart it, and require all three nodes to reconverge. Exercises the redial supervisor, fresh replication sessions, and immediate correlated reconciliation across process death. - external client — a separate SDK-only fourth container talks to three sharded nodes through the application protocol. It checks remote routing, SQL/transforms, client reconnection after an ingress SIGKILL, and recovery.
CI runs all scenarios on every push/PR touching the relevant paths
(.github/workflows/docker-integration.yaml).
Public API
actordb::node — the main entry point
use ;
use Duration;
let node = start
.await?;
// port 0 picks an ephemeral port; the bound address is available via `node.addr()`
node.connect.await?;
// connect() returns after the transport dial is owned by the redial supervisor;
// authentication and immediate reconciliation continue asynchronously. The
// first authenticated peer id at this address is pinned for later reconnects.
node.put.await?;
node.put_with_ttl.await?;
node.delete.await?;
let value = node.get.await?; // routed read, Option<VersionedValue>
let all = node.get_all.await?; // full local copy (live keys)
let health = node.health.await?; // per-fragment supervision health
// Bulk APIs: keys hash to fragments, then exact-byte/count-bounded chunks per
// remote fragment/owner attempt. Per-key Results remain in input order.
let puts = node.put_multi.await;
let gets = node.get_multi.await;
let peers = node.peer_count.await;
// Local-only: prompt even when a replica is unavailable.
node.shutdown.await?;
// Alternatively, choose the bounded peer fence before local shutdown:
// let report = node.shutdown_with_replication_drain(
// ReplicationDrainOptions::builder().timeout(Duration::from_secs(30)).build(),
// ).await?;
Node::start uses the default transport and replication liveness policy. Use
Node::start_with_options when those runtime-only limits need tuning:
use NodeRuntimeOptions;
use Duration;
let runtime = builder
.peer_queue_frame_capacity
.peer_queue_byte_capacity
.replication_control_message_capacity
.inbound_connection_capacity
.receive_byte_capacity
.queue_admission_timeout
.reconcile_data_admission_timeout
.forwarded_attempt_timeout
.connect_timeout
.handshake_timeout
.socket_read_timeout
.socket_write_timeout
.replication_ack_timeout
.build; // Omitted relay, read, mutation, and health settings retain defaults.
let node = start_with_options
.await?;
All configuration and actor argument types provide Bon builders with defaults
for optional tuning and compile-time checks for required dependencies. Existing
struct literals, Default, with_* helpers, and getters remain available.
build() constructs settings; startup/spawn still validates their values.
See the builder reference for
nested pool defaults, optional setters, and the 13 supported input types.
The defaults are a 256-frame queue per peer, 64 protocol-control messages per
fragment/peer sender, at most 1024 concurrent inbound handshakes or
connections, and a node-wide 64 MiB raw receive-body budget. Accepted and
outbound-dialed connections reserve body bytes before allocation, including
handshake frames; saturation closes only the contributing connection. Permits
remain attached through decoding and the handler turn. This counts encoded
bodies, not protobuf/actor/cache copies or total process memory. Queue
admission, a complete forwarded owner attempt, one dial,
and the absolute two-flight handshake each have 5-second deadlines. Reading a
frame body after its length prefix and writing a complete frame each have a
30-second deadline; an idle established connection has no read deadline until
it sends a prefix. Streamed reconciliation-data pages have a separate
30-second queue-admission deadline so a legal near-limit value can wait behind
another slow near-limit frame; an acceptable ordinary replication ACK is due
within 30 seconds. Locally applied mutations have a separate per-sender ingress lane
sized by SenderConfig::buffer_capacity; saturation schedules lossless reset
reconciliation without blocking the write path. Reconciliation barriers use
SenderConfig::reconcile_timeout instead of the ordinary ACK timeout.
Anti-entropy and ordinary replication frames are built against their exact
encoded size, targeting 1 MiB pages and never exceeding the 16 MiB hard frame
limit. The peer writer also enforces a 32 MiB default encoded-byte budget in
addition to its frame-count limit.
Multi-key semantics:
put_multi/get_multipartition keys by fragment and send one or more exact-byte-boundedBatchPut/BatchGetrequests per remote fragment and owner attempt, rather than one message per key. Large get responses advance as connection-correlated ordered prefixes; omitted suffixes are requested under fresh ids without losing duplicate positions.- Local fan-out is capped at 1024 concurrent calls before dispatcher
admission. A remote
BatchGetuses one aggregate actor continuation but admits and sticky-routes only its next ordered key, stopping by exact encoded result bytes before an omitted suffix is reserved or cloned. - Local and remote puts await the serving fragment's exact per-key apply outcome. Remote batches preserve request order/cardinality and use the configured per-attempt deadline (5 seconds by default), including outbound queueing and the response.
- Remotely routed single-key and batch operations fail over across replica
holders in placement order on transport, timeout, or malformed-response
failures. Every owner attempt has one configured budget (5 seconds by
default) covering queue admission and the response. Typed
Unavailablepositions try the next holder with their original stamped mutations, even after other positions in the batch finish. Other valid per-key outcomes are returned directly and are excluded from subsequent retries. - Errors are per-key and classified:
NodeError::Actorfor misconfiguration (no replica listed for a fragment) or local actor-framework failure,NodeError::Networkfor connectivity failures, plus exactBackpressure,Storage,Superseded, andExpiredmutation outcomes. put_multi_with_priority/get_multi_with_priorityset the operation priority explicitly (default:Background); the priority is carried on the wire and honored by the remote owner.
Key NodeConfig knobs:
| Field | Default | Meaning |
|---|---|---|
fragment_count |
1 | Fragments in the keyspace, at most u32::MAX (must agree cluster-wide and with an existing RocksDB manifest) |
fragment_map |
{} | Static placement: fragment → replica node ids (empty = serve all); non-empty maps must cover every in-range fragment with a non-empty, duplicate-free replica list |
members |
{} | Static membership: node id → unique address; each configured id is enforced from the first authentication attempt |
default_pool / high_pool / background_pool |
see below | Per-priority PoolConfig { workers, queue_cap, rate_limit_per_sec } |
high_pool defaults |
2 workers / cap 5_000 / no limit | Latency-sensitive traffic |
background_pool defaults |
2 workers / cap 5_000 / 1000 ops/s | Bulk/scan traffic (rate-limited) |
flush_batch_size / flush_interval |
1000 / 100ms | Persistence batching |
clean_interval |
30s | TTL-expiry tombstone materialization cadence |
snapshot_interval |
60s | Periodic durable flush |
tombstone_gc_age |
None | Compatibility field; every Some(age) is rejected because safe distributed GC is not implemented |
replication |
defaults | SenderConfig: batching, in-flight/buffer bounds, reconcile cadence/timeout, required ACK durability |
placement_controller |
None | Phase 8: Some(Arc<dyn PlacementController>) opts into dynamic placement |
placement_poll_interval |
1s | Phase 8: how often the driver re-evaluates desired placement |
placement_drain_warn_interval |
10s | Warning cadence for a safe, non-destructive stalled drain |
store |
None | Some(Arc<dyn KvStore>); NodeConfig::with_rocksdb(path) uses SyncWal, while with_rocksdb_options selects explicit RocksDB policy |
The local node id, every members key, and every placement replica id must
contain 1 to 255 UTF-8 bytes. Placement replica lists must be duplicate-free.
RocksDB permanently binds its first Node id and fragment count in a separate
metadata column family and rejects mismatches before network bind. Non-empty
pre-manifest databases are rejected without mutating their column-family layout
and require explicit one-time legacy adoption; see
Persistence.
- Local and remote writes await the serving worker's exact apply outcome;
after
Ok, that owner reads the version or a newer winner. Replication to the owner's other holders remains asynchronous. - Conflicts resolve with last-writer-wins on HLC timestamps,
tie-broken by
node_id; tombstones win exact ties. - Failures surface as typed
NodeErrorvariants, includingBackpressure,Storage,Superseded, andExpiredrather than false success.
actordb::actor — actors and the storage core
KvActor— public per-fragment dispatcher; its managed sticky-hash worker pools are internal;FragmentState— the shared cache/buffer/store. Direct embedders sendKvMessagetoKvActorrather than splitting admission from a raw worker route.VersionedValue { value, timestamp_ms, node_id, expires_at_ms, is_tombstone }— a value plus its LWW version;wins_overimplements the conflict rule,is_expiredthe TTL check.HlcClock— hybrid logical clock behind all timestamps.ApplyOutcome::{Applied, AlreadyPresent, Superseded, Expired}— the exact result used instead of inferring write success from a later read.ApplyError::{Backpressure, InvalidInput, Storage}distinguishes a nonfatal unencodable direct actor request from persistence failure; invalid input does not restart the fragment worker.BatchReadEntry,BatchReadPage,SnapshotSummary,SummaryPage, andVersionedPagesupport priority-aware ordered batch reads plus pinned-time, revision-aware anti-entropy reads without cloning whole fragments or arbitrary fetch chunks; existingGetAllandGetSnapshotremain available for compatibility and debugging.KvMessage::{ApplyOp, Flush, FlushStable, Get, GetBatch, GetAll, GetSnapshot, GetSummaryPage, GetVersioned, GetVersionedPage, GetRevision, GetShedCounts}— for embedders driving the dispatcher directly instead of throughNode.KvMessageis non-exhaustive; downstream matches must retain a wildcard arm as internal routing/query variants evolve.FlushStablerequests the backend's explicit stable fence and reportsApplyDurability::StableCommittedonly when that fence succeeds.
Traffic isolation + backpressure (Phase 7)
Each fragment runs three isolated worker pools instead of one:
OpPriority::{High, Default, Background}selects the pool.put_with_priority/get_with_priority(and theput_multi_with_priority/get_multi_with_priorityvariants) set it; plainput/get/put_multi/get_multiuseDefault(single-key) orBackground(bulk).- Each
PoolConfigsetsworkers,queue_cap, and an optional token-bucketrate_limit_per_sec. When a pool is over its queue cap or rate limit, the op is shed explicitly: counted in the pool's shed counter, and the caller getsNodeError::Backpressure— a shedGetnever returnsOk(None), so backpressure is never confused with "not found". queue_capcovers work accepted into that priority's workers (active plus queued), not the shared dispatcher mailbox or replicated ingress. Accepted reads are handed to their worker without making the dispatcher await them, so a blocked background worker does not block high-priority admission, health, or supervision.- Priority is preserved end-to-end: forwarded single-key operations and batch messages carry the originator's priority on the wire, and the owner applies/serves them in the matching pool. Priority selects that remote actor pool; it does not prioritize the per-peer socket's ordinary data lane.
- Shed counts are surfaced per pool
(high, default, background)vianode.health()→FragmentHealth.shed_counts.
Ordering honesty: local and forwarded puts wait for the serving worker's exact apply outcome. At that point its shared cache contains the applied version or an identical replay; by return time a newer concurrent LWW winner may exist. The configured remote deadline (5 seconds by default) covers both bounded outbound-queue admission and the reply; cancellation and late replies remove or ignore pending state.
Supervisor and maintenance actors
These types live under actordb::actor. actordb::supervision reexports the
same types for compatibility, including their messages, state, and Bon builders.
FragmentSupervisor— owns a fragment's dispatcher,Cleaner, andSnapshotter; restarts dead children in place.FragmentHealth— per-fragment health snapshot (worker count/names, restart counts, per-poolshed_counts).Cleaner— materializes expired values as durable TTL markers.Snapshotter— periodically flushes the fragment's retained write buffer.
actordb::replication — async sender/receiver
- Per fragment, per replica peer, a sender task batches mutations (writes + tombstones) off the write path: ships on a timer, bounded in-flight, and retains each batch until a typed ACK reaches the configured durability fence. Receiver apply/flush failures NACK and requeue the exact batch.
- An ordinary ACK has a 30-second default deadline. Expiry rotates the sender session and replays all unacknowledged, already-stamped operations in their original order; LWW makes duplicates idempotent, and ACKs from the old session are ignored. An ACK below the requested durability neither retires the batch nor extends its deadline.
- Session ids per pairing; stale batches discarded on reconnect.
- Buffer overflow →
buffer_reset: shed, rotate the session, and reconcile via timestamp/tombstone comparison immediately or when the peer returns. - Periodic anti-entropy repairs residual divergence in both static and dynamic
placement. Both sides stream ordered, byte-bounded summary pages at a pinned
TTL time; revision drift or any repair forces a fresh stable verification
pass. Fetch requests contain up to 256 exact-byte-bounded keys; the responder
reads values incrementally and streams byte-bounded data pages through its
bounded writer. Repair and ordinary replication batches use the same exact
protobuf sizing: 1 MiB soft pages and 16 MiB hard frames. Reconcile rounds are counted per sender
(
SenderStateReport::reconcile_rounds).
actordb::placement — dynamic placement controller (Phase 8)
Opt-in via NodeConfig::placement_controller. With no controller the
node keeps static placement and behaves exactly as before.
use ManualPlacement;
use Arc;
let controller = new;
let config = builder
.placement_controller
.placement_poll_interval
.build;
// ... later, migrate fragment 0 from "a" to "b":
let mut overlap = new;
overlap.insert;
overlap.insert;
controller.set_placement;
// wait until b reports fragment 0 as Serving...
let mut final_map = new;
final_map.insert;
final_map.insert;
controller.set_placement;
PlacementControllertrait:desired_placement(),on_membership_change(),on_fragment_ready(). Implement it for an external cluster manager;ManualPlacement(interior-mutable map) covers tests and operator-driven moves.- The driver polls the controller, publishes its map as the routing table, and converges: gained fragments get a supervised subtree plus a streamed bootstrap from every connected source candidate (on-demand reconcile with bounded fetch chunks and value pages, 30s timeout); only an already-serving holder may answer, and timeout leaves the new holder non-authoritative. Revoked fragments drain their replication senders, then stop.
- Dynamic startup synchronously loads assigned durable subtrees for validation
but leaves them
Bootstrappinguntil an already-serving replica completes a verified reconcile round. For a brand-new cluster, seed each fragment on one node first, wait forServing, and only then publish overlap replicas; simultaneously starting an all-new replica set remains non-authoritative by design because no node can prove it has the latest state. - Migration protocol (controller-sequenced, with overlap): add the
new owner → wait for it to report
Serving(bootstrap done) → revoke the old owner. At least one node always serves the fragment; LWW keeps the overlap consistent. A bootstrapping node forwards reads/writes to the previous owner until it flips to serving. - A node that receives a routed op for a fragment it no longer serves re-forwards it to the current owner (hop-limited, never bounced back to the sender) instead of dropping it — this covers the window where peers' placement maps disagree.
node.placement_report()— per-fragment lifecycle state (Bootstrapping/Serving/Draining/Stopped) plus the authoritative placement map.- Static and dynamic placement use the same correlated reconciliation engine.
SyncRequest/SyncResponseare retired in protocol v3, andnode.sync_entries_applied()is a compatibility counter that remains zero. - Assumes the peer mesh is established (
membersor manualconnect()); the driver does not dial.
actordb::proto — wire protocol
Prost-generated protobuf types (Envelope, Hello, HelloAuth, ReplicateOp,
ForwardedPut, ForwardedGet, GetResponse, ReconcileRequest,
ReconcileResponse, ReconcileFetch, ReconcileData, SyncEntry, BatchPut,
BatchGet, BatchGetResponse, MutationResponse, BatchAck, BatchNack),
sent as 4-byte length-prefixed frames over TCP. SyncRequest and SyncResponse
remain generated for source compatibility but are retired; a negotiated v3
peer that sends either is rejected. These types are mostly of interest when
implementing a compatible peer.
Current limitations
This is a working prototype, not production-ready. Known gaps, tracked as issues in the issue tracker:
- Persistence is opt-in — pass a
storeinNodeConfigfor durability; without it the dataset lives in memory and a restart wipes it (recoverable only via peer reconciliation). - No built-in TLS or individual peer identity — plaintext TCP. Protocol v3
gives each physical connection two fresh 256-bit challenges. With a
NodeConfig::cluster_secretcontaining at least 32 independently generated, high-entropy random bytes, both peers prove fresh possession by HMAC-SHA256 over the ordered dialer/listener transcript, roles, ids, version ranges, capabilities, and challenges. This prevents replay of a captured v3 handshake, but a shared secret proves cluster membership, not which member is speaking: any holder can claim any node id. A guessable password can also be tested offline against a captured transcript. The proof provides no confidentiality or integrity for application frames after the handshake. Use a protected, mutually authenticated customNetworkwhen those properties matter. - Anti-entropy is correlated reconciliation — static placement, dynamic placement, late joins, reconnect recovery, buffer resets, and periodic repair all use the same session/round-correlated reconciliation engine. In legacy map-empty mode, per-peer senders are reaped on disconnect instead of retaining an unbounded set of discovered identities; mandatory immediate reconciliation after reconnect recovers downtime writes and tombstones. Senders named by an explicit placement map remain allocated while disconnected and retain their bounded replay/reset state.
- Transport queues have count and byte bounds — the shared per-peer
writer pipeline defaults to 256 frames and 32 MiB of admitted encoded data;
a single accepted frame can approach 16 MiB. A producer first reserves the
data-lane ingress and its exact encoded-byte permits, then allocates the
frame; the owned permit travels with the frame through the socket write and
releases on every drop path. Queue admission and socket writes have bounded
waits; expiry cancels the connection so the dialer can reconnect. The frame
budget is split between data and a writer-priority response lane, and a full
16 MiB maximum response is reserved from ordinary producers. Responses produced
by the sole connection reader use nonblocking admission and cancel on
saturation, so the reader never waits behind its own writer. After at most
16 consecutive responses, a ready data frame is written to prevent starvation.
Non-serving read/mutation relays separately share node-wide limits of 1024
live attempts/timers and 64 MiB of retained request-key storage (String/Vec
storage and buffer capacities, not mutation values or total heap). Admission
never waits; rejected mutations return
Unavailable, and legacy reads stay unavailable without false misses. Exact physical origin/owner identities prevent stale disconnect cleanup from removing replacement work. Each Node sender has a bounded mutation lane and a separate bounded protocol-control lane; mutation overflow advances a persistent reset generation, while control overflow closes the contributing connection for replay. Sender actor lanes remain operation/message-count bounds. Protocol builders separately enforce exact encoded sizes: anti-entropy, ordinary replication, and aggregate batches target 1 MiB pages and no frame may exceed 16 MiB. Mutation batches also reserve their worst-case complete replies; get responses use ordered prefix pulls. An irreducible oversized record or a record with empty or over-limit provenance is rejected before local apply; a legacy persisted record violating either invariant fails node startup instead of becoming permanently unreplicable. Before Prost decoding, an allocation-free scan caps anti-entropy pages at 256 repeated items and general batch fields at 4096, preventing empty-message decode amplification. One ticketed FIFO credit schedules anti-entropy rounds fairly across every fragment on a physical connection. Receiver actor mailbox byte accounting remains follow-up work. Inbound handlers are capped at 1024 by default, the two-flight handshake has one absolute 5-second deadline, and completed Node-owned tasks are reaped while the node runs. - Fan-out is sequential — remote fragment groups and their replica-holder attempts are currently awaited one at a time. Operation priority chooses the destination actor pool but does not schedule the ordinary socket data lane.
- Wire upgrades are negotiated but coordinated — protocol-v3
Helloadvertises an inclusive version range, required capability bits, and a fresh challenge;HelloAuthcompletes the transcript proof. Missing or incompatible fields fail closed before peer installation. The current binary supports only wire v3 and requires the paged-anti-entropy, byte-bounded-batches, and stable-durability-ACK capabilities. Byte-bounded batches change batch-get responses to ordered-prefix semantics, while stable durability ACKs are required before RocksDB may retire a handed-off prefix; binaries missing either contract are rejected. It is incompatible with v2, so deploy compatible binaries together rather than performing a mixed v2/v3 rolling upgrade. This transition is also a Rust source break for advanced users of the public generatedactordb::protoAPI and the publicreplication::SenderMsg: reconciliation messages gained cursor, revision, page, and completion fields, so struct literals and exhaustive assumptions must be updated. - Shutdown is explicit and fallible —
Node::shutdown().awaitatomically closes public-operation admission (NodeError::ShuttingDownfor later calls), joins listener/connection/dial work, waits for already-admitted API work, drains the bounded serving forwarded-mutation completion owner without aborting accepted apply/outcome/fanout, stops replication senders, drains every fragment mailbox, and reports the final local-store flush. A flush error retains the quiesced fragment and its batch; retryshutdownuntil it succeeds. The node does not resume after an error. This local fence does not wait for replication ACKs.shutdown_with_replication_drainis the opt-in bounded alternative: it freezes placement, waits for admitted mutations, and requires a stable full reconciliation plus the configured ACK durability from every frozen target before entering the same local shutdown. Timeout/cancellation leaves the quiesced node and transports alive for retry; ordinaryshutdownexplicitly abandons the peer guarantee.RetryableSenderrequests a fresh exact sender fence, whileUnavailablemeans local-only shutdown has already made the guarantee impossible. Its report proves a per-target fenced LWW version floor, not exact/global cache equality, quorum, or a backend-independent fsync guarantee. Offered RocksDB defaults toSyncWal; explicitAsyncWalis weaker forStoreCommitted, whileStableCommittedforces the explicit WAL fence. Legacy empty-map mode covers only senders known at the fence. DroppingNodeonly requests best-effort cancellation and stop; it cannot report a flush failure, await a join, or safely release the same live RocksDB attachment. This pre-release change is a Rust source break: callers must handle the new shutdownResult, and exhaustive matches overNodeError,ReplicationAckDurability,ApplyDurability,KvMessage, the generatedReplicationAckLevel, or the advancedSupervisorMessageAPI must include their new variants (StableCommittedandFlushStablewhere applicable). - Tombstones are retained —
Node::startrejects everytombstone_gc_age: Some(..)setting. Safe reclamation needs durable per-replica convergence evidence; elapsed wall-clock time cannot prove that condition. Whole retired fragment namespaces are a separate lifecycle case. - Revoked RocksDB prefixes retire after a stable revision-pinned handoff — placement
seals every replacement sender, requires every current replica to complete a
fresh full reconciliation and
StableCommittedbarrier at one unchanged source revision, then quiesces and flushes the source before RocksDB commits a synchronous idempotent range tombstone. A weak ACK, replica-set/generation change, fetched winner, deletion error, or re-add fails closed inDrainingor retains the namespace. Custom stores retain prefixes unless they explicitly opt into lifecycle deletion. RocksDB compaction owns later physical space reclamation. The certificate covers LWW winners and their TTL metadata; independent expiry cleanup may make later byte snapshots differ. - Custom worker-pool supervision is not lossless — a worker crash drops
accepted jobs in that worker's mailbox and closes their reply ports; those
jobs are not replayed. Exact per-worker reservations reclaim their capacity,
and orderly shutdown drains accepted work. A restarted dispatcher is
republished through a generation-checked fragment route, but migration to
ractor's
Factorylifecycle remains tracked for discard policy and lossless restart. The formerly public splitWorkerPooladmission/routing surface was removed in this pre-release API: direct embedders should useKvActor.
Contributors
To learn more about contributing to actordb please see CONTRIBUTING.md.
License
This project is licensed under MIT.