Rindle docs and package mapSkip to main content

Cluster

Struct Cluster 

Source
pub struct Cluster { /* private fields */ }
Expand description

The multi-threaded replica handle (cheap Rc clone). Single-thread-use on the coordinator side; a server runs it on a dedicated owner thread and continuously drains the ClusterEvent channel returned by open on whatever thread it likes — but it must never STOP draining while writes flow (the channel-out is bounded; see open’s draining contract).

Implementations§

Source§

impl Cluster

Source

pub fn open( path: impl AsRef<Path>, n_workers: usize, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>

Open a parallel replica over a file-backed SQLite database with n_workers IVM worker threads. Returns the handle and the channel-out Receiver for derived ClusterEvents. A fresh file gets ordinary journal_mode = wal (design 306 D5); an existing wal or wal2 file keeps its mode. Asserts sqlite3_threadsafe() != 0, like Db::open.

§Draining contract

The consumer MUST drain the returned Receiver continuously, on its own thread, for the whole life of the Cluster. The channel-out is BOUNDED (FOLLOWER-LAG-SHED §4, rung 0b — event_channel_bound(n_workers) events, to bound memory under a slow consumer). A full event channel blocks a worker, which can block capture streaming or a later snapshot barrier on the writer. Reading events only after writes return can therefore deadlock. The drain loop must also avoid blocking on slow downstream subscribers. Commit return does not mean all worker events have been delivered; use Progressed for that boundary.

Prefer ClusterConsumer, which spawns and owns that continuous drain thread for you; reach for raw open only when you intend to run the drain loop yourself.

Source

pub fn open_with_planning( path: impl AsRef<Path>, n_workers: usize, plan_queries: bool, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>

Like open, but with explicit control over the cost-based join-flip planner. When plan_queries is true, every worker runs the planner (a SqliteCostModel over its own connection, cached) at query registration to annotate flip before lowering. Result-preserving; the plan is frozen per registration. The planner is server-side (table-source) only and open enables it by default; pass false here to opt out (e.g. to pin the unplanned path).

Source

pub fn open_with_options( path: impl AsRef<Path>, n_workers: usize, plan_queries: bool, operator_storage: OperatorStorage, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>

Like open_with_planning, but also selects the operator-scratch-state backend for every worker Engine. Pass OperatorStorage::SqliteSpill to spill stateful-operator state to a per-worker on-disk temp database instead of RAM. The schema-only engine never builds stateful operators, so it always stays memory-backed. Fresh files get plain wal (design 306 D5); use open_with_journal for the daemon’s wal2 opt-in.

Source

pub fn open_with_journal( path: impl AsRef<Path>, n_workers: usize, journal: JournalMode, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>

Like open, but with an explicit fresh-file JournalMode — the daemon’s roles pass JournalMode::Wal2 here (design 306 D5: wal2 is something the daemon requests, never something the library imposes). An existing file already in wal or wal2 keeps its mode either way.

Source

pub fn open_with( path: impl AsRef<Path>, n_workers: usize, opts: OpenOptions, ) -> Result<(Cluster, Receiver<ClusterEvent>), ReplicaError>

The full-combination opener: every OpenOptions field is honored, so any planner × operator-storage × journal combination is one call (e.g. a planned, spill-backed cluster on a wal2 daemon store). The named rungs above are conveniences over this.

Source

pub fn register_table(&self, table: &str) -> Result<(), ReplicaError>

Register a base table: discover its columns + PK, ensure the PK UNIQUE index TableSource requires, teach the capture hook the column types, and build the source on every worker (any may host a query referencing it). Idempotent. Rejects BLOB columns / PK-less tables.

The table must be plain — SQL triggers and generated columns are rejected (see the crate-level “supported usage” docs). Foreign-key cascades are represented by the explicit row deltas observed by the preupdate hook.

Source

pub fn query(&self, query_id: QueryId, ast: Ast) -> Result<usize, ReplicaError>

Register a live query under the caller-supplied QueryId tag. The query is sharded to a worker, built + hydrated there, and its Hydrated baseline emitted to the channel-out. Returns the index of the worker that hosts it (a BuildError surfaces synchronously here as Err). Later writes can emit provisional Changed slices before commit. That worker emits Progressed after successful commit and all its slices — so the returned index is what the async progress layer (the drain thread) keys a query’s cv position by. The cluster owns the assignment; callers pass the returned index along rather than recomputing it, so a future scheduling change (work-stealing) stays internal to the cluster (§2.6).

Rejected while the open transaction is speculative (view_is_speculative). Registration COLD-hydrates on the worker, and a worker mid-stream holds the pushed chunks in its sources’ overlays — so the baseline would be drawn from rows no one has committed, while tagged with the PRE-commit hydrated_tx read below. That is strictly worse than the equivalent one-shot leak: a one-shot is thrown away, but a subscription applies every later delta on top of its baseline, so a poisoned one breaks view-after-write == fresh-query for the life of the subscription. (A stream that has pinned snapshots but pushed nothing is fine — the hydrate is a pure read of the committed post-(N-1) state; see the speculative field docs.)

Refusing (rather than deferring internally) is forced by the same threading that shapes read_snapshot: WorkerPool::register_query_on blocks this thread on the worker’s reply, and only this thread can send the transaction’s terminal marker — so a worker that held the registration until commit would deadlock against its own coordinator. Callers that can wait should re-issue after the commit.

Source

pub fn family( &self, query_id: QueryId, stripped: Ast, params: Vec<Box<str>>, bindings: Vec<Binding>, ) -> Result<usize, ReplicaError>

Register a parameterized query family (design 310 §5) under query_id: one pipeline over stripped (the family’s template AST) partitioned on params, with bindings bound and hydrated up front — each emits a Update::PartitionHydrated instead of a Hydrated. Placed on one worker by the same query_id % n rule as query (design §6, v1: one worker per family). Returns the worker.

Source

pub fn bind( &self, query_id: QueryId, binding: Binding, ) -> Result<(), ReplicaError>

Bind one more partition of the family registered under query_id (design 310 §4.4): it hydrates on the hosting worker’s latest committed snapshot and emits its PartitionHydrated. Refused, like query, while a speculative transaction is streaming.

Source

pub fn unbind( &self, query_id: QueryId, binding: Binding, ) -> Result<bool, ReplicaError>

Unbind one partition of the family under query_id; nothing is emitted. Ok(false) if it was not bound.

Source

pub fn analyze_query(&self, ast: Ast) -> Result<AnalyzeReport, ReplicaError>

Analyze ast COLD against the current committed state and return where its time and rows go (ANALYZE-QUERY-DESIGN.md). Read-only and fully self-contained: it runs on the CALLING thread over a private connection (analyze_standalone) — no worker engine, no live statement cache, no lease — so it never enters a worker’s command queue and a genuinely slow cold hydrate cannot stall (or be stalled by) commits. BuildError (unknown table/column, unsupported shape) surfaces as Err.

Source

pub fn analyze_spec(&self) -> AnalyzeSpec

The Send snapshot for running analyze_standalone on another thread: the daemon’s /analyze builds one here on its command loop, then runs the analysis on a dedicated thread so the loop — and with it the write path — never blocks on a slow cold hydrate.

Source

pub fn query_json( &self, query_id: QueryId, json: &str, ) -> Result<usize, ReplicaError>

Convenience: parse a Zero-wire AST from JSON, then Cluster::query.

Source

pub fn destroy_query(&self, query_id: QueryId) -> bool

Gracefully tear down a query the consumer no longer wants (destroyed / dematerialized / unsubscribed): reclaim its pipeline on the hosting worker. Unlike a fault, this is solicited, so it emits no ClusterEvent — after it returns, no further events arrive for query_id. Returns true if a live query was found and removed; idempotent (a repeat call returns false). The reclaimed graph slots are recycled by a later query.

Source

pub fn read_snapshot( &self, query_id: QueryId, ) -> Result<Vec<CaughtChange>, ReplicaError>

Re-read the current assembled view of query_id as a one-shot snapshot — the SSR read path (SSR-DESIGN.md §3). Routes to the hosting worker (same query_id % n shard as query) and returns the live view as hydration Adds, with no subscription and no streaming state. A degraded shard or an unregistered query yields an empty snapshot (the one-shot reports “no rows” rather than failing); a raised read boundary on a live worker (§5.3 sum overflow) is Err — never a NULL aggregate rendered as data. A snapshot is refused while the open transaction is speculative (view_is_speculative): it has already streamed uncommitted changes into the worker graph, and only its terminal commit marker makes those changes publicly visible. Callers that can wait should poll that predicate and defer the read to the commit rather than surfacing this error — a caller pinned to one thread with the writer (rindled’s engine loop) must NOT block here: the same thread has to keep running to reach the commit.

Source

pub fn view_is_speculative(&self) -> bool

Whether the workers’ assembled views currently carry uncommitted rows — true from the moment an open transaction streams its first chunk (PUSH_CHUNK_ROWS) until that transaction commits or rolls back. This is the exact window in which read_snapshot refuses, exposed as a predicate so a caller can QUEUE a one-shot read until the transaction closes instead of failing it. An open transaction that never crosses the streaming threshold is not speculative — it holds its changes on the writer, so reads stay serviceable.

Source

pub fn write(&self) -> Result<ClusterWriteTxn, ReplicaError>

Open the single-writer transaction. Errors if one is already open.

Source

pub fn committed_tx_id(&self) -> TxId

The last durably-committed global tx id (0 if none yet).

The database’s schema cookie (PRAGMA schema_version), read on the writer connection so an open mutation transaction’s uncommitted DDL is visible. Hosts whose trusted SQL facade runs no statement classification compare it across a transaction to detect DDL that must invalidate reader pools and capture registration.

Source

pub fn foreign_key_audit( &self, max_rows: usize, ) -> Result<ForeignKeyAudit, ReplicaError>

Walk every declared foreign key and report violating rows — the opt-in audit a replica runs instead of paying for enforcement on every applied row.

PRAGMA foreign_key_check reports what is in the file, not what SQLite would have refused, so it answers the same question whether this cluster was opened ForeignKeys::Enforced or ForeignKeys::Unenforced — which is exactly why a follower can apply with enforcement off and still prove referential integrity on demand. It is a full scan of the referencing tables: an operator command, a post-restore gate, or a soak assertion, never a per-commit step. Runs on the read-only connection, so it takes no write lock and does not disturb an applier.

Pass FOREIGN_KEY_AUDIT_ROW_CAP unless you have a reason not to, or 0 for no cap.

Source

pub fn sync(&self)

Block until every worker has drained its command queue — i.e. emitted the Hydrated/Changed events for all preceding query / commit calls to the channel-out. A quiesce/test affordance (a steady-state consumer just drains the channel continuously); relies on per-worker FIFO command order, so a returned ping implies all prior work is done.

Doubles as the liveness sweep: a worker that died (or hung) is detected here and respawned (its lost queries delivered a terminal ClusterEvent::Faulted), so a steady-state consumer that drains + periodically sync()s also self-heals.

Source

pub fn set_push_deadline_ms(&self, ms: u64)

Set the per-push derive deadline (FOLLOWER-LAG-SHED §6.6 — the runaway-push bail), in milliseconds; 0 disables it. A single chunk’s derive exceeding this faults the worker’s queries promptly and cleanly (FaultCause::PushDeadline, no detached thread) instead of wedging until the barrier watchdog detaches the whole worker. Applies immediately to every worker, respawns included. Default: 10 s, aligned with the barrier watchdog it pre-empts.

Source

pub fn set_join_precheck_bounds( &self, per_join: Option<usize>, per_graph: usize, )

Set the join membership pre-check bounds (designs/311-JOIN-MEMBERSHIP-PRECHECK-DESIGN.md §8) on every worker engine — respawns and post-fault rebuilds included. per_join is the distinct-key bound one join may track (None = off, the engine default); per_graph caps the total across a worker’s joins. Applied at each worker’s next commit barrier, before anything derives; a change resets the workers’ sets, which rebuild by observing the next hydrate-shaped fetch through each join.

Source

pub fn read<T>( &self, f: impl FnOnce(&Connection) -> Result<T>, ) -> Result<T, ReplicaError>

Run SQL against a physically read-only connection using SQLite snapshot semantics. This connection does not see the writer’s uncommitted changes. Schema changes belong in Self::exec_ddl, and row mutations in Self::write.

Source

pub fn view_schema(&self, ast: &Ast) -> Result<Schema, ReplicaError>

The hierarchical view [Schema] ast materializes to over the registered tables — the coordinator parity for Db::view_schema. Derived from the schema-only engine (the same source-schema mapping the workers use), so the shape, sort, singular flag, and in-view relationships line up with the change stream. A layer that ships views to a remote (the normalized wire schema + hello) needs this without registering the query. BuildError (unknown table/column) surfaces as Err.

Source

pub fn maintain( &self, opts: &MaintenanceOptions, ) -> Result<MaintenanceReport, ReplicaError>

Run one online maintenance pass on the writer connection: refresh planner stats (PRAGMA optimize, bounded by the analysis_limit set at open), return up to incremental_vacuum_pages freelist pages to the OS once at least freelist_threshold_pages have built up, and take a non-blocking PASSIVE WAL checkpoint — see MaintenanceOptions.

Designed to run on a timer in the gap between commits on the coordinator thread: each step is bounded (no full VACUUM/ANALYZE, no exclusive lock) and the maintenance writes land only in sqlite_stat* / the freelist, which the CDC capture hook ignores. It is skipped (returning MaintenanceReport::skipped()) while a write transaction is open — e.g. a follower stream holds one open across frames — so it never disturbs an in-flight txn.

Source

pub fn checkpoint_truncate(&self) -> Result<WalCheckpoint, ReplicaError>

Checkpoint and truncate the WAL through the coordinator writer. Snapshot/install code uses this after quiescing writes so the portable main file carries every committed frame. The public Self::read connection is physically read-only and deliberately cannot perform a checkpoint.

Source

pub fn exec_ddl(&self, sql: &str) -> Result<(), ReplicaError>

Run schema DDL (CREATE/ALTER/DROP/REINDEX) against the writer connection — the supported way to define the plain base tables you then register_table and write through. The historical bounded ANALYZE call is also accepted; row-changing statements and row-producing DDL are rejected. The complete batch and schema-envelope validation commit atomically. Rejected while a write transaction is open (DDL there would be invisible to the workers until commit). Mirrors Db::exec_ddl.

Source

pub fn exec_ddl_with_marker( &self, marker_table: &str, key: &str, statements: &[String], ) -> Result<DdlApplyReport, ReplicaError>

Apply schema statements AND stamp a durable idempotency marker (keymarker_table) in ONE ordinary transaction on the writer connection. See ApplyStore::exec_ddl_with_marker — this is the same primitive, delegated (design 309). A marker and its DDL commit together, so retrying an applied migration can detect the marker without applying the statements again.

Source

pub fn exec_ddl_with_marker_and_step_effects<F>( &self, marker_table: &str, key: &str, statements: &[String], apply_step_effects: F, ) -> Result<DdlApplyReport, ReplicaError>
where F: FnMut(&Connection, &DdlStep) -> Result<()>,

exec_ddl_with_marker, with one caller-owned bookkeeping hook invoked after every real statement and its authorizer actions. See ApplyStore::exec_ddl_with_marker_and_step_effects.

Source

pub fn analyze_full(&self) -> Result<(), ReplicaError>

Run a full ANALYZE on the writer to build deep planner statistics (sqlite_stat4), not just the bounded sqlite_stat1 the maintenance tick refreshes. Call this once after a bulk load / seed (or periodically) to sharpen the cost model. Restores the bounded analysis_limit afterward, so it does not slow the ongoing maintenance tick. Rejected while a write transaction is open.

This used to be described as what makes an ORDER BY … LIMIT n displacement re-fetch seek rather than scan (GitHub #68). It is not, since the query builder began lifting a sargable leading-column bound — see maintenance::analyze_full.

Source

pub fn enable_client_mutations(&self) -> Result<(), ReplicaError>

One-time (idempotent) setup for the client-mutations protocol (the coordinator parity for Db::enable_client_mutations): create CLIENT_MUTATIONS_TABLE and register it like any base table (CDC capture + sources on the schema engine and every worker), so lmid rides the change stream co-transactionally with the data and each client’s one-row system query can host it. Rejected mid-write-txn.

Source

pub fn enable_realtime_lifecycle(&self) -> Result<(), ReplicaError>

One-time (idempotent) setup for the §4 realtime lifecycle (the coordinator parity for Db::enable_realtime_lifecycle — see its doc for why every table is REGISTERED, not just created): the scope-session doorbell (§4.1), the downgrade watermark fence (§4.2), the mutation-outcomes resolution surface (populated by Slice I-ii), plus ROOM_CLIENT_MUTATIONS_TABLE — §7.1’s “ordinary footprint data after downgrade”. Requires enable_client_mutations first (it owns the room ledger’s DDL); rejected mid-write-txn.

Source

pub fn client_lmid(&self, client_id: &str) -> Result<u64, ReplicaError>

The high-water mutation id durably recorded for client_id (0 if none — a new client). Coordinator parity for Db::client_lmid; requires enable_client_mutations.

Source

pub fn apply_mutations( &self, registry: &MutatorRegistry, envelopes: &[MutationEnvelope], ) -> Result<MutationOutcome, ReplicaError>

Apply a client’s mutation push over the cluster (the coordinator parity for Db::apply_mutations): each envelope’s mutator runs in its own transaction (with the co-transactional lmid upsert), in mid order. Same per-envelope rules as the single-thread path — mid ≤ lmid skipped (idempotent redelivery), mid == lmid + 1 applied, a gap rejected at that envelope; a mutator error / panic / unknown name rolls effects back and commits lmid only (processed-as-no-op — no rejection signal).

Unlike the Db path it does not return the changed-query set inline — every commit’s batches (including the lmid row on the client’s own system query) flow asynchronously through the drain. Must be called with no write transaction open (it manages its own).

Trait Implementations§

Source§

impl Clone for Cluster

Source§

fn clone(&self) -> Cluster

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,