tsoracle — monotonic timestamps and gapless sequences in Rust
Strictly monotonic timestamps and gapless sequences in Rust.
Distributed · highly-available · fault-tolerant · embeddable
$ cargo install tsoracle $ tsoracle
A timestamp oracle is a service that hands out strictly increasing integer IDs which order events across a distributed system. You reach for one when you're building a database with MVCC, merging change-data from many shards, or maintaining audit logs with a real happens-before relation across machines.
Per-host wall clocks aren't monotonic enough. Database sequences don't scale across shards. A TSO is the canonical answer — and the one Spanner, CockroachDB, and FoundationDB all use internally.
tsoracle goes one step further: the same engine also issues gapless sequences. Where a timestamp only has to be ordered, a sequence has to be contiguous — …, 41, 42, 43, … with nothing skipped — exactly what you want for surrogate keys, invoice and order numbers, or ledger line numbers. One round-trip, one durable counter per named key, no holes across crashes or failover. Call get_seq() instead of get_ts().
→ Read: Why distributed systems need a TSO · Gapless sequences with GetSeq
Every issued timestamp is strictly greater than every previous one. No duplicates, no regression.
Need contiguous IDs, not just ordered ones? get_seq() hands out dense, gapless runs per named key — nothing skipped, never reused across crashes or failover.
Window state is fsync'd before any timestamp from it is handed out. Restarts never rewind.
Production openraft and OmniPaxos drivers included. One trait to back tsoracle with raft-rs, etcd, or your own replicated log.
Leader discovery, request coalescing, reconnection — handled. Just call get_ts() or get_seq().
Allocator, leader, and request metrics emitted through the metrics facade. Prometheus-ready.
Run the standalone CLI, or embed tsoracle-server in your own binary with a few lines of Rust.
The leader fsyncs the new high-water mark and replicates it through raft before the range leaves the process. Clients hand out IDs from the batch locally — millions per second on commodity hardware.
get_seq() rides the same path for gapless IDs: the leader advances a durable per-key counter, fsyncs and replicates the advance, and returns a contiguous block. The one difference is that a gapless advance can't be un-spent, so the call is non-idempotent — an ambiguous failure is surfaced as SeqUncertain, never silently retried.
$ cargo install tsoracle
$ tsoracle
# Listens on 127.0.0.1:50551
# Persists state to ./tsoracle-data/
# Bare `tsoracle` is shorthand for `tsoracle serve file`.use tsoracle_server::Server;
use tsoracle_driver_file::FileDriver;
let driver = FileDriver::open_or_init("./data")?;
let server = Server::builder()
.consensus_driver(driver)
.build()?;
server.serve_with_shutdown(addr, shutdown).await?;The embedded path runs tsoracle-server as a library in your process. See the embedded-server example for the runnable version.
Other distribution channels: source releases and changelogs on GitHub Releases, the published Rust crates on crates.io, container images at ghcr.io/prisma-risk/tsoracle, and a Helm chart at ghcr.io/prisma-risk/charts. The full source tree is at github.com/prisma-risk/tsoracle under the Apache-2.0 license.
tsoracle is tested with coverage-guided fuzzing on its postcard decoders, failpoint-driven crash tests on the storage and replication paths, and an in-process + multi-process stress harness covering both the openraft and OmniPaxos backends. Fuzz harnesses run nightly; the stress harness runs on every PR.
Details: crates/tsoracle-tests · fuzz/
Bug reports and enhancement proposals are tracked as issues on GitHub. Pick the template that matches what you're reporting:
- Bug report — for a defect with a concrete reproduction. The template walks you through the trace, the affected lines, and a regression test that fails today.
- Work scope — for an enhancement or any bounded change (docs, refactor, new component, hardening) with explicit scope and exit criteria.
Browse the existing reports at github.com/prisma-risk/tsoracle/issues. Blank issues are disabled — please use one of the templates so triage stays fast.
Security vulnerabilities follow a separate, private path: please do not open a public issue. Open a draft security advisory instead. We acknowledge within 24 hours, triage within 72 hours, and coordinate disclosure within 30 days. The full policy is in SECURITY.md.
Contributions are welcome — from a one-line clarification to a new ConsensusDriver implementation. The most useful changes are grounded in real use of tsoracle: a bug you hit in deployment, a missing operational hook, an example that fills a documentation gap. For anything larger than a few lines, please open an issue first — aligning on scope before code is faster than redesigning in review. Discussion happens on the issue itself; there is no separate forum or chat.
The contributor guide — local setup, the checks CI will run on your PR, the panic policy, the release flow — is in CONTRIBUTING.md. In short:
- The Rust toolchain is pinned in
rust-toolchain.toml— anycargocommand installs the right version. You'll also needprotocand LLVM/Clang (libclang) for the proto and RocksDB builds. - Match CI before pushing:
cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features -- -D warnings,cargo test --workspace --all-features. A tracked pre-commit hook runs the first two automatically. - Commit messages follow Conventional Commits (
feat:,fix:,chore:, …) — the prefix drives the per-crate semver bump on release-plz.
Looking for somewhere to start? The good first issue and help wanted labels list issues that are scoped and well-described for new contributors.
- Why distributed systems need a timestamp oracle
A timestamp oracle hands out strictly increasing IDs that order events across a distributed system. Why cross-machine ordering is hard, and what a TSO solves.
- TSO vs UUIDv7
A timestamp oracle gives strict global monotonicity; UUIDv7 gives k-sortable IDs locally. When each is correct, and where UUIDv7 silently fails.
- Gapless sequences: GetSeq alongside GetTs
tsoracle now hands out two kinds of strictly-ordered integer: monotonic timestamps and gapless, dense sequences. What gaplessness costs, why GetSeq is non-idempotent, and when to reach for it.
How tsoracle works — the architecture summary on this site.
DeepWiki — prose documentation covering the window allocator, the ConsensusDriver contract, and operational topics (fsync cost, leader handoff, deployment topologies).
docs.rs/tsoracle-server — generated API reference.