tsink
tsink is a lightweight, in-process time-series database engine for Rust applications.
It stores time-series data in compressed chunks, persists immutable segment files to disk, and can replay a write-ahead log (WAL) after crashes — all without requiring an external server.
0.9.0upgrade note: persistent on-disk data from0.8.xis not wire-compatible with0.9.0(STORAGE_FORMAT_VERSION=2with disk-backed series index + roaring postings).
Features
- Embedded API — no external server, network protocol, or daemon required.
- Thread-safe — the storage handle is an
Arc<dyn Storage>, safe to share across threads. - Multi-series model — series identity is metric name + exact label set.
- Typed values —
f64,i64,u64,bool,bytes, andstring. - Rich queries — downsampling, aggregation (12 built-in functions), pagination, and custom bytes aggregation via the
Codec/Aggregatortraits. - Disk persistence (v2) — immutable segment files + disk-backed global
series_index.binand roaring-bitmaps postings. - WAL durability — selectable sync mode (
PeriodicorPerAppend) and replay policy (Salvagedefault orStrict). - Atomic snapshot/restore — create segment-consistent, WAL-aware backups and restore them atomically.
- Out-of-order writes — data is returned sorted by timestamp regardless of insertion order.
- Concurrent writers — multiple threads can insert simultaneously with sharded internal locking.
- Optional PromQL engine — instant and range queries with 20+ built-in functions; enable with the
promqlCargo feature. - LSM-style compaction — tiered L0 → L1 → L2 segment compaction reduces read amplification.
- Adaptive compression — Gorilla/delta codecs plus payload-level zstd compression when beneficial.
- Observability snapshots — structured WAL/flush/compaction/query counters and health state via
observability_snapshot(). - Single-process safety — per-
data_pathlock file (.tsink.lock) blocks concurrent writers in multiple processes. - cgroup-aware defaults — worker thread defaults respect container CPU quotas.
- Resource limits — configurable memory budget, series cardinality cap, and WAL size limit with admission backpressure.
Table of Contents
- Installation
- Quick Start
- Async Usage
- Server Mode
- Python Bindings
- Query APIs
- Series Discovery
- Value Model
- Label Constraints
- PromQL Engine
- Persistence and WAL
- Observability
- Snapshots and Restore
- On-Disk Layout
- Compression and Encoding
- Performance
- Architecture
- StorageBuilder Options
- Resource Limits and Backpressure
- Container Support
- Error Handling
- Advanced Usage
- Examples
- Benchmarks and Tests
- Development Scripts
- Project Structure
- Contributing
- Minimum Supported Rust Version
- License
Installation
[]
= "0.9"
Enable PromQL support:
[]
= { = "0.9", = ["promql"] }
Enable async storage facade (dedicated worker threads, runtime-agnostic futures):
[]
= { = "0.9", = ["async-storage"] }
Quick Start
use Error;
use ;
Async Usage
async-storage exposes AsyncStorage and AsyncStorageBuilder.
The async API routes requests through bounded queues to dedicated worker threads, while reusing the existing synchronous engine implementation. It is runtime-agnostic — no dependency on tokio, async-std, or any specific executor.
use ;
# async
AsyncStorageBuilder forwards all core StorageBuilder configuration, including WAL replay mode (with_wal_replay_mode) and background fail-fast (with_background_fail_fast).
AsyncStorage also provides synchronous accessors for introspection:
| Method | Description |
|---|---|
memory_used() |
Current in-memory usage in bytes. |
memory_budget() |
Configured memory budget. |
snapshot(path).await |
Create an atomic snapshot using the async write worker. |
inner() |
Access the underlying Arc<dyn Storage>. |
into_inner(self) |
Unwrap the underlying storage handle. |
Server Mode (Prometheus Wire Compatible)
Experimental: tsink-server is still experimental and under development.
This workspace includes a binary crate at crates/tsink-server that runs tsink as an async network service (tokio-based) with Prometheus remote storage wire format, PromQL HTTP API, TLS, and Bearer token authentication.
Run the server:
CLI Options
| Flag | Default | Description |
|---|---|---|
--listen <ADDR> |
127.0.0.1:9201 |
Bind address. |
-V, --version |
— | Print version and exit. |
--data-path <PATH> |
None (in-memory) | Persist tsink data under PATH. |
--wal-enabled <BOOL> |
true |
Enable WAL. |
--no-wal |
— | Disable WAL (shorthand). |
--timestamp-precision <s|ms|us|ns> |
ms |
Timestamp precision (server defaults to milliseconds). |
--retention <DURATION> |
14d | Data retention period (e.g. 14d, 720h). |
--memory-limit <BYTES> |
Unlimited | Memory budget (e.g. 1G, 1073741824). |
--cardinality-limit <N> |
Unlimited | Max unique series. |
--chunk-points <N> |
2048 | Target points per chunk. |
--max-writers <N> |
Available CPUs | Concurrent writer threads. |
--wal-sync-mode <MODE> |
periodic |
WAL fsync policy (per-append or periodic). |
--tls-cert <PATH> |
— | TLS certificate file (PEM). Requires --tls-key. |
--tls-key <PATH> |
— | TLS private key file (PEM). Requires --tls-cert. |
--auth-token <TOKEN> |
— | Require Bearer token on all endpoints except health probes. |
--enable-admin-api |
false |
Enable admin snapshot/restore endpoints. |
--admin-path-prefix <PATH> |
— | Restrict admin snapshot/restore paths under a canonical root. Requires --enable-admin-api. |
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /healthz |
Health check (returns ok). |
| GET | /ready |
Readiness probe (returns ready). |
| GET | /metrics |
Self-monitoring metrics (Prometheus exposition format). |
| GET/POST | /api/v1/query |
PromQL instant query. |
| GET/POST | /api/v1/query_range |
PromQL range query. |
| GET | /api/v1/series |
Series metadata (accepts match[] selectors). |
| GET | /api/v1/labels |
All label names. |
| GET | /api/v1/label/<name>/values |
Values for a given label. |
| POST | /api/v1/write |
Prometheus remote write (protobuf + snappy). |
| POST | /api/v1/read |
Prometheus remote read (protobuf + snappy). |
| POST | /api/v1/import/prometheus |
Prometheus text exposition format ingestion. |
| GET | /api/v1/status/tsdb |
TSDB stats (JSON). |
| POST | /api/v1/admin/snapshot |
Admin-only endpoint (disabled by default): create snapshot ({\"path\":\"...\"}). |
| POST | /api/v1/admin/restore |
Admin-only endpoint (disabled by default): restore snapshot ({\"snapshotPath\":\"...\",\"dataPath\":\"...\"}). |
| POST | /api/v1/admin/delete_series |
Admin-only endpoint (disabled by default, currently stubbed with 501). |
TLS
Provide both --tls-cert and --tls-key to enable TLS:
Authentication
When --auth-token is set, all requests except GET /healthz and GET /ready must include the header Authorization: Bearer <TOKEN>. Unauthenticated requests receive a 401 Unauthorized response.
Admin API
Admin endpoints are disabled by default. Enabling requires both --enable-admin-api and --auth-token:
To constrain snapshot/restore destinations to a fixed root:
Graceful Shutdown
The server handles SIGTERM and SIGINT signals. On receipt it stops accepting new connections, waits up to 10 seconds for in-flight requests to complete, then closes storage cleanly.
PromQL HTTP API
The query endpoints follow the Prometheus HTTP API response format:
# Instant query
# Range query
Prometheus Integration
remote_write:
- url: http://127.0.0.1:9201/api/v1/write
remote_read:
- url: http://127.0.0.1:9201/api/v1/read
Text Format Ingestion
Post Prometheus exposition format text directly:
Python Bindings
The tsink_uniffi package provides Python bindings for tsink via UniFFI. It exposes the core storage engine to Python with native performance.
Installation
Quick Start
# Build an in-memory store
=
# 64 MB
=
=
# Insert rows
# Query a single series
=
Series Discovery and Aggregation
# List all known metric + label-set combinations
=
# Query all label sets for a metric
=
# Aggregation query
=
Memory Introspection
The Python API mirrors the Rust API — see the sections below for details on query options, aggregation functions, and value types.
Query APIs
| Method | Description |
|---|---|
select(metric, labels, start, end) |
Returns points sorted by timestamp for one series. |
select_into(metric, labels, start, end, &mut buf) |
Same as select, but writes into a caller-provided buffer for allocation reuse. |
select_all(metric, start, end) |
Returns grouped results for all label sets of a metric. |
select_with_options(metric, QueryOptions) |
Supports downsampling, aggregation, custom bytes aggregation, and pagination. |
list_metrics() |
Lists all known metric + label-set series. |
list_metrics_with_wal() |
Like list_metrics, but also includes series only present in the WAL. |
select_series(SeriesSelection) |
Matcher-based series discovery (=, !=, =~, !~) with optional time-window filtering. |
All time ranges are half-open: [start, end).
Matcher-based select_series queries are resolved via persisted inverted postings (roaring bitmaps) when available, not by full scan.
Downsampling and Aggregation
use ;
let storage = new.build?;
storage.insert_rows?;
let opts = new
.with_downsample
.with_pagination;
let buckets = storage.select_with_options?;
assert_eq!;
Built-in aggregation functions:
None, Sum, Min, Max, Avg, First, Last, Count, Median, Range, Variance, StdDev.
Custom Bytes Aggregation
For non-numeric data, implement the Codec and Aggregator traits to define custom aggregation logic over bytes-encoded values:
use ;
;
;
let opts = new
.with_custom_bytes_aggregation;
Series Discovery
Use select_series with matcher-based filtering to discover series dynamically:
use ;
let selection = new
.with_metric
.with_matcher
.with_matcher
.with_time_range;
let series = storage.select_series?;
Supported matcher operators:
| Operator | Constructor | Description |
|---|---|---|
= |
SeriesMatcher::equal(name, value) |
Exact label match. |
!= |
SeriesMatcher::not_equal(name, value) |
Negated exact match. |
=~ |
SeriesMatcher::regex_match(name, pattern) |
Regex label match. |
!~ |
SeriesMatcher::regex_no_match(name, pattern) |
Negated regex match. |
Value Model
DataPoint stores a timestamp: i64 and a value: Value.
| Variant | Rust type |
|---|---|
Value::F64(f64) |
f64 |
Value::I64(i64) |
i64 |
Value::U64(u64) |
u64 |
Value::Bool(bool) |
bool |
Value::Bytes(Vec<u8>) |
raw bytes |
Value::String(String) |
UTF-8 string |
Notes:
- A series (same metric + labels) must keep a consistent value type family.
bytesandstringdata uses blob-lane encoding on disk.- Convenience conversions are provided:
DataPoint::new(ts, 42.5)auto-converts viaInto<Value>. - Accessor methods:
value.as_f64(),value.as_i64(),value.as_u64(),value.as_bool(),value.as_bytes(),value.as_str(). value.kind()returns a&'static strtag:"f64","i64","u64","bool","bytes", or"string".Value::F64(NAN)compares equal to itself, unlike standardf64, for consistent equality semantics in collections.
Automatic From conversions are provided for: f64, i64, i32, u64, u32, usize, bool, Vec<u8>, &[u8], String, and &str.
Label Constraints
Labels are key-value pairs that identify a series alongside the metric name. Labels are automatically sorted for consistent series identity — insertion order does not matter.
| Constraint | Limit |
|---|---|
| Label name length | 256 bytes |
| Label value length | 16,384 bytes (16 KB) |
| Metric name length | 65,535 bytes |
Empty label names or values are rejected. Oversized metric/label names and values are rejected with validation errors (InvalidMetricName / InvalidLabel); they are not silently truncated on write.
PromQL Engine
Enable with the promql feature. The engine supports instant and range queries over data stored in tsink.
use Arc;
use ;
use Engine;
let storage = new.build?;
storage.insert_rows?;
let engine = new;
// Instant query — evaluates at a single point in time.
let result = engine.instant_query?;
// Range query — evaluates at each step across a time window.
let result = engine.range_query?;
Use Engine::with_precision(storage, precision) if your timestamps are not in nanoseconds.
Supported functions:
| Category | Functions |
|---|---|
| Rate/counter | rate, irate, increase |
| Over-time | avg_over_time, sum_over_time, min_over_time, max_over_time, count_over_time |
| Math | abs, ceil, floor, round, clamp, clamp_min, clamp_max |
| Type conversion | scalar, vector |
| Time | time, timestamp |
| Sorting | sort, sort_desc |
| Label manipulation | label_replace, label_join |
Aggregation operators: sum, avg, min, max, count, topk, bottomk — with by/without grouping.
Binary operators: +, -, *, /, %, ^, ==, !=, <, >, <=, >=, and, or, unless — with on/ignoring vector matching and bool modifier.
Recent 0.9.0 alignment updates include metricless-selector matcher semantics, stricter one-to-one vector matching for binary ops, and metric-name dropping behavior for vector binary results.
Persistence and WAL
Set with_data_path(...) to enable persistence:
use Duration;
use ;
let storage = new
.with_data_path
.with_chunk_points
.with_wal_enabled
.with_wal_sync_mode
.with_wal_replay_mode // default
.build?;
Behavior:
close()flushes active chunks and writes immutable segment files.- With WAL enabled, reopening the same path replays durable WAL frames automatically.
- Recovery is idempotent — a high-water mark prevents double-apply of WAL frames.
- Persistent flush/compaction maintenance runs for
data_pathstorage even when WAL is disabled. - A per-path process lock file (
.tsink.lock) prevents concurrent opens of the samedata_path.
Sync Modes
| Mode | Trade-off |
|---|---|
WalSyncMode::Periodic(Duration) |
Lower fsync overhead; small recent-write loss window on crash. |
WalSyncMode::PerAppend |
Strongest durability for acknowledged writes; higher fsync cost. |
Replay Modes
| Mode | Behavior on mid-log WAL corruption/truncation |
|---|---|
WalReplayMode::Salvage (default) |
Replays valid prefix and stops at first broken frame. |
WalReplayMode::Strict |
Fails startup replay immediately with corruption error. |
Observability
Use observability_snapshot() to inspect structured runtime internals:
let obs = storage.observability_snapshot;
println!;
println!;
println!;
println!;
println!;
obs.health also exposes background_errors_total, fail_fast_enabled, fail_fast_triggered, and last_background_error.
Snapshots and Restore
Create an atomic, segment-consistent, WAL-aware snapshot from a live storage:
storage.snapshot?;
Restore atomically into another data directory:
restore_from_snapshot?;
restore_from_snapshot replaces the destination through a staged publish (with rollback on activation failure) and preserves WAL replay semantics on reopen.
On-Disk Layout
When persistence is enabled, tsink writes separate numeric/blob lane segment families plus a global series index:
<data_path>/
series_index.bin
.tsink.lock
lane_numeric/
segments/
L0/...
L1/...
L2/...
lane_blob/
segments/
L0/...
L1/...
L2/...
wal/ # Present when WAL is enabled
wal-0000000000000000.log
wal-0000000000000001.log
...
Each segment directory contains:
manifest.bin, chunks.bin, chunk_index.bin, series.bin, postings.bin.
Storage format notes:
0.9.0uses segment format v2 (TSM2/CHK2/CID2/SRS2/PST2headers).postings.binstores roaring bitmap payloads for matcher acceleration.chunks.binrecords may be stored uncompressed or zstd-compressed (flagged per chunk).- CRC32 + XXH64 checksums and crash-safe staged publish (
write tmp -> fsync -> atomic rename) are used for integrity and durability.
Compaction
tsink uses tiered LSM-style compaction across three levels:
| Level | Trigger | Description |
|---|---|---|
| L0 | Every flush | Newly flushed segments land here. |
| L1 | 4 L0 segments | L0 segments are merged and re-chunked into L1. |
| L2 | 4 L1 segments | L1 segments are merged into larger L2 segments. |
Compaction runs automatically in the background and is transparent to reads and writes.
Compression and Encoding
tsink uses two parallel encoding lanes based on value type:
Numeric Lane
Timestamps and numeric values (f64, i64, u64, bool) are encoded with specialized codecs. The encoder tries all applicable candidates and picks the most compact.
Timestamp codecs:
| Codec | Strategy |
|---|---|
| Fixed-step RLE | Run-length encoding for fixed-interval timestamps. |
| Delta-of-delta bitpack | Delta-of-delta encoding with bit-packing (primary strategy). |
| Delta varint | Varint-encoded deltas for irregular intervals. |
Value codecs:
| Codec | Type | Strategy |
|---|---|---|
| Gorilla XOR | f64 |
Gorilla-style XOR of IEEE 754 floats. |
| Zigzag delta bitpack | i64 |
Zigzag encoding + delta + bit-packing. |
| Delta bitpack | u64 |
Delta encoding + bit-packing. |
| Constant RLE | any numeric | Run-length encoding for constant values. |
| Bool bitpack | bool |
Bit-level packing (1 bit per value). |
Blob Lane
bytes and string values are encoded with delta block compression in a separate blob lane.
After lane encoding, chunk payloads are optionally compressed with fast zstd (level 1) when that reduces size. Read paths transparently decode both compressed and raw payloads.
Performance
Compression
The adaptive codec selection (Gorilla XOR, delta-of-delta, RLE, bitpacking) achieves ~0.68 bytes per data point for typical f64 time-series workloads — down from 16 bytes uncompressed (8-byte timestamp + 8-byte value), a ~23x compression ratio.
Throughput
Insert throughput (single-series, in-memory):
| Batch size | Latency | Throughput |
|---|---|---|
| 1 | ~1.7 us | ~577K points/sec |
| 10 | ~5.3 us | ~1.89M points/sec |
| 1,000 | ~155 us | ~6.4M points/sec |
Select throughput (single-series, in-memory):
| Result size | Latency | Throughput |
|---|---|---|
| 1 point | ~114 ns | ~8.8M queries/sec |
| 10 points | ~296 ns | ~33.6M points/sec |
| 1,000 points | ~15.4 us | ~64M points/sec |
| 1,000,000 points | ~20.9 ms | ~48M points/sec |
Numbers above are ballpark figures from a single run (--quick mode). Run benchmarks on your hardware:
Architecture
┌─────────────────────────────────────────────────────┐
│ Public API │
│ StorageBuilder / Storage / AsyncStorage / PromQL │
├────────────┬──────────────┬─────────────────────────┤
│ Writers │ Readers │ Compactor │
│ (N threads)│ (concurrent)│ (background merges) │
├────────────┴──────────────┴─────────────────────────┤
│ Engine (partitioned by time) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Active │ │ Immutable│ │ Segments │ │
│ │ Chunks │→ │ Chunks │→ │ (L0/L1/ │ │
│ │ (memory) │ │ (memory) │ │ L2 disk)│ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────┤
│ WAL (write-ahead log) │ Series Registry + Index │
└─────────────────────────┴───────────────────────────┘
Key internals:
- Time partitions split data by wall-clock intervals (default: 1 hour).
- Chunks group data points (default: 2048 per chunk) with delta-of-delta timestamp encoding and per-lane value encoding (numeric vs. blob).
- Series registry + global index maps metric name + label set → series ID and persists dictionaries in
series_index.binfor faster reopen/startup. - Inverted postings use roaring bitmaps for metric/label matcher candidate resolution.
- Segment files are immutable, CRC32 + XXH64 checksummed, and consist of:
manifest.bin,chunks.bin,chunk_index.bin,series.bin,postings.bin. - Sharded locking (64 internal shards) reduces write contention under high concurrency.
- Background flush periodically converts active chunks into immutable chunks (default: every 250ms).
- Background compaction merges segments across levels (default: every 5s).
StorageBuilder Options
| Method | Default | Description |
|---|---|---|
with_data_path(path) |
None (in-memory only) |
Directory for segment files and WAL. |
with_chunk_points(n) |
2048 |
Target data points per chunk before flushing. |
with_wal_enabled(bool) |
true |
Enable/disable write-ahead logging. |
with_wal_sync_mode(mode) |
Periodic(1s) |
WAL fsync policy. |
with_wal_replay_mode(mode) |
Salvage |
WAL corruption handling during replay (Salvage / Strict). |
with_wal_size_limit(bytes) |
Unlimited | Hard cap on total WAL bytes across all WAL segments. |
with_wal_buffer_size(n) |
4096 | WAL buffer size in bytes. |
with_retention(duration) |
14 days | Data retention window. |
with_retention_enforced(bool) |
true |
Enforce retention window (false keeps data forever). |
with_timestamp_precision(p) |
Nanoseconds |
Timestamp unit (Seconds, Milliseconds, Microseconds, Nanoseconds). |
with_max_writers(n) |
Available CPUs (cgroup-aware) | Maximum concurrent writer threads. |
with_write_timeout(duration) |
30s | Timeout for write operations. |
with_partition_duration(duration) |
1 hour | Time partition granularity. |
with_memory_limit(bytes) |
Unlimited | Hard in-memory budget with admission backpressure before writes. |
with_cardinality_limit(series) |
Unlimited | Hard cap on total metric+label series cardinality. |
with_background_fail_fast(bool) |
false |
Stop new operations after background flush/compaction worker failures. |
Resource Limits and Backpressure
tsink provides three configurable resource limits that protect against unbounded growth:
Memory Limit
let storage = new
.with_memory_limit // 512 MB
.build?;
When the memory budget is reached, new writes block until a background flush frees memory. This provides admission backpressure rather than OOM crashes. 0.9.0 also switched write-path memory checks to incremental shard accounting to avoid full memory scans on each admission decision.
Cardinality Limit
let storage = new
.with_cardinality_limit
.build?;
Caps the total number of unique metric + label-set combinations. Writes that would create new series beyond this limit return TsinkError::CardinalityLimitExceeded.
WAL Size Limit
let storage = new
.with_wal_size_limit // 1 GB
.build?;
Caps the total WAL bytes on disk. Writes that would exceed this limit return TsinkError::WalSizeLimitExceeded.
Container Support
tsink automatically detects cgroup CPU quotas when running inside containers (Docker, Kubernetes, etc.). This affects:
- Writer thread count — defaults to available CPUs within the cgroup quota, not the host CPU count.
- Async read worker default (when using
async-storage) — also derived from cgroup CPU availability.
tsink::cgroup::get_memory_limit() is available if you want to derive an explicit with_memory_limit(...) value from container limits.
Override with the TSINK_MAX_CPUS environment variable:
TSINK_MAX_CPUS=4
Error Handling
All fallible operations return tsink::Result<T>, which wraps TsinkError. Key error variants:
| Error | Cause |
|---|---|
InvalidTimeRange |
start >= end in a query. |
WriteTimeout |
Writer could not acquire a slot within the configured timeout. |
MemoryBudgetExceeded |
Write blocked and memory budget was not freed in time. |
CardinalityLimitExceeded |
Too many unique series. |
WalSizeLimitExceeded |
WAL disk usage reached the configured cap. |
ValueTypeMismatch |
Inserting a different value type into an existing series. |
OutOfRetention |
Data point timestamp is outside the retention window. |
DataCorruption |
Checksum mismatch during segment read. |
StorageShuttingDown |
Background fail-fast was triggered after a worker failure. |
InvalidConfiguration |
Invalid builder/runtime settings (for example concurrent data_path lock conflict, invalid snapshot/restore paths, or unsupported mode combinations). |
StorageClosed |
Operation attempted after close() was called. |
Advanced Usage
Concurrent Operations
The storage handle is Arc-based and safe to share across threads:
use Arc;
use thread;
use ;
let storage = new
.with_max_writers
.build?;
let mut handles = vec!;
for worker_id in 0..8
for handle in handles
Out-of-Order Writes
tsink accepts data points in any order and returns them sorted by timestamp on read:
use ;
storage.insert_rows?;
let points = storage.select?;
// points are returned in chronological order: 1.0, 2.0, 3.0, 5.0
assert!;
WAL Recovery
After a crash, tsink automatically replays the WAL on the next open:
use ;
// First run — data is written and WAL-protected
let storage = new
.with_data_path
.build?;
storage.insert_rows?;
// Crash happens here — close() was never called
// Next run — recovery is automatic
let storage = new
.with_data_path // Same path
.with_wal_replay_mode
.build?; // WAL replay happens here
// Previously inserted data is available
let points = storage.select?;
Recovery is idempotent — a high-water mark ensures WAL frames are never applied twice. Use WalReplayMode::Strict if you prefer startup failure over salvage when a WAL tail is corrupted.
Snapshot Workflow Example
Use snapshot() for an atomic, segment-consistent, WAL-aware backup of a live storage:
use ;
let storage = new
.with_data_path
.build?;
storage.insert_rows?;
storage.snapshot?;
storage.close?;
Restore the snapshot atomically into another data path:
use StorageBuilder;
restore_from_snapshot?;
let restored = new
.with_data_path
.build?;
restore_from_snapshot replaces the target data directory in a single staged publish step and preserves WAL replay semantics on reopen.
Multi-Dimensional Label Querying
use ;
storage.insert_rows?;
// Query all label combinations for a metric
let all_results = storage.select_all?;
for in all_results
// Discover all known series
let all_series = storage.list_metrics?;
for series in all_series
Production Configuration
use Duration;
use ;
let storage = new
.with_data_path
.with_timestamp_precision
.with_retention // 30 days
.with_partition_duration // 6-hour partitions
.with_chunk_points
.with_max_writers
.with_write_timeout
.with_memory_limit // 1 GB
.with_cardinality_limit
.with_wal_sync_mode
.with_wal_buffer_size // 16 KB
.build?;
Examples
| Example | Description |
|---|---|
basic_usage |
Simple insert and select with labels. |
persistent_storage |
Disk persistence and WAL recovery. |
production_example |
Multi-threaded ingest/query workload with tracing-friendly logging and persistent config knobs. |
comprehensive |
Multiple features: concurrent ops, retention, downsampling, and aggregation. |
Benchmarks and Tests
Benchmark Suites
| Benchmark | Description |
|---|---|
storage_benchmarks |
Criterion-based matrix of insert/select operations at various scales. |
workload |
Realistic workload simulation with multiple series, out-of-order writes, and bytes-per-point measurement. |
Development Scripts
| Script | Description |
|---|---|
scripts/measure_perf.sh <quick|full> |
Run Criterion benchmarks with quick or full sample sizes. |
scripts/measure_bpp.sh <quick|full> |
Measure bytes-per-point compression efficiency. |
scripts/check_bench_regression.sh [threshold] |
Detect Criterion benchmark regressions beyond a threshold (default: 50%). |
The measure_bpp.sh script accepts environment variables for workload tuning:
| Variable | Description |
|---|---|
TSINK_BPP_RUNS |
Number of workload benchmark repetitions in measure_bpp.sh. |
TSINK_ACTIVE_SERIES |
Number of concurrent series. |
TSINK_PRIME_ALL_SERIES |
Prime all series before measurement (1/true to enable). |
TSINK_MIN_POINTS_PER_SERIES |
Minimum target points per series before measuring BPP. |
TSINK_WARMUP_POINTS / TSINK_MEASURE_POINTS |
Points ingested during warmup and measurement phases. |
TSINK_BATCH_SIZE |
Insert batch size. |
TSINK_OOO_MAX_SECONDS / TSINK_OOO_PERMILLE |
Out-of-order write tuning. |
TSINK_SPARSE_EMIT_PERMILLE / TSINK_SHORT_LIVED_LIFETIME_STEPS |
Sparse and short-lived series workload tuning. |
TSINK_RETENTION_SECONDS / TSINK_PARTITION_SECONDS |
Retention and partition windows. |
TSINK_STEP_SECONDS / TSINK_SETTLE_MILLIS |
Workload step spacing and settle delay tuning. |
TSINK_FAIL_ON_TARGET |
Fail if compression target is not met. |
Project Structure
.
├── src/ # Core tsink engine + public API
├── crates/
│ ├── tsink-server/ # Prometheus wire-compatible server binary
│ └── tsink-uniffi/ # Python bindings (UniFFI)
├── examples/ # Runnable usage examples
├── benches/ # Criterion benchmark suites
├── tests/ # Integration tests
├── scripts/ # Benchmark/perf helper scripts
├── CHANGELOG.md # Release notes
└── README.md
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Minimum Supported Rust Version
No explicit MSRV is pinned yet. The crate uses Rust 2021 edition and is tested on stable.
License
MIT