Expand description
Database-ready quorum WAL over 1, 3, or 5 GCS Rapid zonal buckets (typically three).
Start with SegmentedVolume::recover using the database’s durable
checkpoint boundary. Consume the returned Recovery stream through its
fixed end, then call Recovery::start. Submit caller-numbered opaque records
through WalHandle::enqueue_append; admission returns an
AppendCompletion without waiting for durability. A completion success is
durable on a strict-majority quorum of the configured zones. Use
Error::may_have_committed on a completion failure: ambiguous outcomes
may replay after takeover, so delivery is at-least-once.
Error::ActiveSegmentFull is different: it is definitive admission
backpressure, consumes no sequence number, and leaves the writer healthy.
Truncate retained sealed history so a deferred rotation can proceed, then
retry the same record.
One sequence number identifies one application-encoded transaction record;
the WAL never adds another application-level batching layer. Startup replay
uses record boundaries through WalSeqNo and is available only on
Recovery. Once the database has
durably checkpointed its own state, call WalHandle::truncate_before to
delete whole sealed segments below that checkpoint. Startup and periodic
maintenance retry already-authorized deletion tombstones before repairing
missing immutable sealed copies; degraded rotations also schedule a targeted
repair. Active segments are never repaired in place, and maintenance never
advances the database’s checkpoint floor autonomously. See the database_wal
example for the complete lifecycle.
Every fallible public operation returns Error.
§Operational constraints
A typical production volume uses three Rapid buckets in distinct zones of
one region plus a regional bucket for the default GCS manifest store. Bucket
arguments are full v2 resource names, and zonal list order is durable replica
identity. Current manifests bind that ordered set in chorus.buckets;
its length is the replica count, and missing, duplicate, or later mismatched
bindings are rejected. Physical zone placement is not discoverable through
this API and remains an operator check.
Rapid zonal buckets have neither Object Versioning nor soft delete, so
replacement and truncation deletes are permanent. Archive immutable sealed
segments before truncation when the database needs point-in-time recovery.
The default GCS manifest directory retains roughly 115 current checksummed
segments before rotation defers; custom ManifestStore implementations
may report a larger budget. WalEngineConfig::max_active_segment_bytes
bounds that deferral with non-poisoning Error::ActiveSegmentFull
backpressure.
The default GCS manifest is one repeatedly updated object. Treat
WalEngineConfig::max_segment_bytes as the single-pending refill floor:
for encoded throughput T bytes/s and worst-case provision-plus-fold
latency L, size it above T * L with operational headroom. Exceeding that
bound is fail-closed: append dispatch pauses before an unregistered segment
can receive records.
§Complete example
This is the same source built as the database_wal Cargo example.
//! End-to-end database integration using three GCS Rapid zonal buckets for
//! segment data plus one regional bucket hosting the manifest register.
use std::env;
use anyhow::{Context, Result};
use bytes::{BufMut, Bytes, BytesMut};
use chorus_client::{
BearerAuth, ClientConfig, GrpcReplicaFactory, RefreshingAuthConfig, SegmentedVolume,
WalEngineConfig, WalSeqNo,
};
use futures::TryStreamExt;
#[tokio::main]
async fn main() -> Result<()> {
// Use the Cloud Storage gRPC endpoint (`https://storage.googleapis.com`) for
// every factory unless the deployment has validated another gRPC-compatible
// endpoint. Regional JSON/XML endpoints do not support gRPC. Each zonal
// factory must use a distinct bucket, passed as a full v2 resource name such
// as `projects/_/buckets/my-rapid-zone-a`.
let endpoints = required_csv("CHORUS_GCS_ENDPOINTS")?;
let buckets = required_csv("CHORUS_GCS_BUCKETS")?;
let manifest_endpoint = required_var("CHORUS_GCS_MANIFEST_ENDPOINT")?;
let manifest_bucket = required_var("CHORUS_GCS_MANIFEST_BUCKET")?;
anyhow::ensure!(endpoints.len() == 3 && buckets.len() == 3);
// ADC is the normal production path. All factories share this ArcSwap-backed
// auth handle, so refreshed tokens reach existing gRPC clients without
// rebuilding channels. Use GrpcReplicaFactory::connect(..., Some(token)) for
// a controlled static-token deployment, or None for a local anonymous fake.
let auth = BearerAuth::google_adc(RefreshingAuthConfig::default()).await?;
let mut factories = Vec::with_capacity(3);
for zone in 0..3 {
factories.push(
GrpcReplicaFactory::connect_with_auth(
zone,
&endpoints[zone],
buckets[zone].clone(),
auth.clone(),
)
.await?,
);
}
// the regional bucket carries only the manifest control register
let manifest_factory =
GrpcReplicaFactory::connect_with_auth(3, &manifest_endpoint, manifest_bucket, auth.clone())
.await?;
let volume = SegmentedVolume::new(
factories,
manifest_factory,
"databases/orders/wal",
ClientConfig::default(),
)?;
// Load this boundary from the database's own durable checkpoint. Recovery
// starts replay there. The manifest independently prevents objects below a
// committed truncation floor from resurrecting history.
let checkpoint_resume = WalSeqNo::record(0);
// Recovery fences and seals the manifest's active tail, then directly
// streams the fixed replay range. It does not create the next appendable
// segment until this stream has been consumed successfully.
let mut recovery = volume.recover(checkpoint_resume).await?;
let replay_end = recovery.end;
while let Some(record) = recovery.try_next().await? {
apply_to_database(record.payload.as_ref())?;
let _next_durable_resume = record.next_seqno();
}
// The default queue is 256 records; size max_segment_bytes separately from
// encoded throughput and the manifest CAS-rate budget.
let mut wal = recovery.start(WalEngineConfig::default()).await?;
// The database owns sequence allocation. `replay_end` is the first sequence
// available after startup. Admission verifies each contiguous number and
// returns before GCS durability, allowing the completions to flow through a
// separate transactional apply pipeline.
let first_seqno = replay_end;
let completion_a = wal
.enqueue_append(
first_seqno,
encode_transaction(&[b"put customer/7 alice", b"update customer-index/7"]),
)
.await?;
let completion_b = wal
.enqueue_append(
WalSeqNo::record(first_seqno.record_index + 1),
encode_transaction(&[b"debit account/9 50"]),
)
.await?;
// The appends are now owned by the WAL and may be concurrently in flight.
// Await durability before applying each transaction to database state. On
// failure, Error::may_have_committed() distinguishes ambiguous outcomes
// that require recovery before reusing or advancing the sequence number.
let (receipt_a, receipt_b) = match tokio::try_join!(completion_a, completion_b) {
Ok(receipts) => receipts,
Err(error) => {
if error.may_have_committed() {
anyhow::bail!(
"append outcome may have committed; restart recovery before reusing the \
sequence number: {error}"
);
}
return Err(error.into());
}
};
println!(
"committed appends {:?} and {:?}; durable resumes {:?} and {:?}",
receipt_a.seqno,
receipt_b.seqno,
receipt_a.next_seqno(),
receipt_b.next_seqno()
);
println!(
"replayed through exclusive record {}",
replay_end.record_index
);
// Sealed-segment repair is automatic at engine startup, after each
// rotation, and periodically. It never gap-fills or mutates the active
// appendable segment, and the engine serializes it with truncation.
// After the database has durably checkpointed all effects before
// `replay_end`, request whole-segment deletion. Replay exists only during
// startup recovery, so a running WAL has no concurrent reader pins. Persist
// `replay_end` as the database checkpoint regardless of deletion counts;
// Chorus does not manufacture a replacement checkpoint.
let report = wal.truncate_before(replay_end).await?;
println!(
"truncation deleted_segments={} deleted_objects={}",
report.deleted_segments, report.deleted_objects
);
// Graceful shutdown rejects new appends and allows the accepted-work drain
// and owned-task joins up to `shutdown_timeout`. On expiry it aborts the
// remaining tasks, bounds the cleanup join by the same interval, and returns
// `Error::ShutdownTimeout`; `abort` remains for exceptional termination.
wal.shutdown().await?;
Ok(())
}
fn required_var(name: &str) -> Result<String> {
env::var(name).with_context(|| format!("set {name}"))
}
fn required_csv(name: &str) -> Result<Vec<String>> {
env::var(name)
.with_context(|| format!("{name} is required"))
.map(|value| value.split(',').map(str::to_owned).collect())
}
fn apply_to_database(record: &[u8]) -> Result<()> {
println!("apply {}", String::from_utf8_lossy(record));
Ok(())
}
// The database, not the WAL, defines transaction encoding. This example uses a
// tiny length-prefixed envelope; production code would normally call its
// existing transaction codec and submit the resulting bytes once.
fn encode_transaction(operations: &[&[u8]]) -> Bytes {
let mut encoded = BytesMut::new();
encoded.put_u32(operations.len() as u32);
for operation in operations {
encoded.put_u32(operation.len() as u32);
encoded.extend_from_slice(operation);
}
encoded.freeze()
}Structs§
- Append
Completion - Owned durability future returned after a record is admitted in sequence.
- Append
Receipt - Durability evidence for one caller-numbered record.
- Bearer
Auth - Cloneable authentication handle shared by existing gRPC clients.
- Client
Config - Retry policy for transport operations used by recovery and writes.
- Grpc
Replica Factory - Reusable Google Storage v2 channel, authentication handle, and zonal routing token for one Rapid bucket.
- Manifest
Version - Opaque optimistic-concurrency token for one observed register state.
- Noop
Metrics Recorder - Recorder used when the application does not configure metrics.
- Recovery
- Startup recovery stream and capability to resume from a fenced frontier.
- Recovery
Timings - Wall-clock cost of the recovery phases that finish before the replay stream
is handed back.
epoch_claimis the manifest CAS that fences prior writers;preparecovers directory adoption and repair, committed-seal enforcement, and the appendable-candidate takeover walk. Replay andRecovery::startare timed by the caller. This is diagnostic only and is never read on the correctness path. - Refreshing
Auth Config - Controls how aggressively a refreshing credential is renewed.
- Repair
Report - Result of one best-effort immutable sealed-segment repair pass.
- Segmented
Volume - WAL namespace: the zonal replica factories for segment data (one per zone) plus one regional factory hosting the manifest control register.
- Truncation
Report - Result of one application-triggered truncation pass.
- Versioned
Manifest - One consistent read of the register: its fields and the version token guarding the next conditional update.
- WalEngine
Config - Capacity controls for the in-process transactional WAL pipeline.
- WalHandle
- Control plane for a running WAL engine.
- WalRecord
- One opaque application record emitted by startup replay.
- WalSeq
No - Application-assigned record number and durable checkpoint boundary.
Enums§
- Error
- Failure returned by every fallible
chorus-clientpublic operation. - Manifest
Store Error - Why a register operation did not produce a new state.
- Transport
Code - Stable transport classification used by retry and fencing logic.
Traits§
- Access
Token Source - Supplies bearer tokens to
BearerAuth. - Counter
Fn - Handle for a monotonically increasing metric.
- GaugeFn
- Handle for an absolute point-in-time metric.
- Histogram
Fn - Handle for a sampled distribution.
- Manifest
Store - A linearizable compare-and-swap register holding the manifest.
- Metrics
Recorder - Registers backend-owned metric handles used directly by Chorus.
- UpDown
Counter Fn - Handle for an additive metric that may increase or decrease.