Timslite

.NET bindings for the timslite time-series storage engine.


Keywords
database, mmap, storage, timeseries, timslite
License
MIT
Install
Install-Package Timslite -Version 0.1.4

Documentation

timslite

高性能 Rust 时序数据存储库: mmap 分段存储、Block 聚合、延迟压缩、持久化队列、Journal 变更日志、独立 C ABI wrapper、Python wrapper、Node.js wrapper、Java wrapper 和 .NET wrapper。

License CI Rust codecov

timslite 是一个可嵌入到其它项目中的本地时序数据存储引擎。它面向需要高性能本地写入、精确时间戳读取、范围查询、持久化消费队列和轻量变更日志的应用,不需要单独部署数据库服务。

你可以把主项目作为标准 Rust library 使用。C ABI 已迁移到独立的 wrapper/cffi crate (timslitecffi),Python/Node.js/Java/.NET wrapper 分别位于 wrapper/pythonwrapper/nodejswrapper/javawrapper/dotnet

当前状态

timslite 仍处于 0.x 早期发布阶段。已发布到 crates.io、PyPI、npm、Maven Central 和 NuGet;仓库主干可能已进入下一版本开发,因此 README 中的官方安装示例固定使用当前已发布的版本。Rust API、C ABI、Python wrapper、Node.js wrapper、Java wrapper、.NET wrapper 和磁盘格式在 1.0 前仍可能调整。

当前可用能力:

  • Rust 核心存储引擎已实现。
  • timslite crate 是标准 Rust library,不导出 C ABI 符号,也不直接构建 cdylib
  • C ABI wrapper 位于 wrapper/cffi,头文件维护在 wrapper/cffi/include/timslite.h
  • Python wrapper 位于 wrapper/python
  • Node.js wrapper 位于 wrapper/nodejs
  • Java wrapper 位于 wrapper/java
  • .NET wrapper 位于 wrapper/dotnet
  • 官方包已覆盖 Rust、Python、Node.js、Java 和 .NET 生态。
  • CI 已覆盖 Rust format、clippy、unit/integration tests 和 Python wrapper tests。
  • 性能基准仍在后续完善中;benches/ 目录已存在,但 benchmark target 还不是必过验证项。

适用场景

适合:

  • 在 Rust 服务或本地工具中嵌入时序数据存储。
  • 高频 timestamp 写入、精确 timestamp 读取和范围查询。
  • 希望用 mmap 分段文件保存本地数据,并对历史 block 做压缩。
  • 需要基于 dataset 的持久化 queue 消费。
  • 需要辅助热迁移、审计或同步的变更日志。

暂不适合:

  • 需要严格事务、WAL 或强一致 crash recovery 的场景。
  • 需要任意 UTF-8 dataset 名称的场景。公开 dataset name、dataset type、queue group name 必须匹配 ^[0-9A-Za-z_-]+$

安装与依赖

Rust 依赖

官方 crates.io 包:

[dependencies]
timslite = "0.1.2"

或使用 Cargo 命令:

cargo add timslite@0.1.2

仓库开发或验证未发布改动时,可以继续使用本地 path 或 git 依赖:

[dependencies]
timslite = { path = "../timslite" }
[dependencies]
timslite = { git = "https://github.com/snower/timslite" }

Python 包

官方 PyPI 包:

python -m pip install "timslite"

发布包优先使用预编译 wheel;如果当前平台没有匹配 wheel,pip 会回退到 sdist 源码构建,需要本机 Rust toolchain 和原生编译工具链。

Node.js 包

官方 npm 包:

npm install timslite

Node.js wrapper 要求 Node.js 22+,timestamp 和 identifier 使用 bigint 暴露以避免精度丢失。发布包包含支持平台的预编译 Node-API native binding;缺失预编译产物时会尝试源码构建。

Java 包

官方 Maven Central 坐标:

<dependency>
  <groupId>io.github.snower</groupId>
  <artifactId>timslite</artifactId>
  <version>0.1.2</version>
</dependency>

Gradle:

implementation("io.github.snower:timslite:0.1.2")

Java wrapper 支持 Java 8+,JAR 内包含已发布平台的 native libraries。

.NET 包

官方 NuGet 包:

dotnet add package Timslite

.NET wrapper 目标框架为 net8.0,NuGet 包内包含已发布 RID 的 native assets。

运行依赖

timslite 不需要外部数据库进程。主要 Rust 依赖:

  • memmap2: mmap 文件 I/O。
  • zstd: 默认 block 压缩算法。
  • miniz_oxide: deflate 压缩支持。
  • log: 日志门面。 主 Rust library 不依赖 C ABI 运行时;libc 仅用于独立的 wrapper/cffi crate。

快速开始

use timslite::{DataSetConfigBuilder, Store, StoreConfig};

fn main() -> timslite::Result<()> {
    let store_config = StoreConfig::builder()
        .enable_background_thread(true)
        .enable_journal(true)
        .build();

    let mut store = Store::open("./data/timslite", store_config.clone())?;

    let dataset_config = DataSetConfigBuilder::from_store(&store_config)
        .index_continuous(0)
        .retention_window(0);

    let dataset = store.create_dataset_with_config(
        "sensor",
        "temperature",
        Some(dataset_config),
    )?;

    dataset.write(1_700_000_001, b"21.5")?;
    dataset.write(1_700_000_002, b"21.7")?;

    let row = dataset.read(1_700_000_001)?;
    assert_eq!(row.unwrap().1, b"21.5");

    let rows = dataset.query(1_700_000_001, 1_700_000_010)?;
    assert_eq!(rows.len(), 2);

    store.close()?;
    Ok(())
}

打开已有 dataset:

use timslite::{Store, StoreConfig};

fn main() -> timslite::Result<()> {
    let mut store = Store::open("./data/timslite", StoreConfig::default())?;
    let dataset = store.open_dataset("sensor", "temperature")?;

    let latest = dataset.read_latest()?;
    println!("latest: {:?}", latest);

    Ok(())
}

DataSet::read_latest() 会读取 latest_written_timestamp 对应的精确 timestamp。如果该 timestamp 已删除或已过期,会返回 Ok(None),不会自动向前搜索上一条有效数据。DataSet::read(timestamp) 始终按传入的精确 timestamp 读取;read(-1) 读取真实 timestamp -1,不表示 latest。

核心概念

Store

Store 是顶层入口,负责:

  • dataset 生命周期、registry、全局运行时上下文。
  • 全局 immutable compressed-block cache。
  • 可选后台维护线程。
  • 可选内置 .journal/logs 专用 journal 存储。
  • 新建 dataset 时使用的默认配置。

Store::create_dataset*Store::open_dataset* 直接返回 DataSet。普通写入、读取、删除、查询和普通 dataset queue 都在 DataSet / DatasetQueue 上操作,Store 不再提供面向 record 的 facade API。

Dataset

dataset 由 (name, dataset_type) 标识,例如 ("sensor", "temperature")

通过 Store 获取的 DataSet 会携带 cache、journal sink、read-only 状态等运行时上下文;调用方不需要也不应该为普通 record API 传入这些内部资源。

常用操作:

  • write(timestamp, data): 写入或修正一个 timestamp。
  • append(timestamp, data): 追加到最新 tail record,或创建新的未来 timestamp。
  • delete(timestamp): 标记一个 timestamp 为删除。
  • read(timestamp): 精确读取一个 timestamp。
  • query(start, end): 范围查询,左右闭区间。
  • read_exist / query_exist / read_length / query_length: 轻量存在性和长度查询。

单条逻辑 record 最大 4 MiB。普通聚合 block payload 上限为 64 KiB;更大的 record 会使用 single-record block。

Timestamp

timestamp 是应用传入的 signed i64,timslite 不要求它必须是系统时间。0 和负数都是合法业务 timestamp;读取 latest 请使用显式 read_latest()

如果业务 timestamp 使用秒,retention_window 也应使用秒。如果业务使用其它单位,retention 也必须使用同一单位。

Index 模式

index_continuous = 0: 稀疏索引,通常作为默认选择。

index_continuous = 1: 连续 timestamp grid,gap 使用 filler entry 表示。适合需要固定 timestamp slot 的场景,但稀疏写入时会产生更多索引项。

压缩与缓存

pending block 保持 raw 且可变。下一次写入导致 pending block overflow 时,旧 block 会 seal、compress,并变为 immutable。只有 immutable compressed block 会进入全局读缓存。

默认压缩算法是 zstd。compress_type = 1 时可使用 deflate。

配置示例

use std::time::Duration;
use timslite::StoreConfig;

let config = StoreConfig::builder()
    .data_segment_size(64 * 1024 * 1024)
    .index_segment_size(4 * 1024 * 1024)
    .initial_data_segment_size(256 * 1024)
    .initial_index_segment_size(4 * 1024)
    .compress_type(0) // 0=zstd, 1=deflate
    .compress_level(6)
    .cache_max_memory(256 * 1024 * 1024)
    .flush_interval(Duration::from_secs(15))
    .idle_timeout(Duration::from_secs(30 * 60))
    .retention_check_hour(0) // UTC hour
    .enable_background_thread(true)
    .enable_journal(true)
    .build();

新建 dataset 时,可以基于 store 默认配置创建 dataset 专属配置:

use timslite::DataSetConfigBuilder;

let dataset_config = DataSetConfigBuilder::from_store(&config)
    .index_continuous(0)
    .retention_window(30 * 86400)
    .build();

Append 语义

append(timestamp, data) 用于扩展最新记录:

  • timestamp < latest_written_timestamp: 返回错误。
  • timestamp > latest_written_timestamp: 创建新 record。
  • timestamp == latest_written_timestamp: 仅当 latest record 仍是未压缩 tail record 时允许原地追加。
  • 空 append 会先执行 timestamp 顺序和 retention 校验,再作为 no-op。
  • 追加后的逻辑 record 仍不能超过 4 MiB。

追加到已有 latest record 不会再次通知普通 dataset queue;创建新 timestamp 时会通知 queue。

Queue

每个 dataset 可以打开一个持久化 queue。每个 consumer group 使用独立的 4 KiB state file 保存消费进度。

use std::time::Duration;
use timslite::{Store, StoreConfig};

fn main() -> timslite::Result<()> {
    let mut store = Store::open("./data/timslite-queue", StoreConfig::default())?;
    let dataset = store.create_dataset_with_config("jobs", "default", None)?;

    let queue = dataset.open_queue()?;
    let consumer = queue.open_consumer("worker_1")?;

    let ts = queue.push(b"job payload")?;

    if let Some((polled_ts, payload)) = consumer.poll(Duration::from_secs(1))? {
        assert_eq!(polled_ts, ts);
        assert_eq!(payload, b"job payload");
        consumer.ack(polled_ts)?;
    }

    Ok(())
}

如果 consumer 需要收到后续 push 的数据,应先打开 consumer 再 push。新 consumer 的初始位置从当前 latest_written_timestamp 开始。

Journal

Journal 由 StoreConfig.enable_journal(true) 控制,默认开启。普通 dataset 的 DataSetConfig.enable_journal 默认关闭,需要显式开启后才记录该 dataset 的 create/drop/write/delete/append。

内置 journal 存储固定为 .journal/logs,不作为普通 DataSet 暴露,记录:

  • 0x01: create dataset。
  • 0x02: drop dataset。
  • 0x11: dataset write。
  • 0x12: dataset delete。
  • 0x13: dataset append。

Journal timestamp 是从 1 开始递增的 sequence,不是系统时间。

Journal v1 是辅助变更日志,不是严格 WAL。write/delete/append record 中保存的是源 dataset 的 index pointer。消费者如果需要 payload,必须在源 dataset 仍可访问时通过 Store::read_journal_source_record(dataset_identifier, index_info) 读取:

let journal_queue = store.open_journal_queue()?;
let consumer = journal_queue.open_consumer("migrator_1")?;

如果源 dataset 已删除、已被 retention 回收、已 checkpoint、已 correction 或已覆盖,旧 journal record 可能无法单独精确 replay。

Retention

retention_window 是 dataset 级配置,单位与业务 timestamp 相同:

let dataset_config = timslite::DataSetConfigBuilder::from_store(&store_config)
    .retention_window(30 * 86400); // timestamp 为秒时表示 30 天

启用 retention 后:

  • 过期 timestamp 的读取返回 None
  • 过期 timestamp 不允许 delete、out-of-order rewrite 或 correction。
  • 回收只删除整个时间范围都已过期的 data/index segment。
  • retention_check_hour 使用 UTC hour,范围 0..=23

C ABI

C ABI 是独立 wrapper crate,不属于主 timslite Rust library。公开 C 头文件位于 wrapper/cffi/include/timslite.h,实现位于 wrapper/cffi,crate 名为 timslitecffi

C ABI wrapper 暂不作为独立官方包发布;需要 C ABI 时从源码构建:

cargo build --manifest-path wrapper/cffi/Cargo.toml --release
cargo test --manifest-path wrapper/cffi/Cargo.toml -- --test-threads=1

主要 API 组:

  • Store lifecycle: tmsl_store_open, tmsl_store_open_with_config, tmsl_store_close
  • Dataset lifecycle: tmsl_dataset_create, tmsl_dataset_create_with_config, tmsl_dataset_open, tmsl_dataset_close, tmsl_dataset_drop
  • Data operations: tmsl_dataset_write, tmsl_dataset_append, tmsl_dataset_delete, tmsl_dataset_read, tmsl_dataset_query
  • Queue operations: tmsl_queue_open, tmsl_queue_consumer_open, tmsl_queue_poll, tmsl_queue_ack
  • Background helpers: tmsl_store_tick_background_tasks, tmsl_store_next_background_delay

read/query/queue API 返回的 buffer 必须使用头文件中声明的 timslite free 函数释放。

Python Wrapper

Python wrapper 位于 wrapper/python,暴露与 Rust 类似的 Store、DataSet、Query、Queue 概念。

官方安装:

python -m pip install "timslite"

仓库开发和测试:

cd wrapper/python
pip install maturin pytest
maturin develop
python -m pytest tests/ -v

Python 例子见 wrapper/python/README.md

Node.js Wrapper

Node.js wrapper 位于 wrapper/nodejs,基于 Node-API (napi-rs),暴露与 Rust 类似的 Store、Dataset、Queue 概念。

官方安装:

npm install timslite

本地构建和测试:

cd wrapper/nodejs
npm install
npm run build
node -e "const t = require('.'); console.log('version:', t.version())"

Node.js wrapper 使用 BigInt 表示 timestamp 和 identifier,避免精度丢失。

Java Wrapper

Java wrapper 位于 wrapper/java,基于 UniFFI 生成 C ABI 绑定并通过 Kotlin/JVM 后端暴露 Java API。支持 Java 8+,提供 Store、Dataset、Queue、Journal 等完整功能。

官方 Maven Central 坐标:

<dependency>
  <groupId>io.github.snower</groupId>
  <artifactId>timslite</artifactId>
  <version>0.1.2</version>
</dependency>

Java wrapper 详情见 wrapper/java/README.md,设计文档见 wrapper/java/design.md

.NET Wrapper

.NET wrapper 位于 wrapper/dotnet,基于 UniFFI 生成 .NET 绑定,提供 Store、Dataset、Queue、Journal 等 API。

官方 NuGet 安装:

dotnet add package Timslite

.NET wrapper 详情见 wrapper/dotnet/README.md

使用注意事项

  • Dataset name、dataset type、queue group name 必须匹配 ^[0-9A-Za-z_-]+$,最长 255 字节。
  • .journal/logs 是 Store 内部保留路径,不作为普通 DataSet 打开。Journal 开启时通过 journal_read / journal_query / open_journal_queue 等专用 API 访问。
  • 磁盘上的多字节 integer 使用 little-endian。
  • 后台 flush 默认 15 秒,只执行 mmap sync,不 seal、不压缩 pending block。
  • timslite 优先高读写性能,不提供严格事务保证。crash 后最近未 flush 的写入可能丢失。
  • 在本仓库运行测试时建议使用 cargo test -- --test-threads=1,因为文件系统测试共享临时路径。

从源码构建与验证

作为依赖集成时,通常只需要正常 cargo build。如果你从源码验证:

cargo build
cargo build --release
cargo test -- --test-threads=1

根 crate 的构建只验证 Rust library;需要 C ABI 时请单独构建 wrapper/cffi:

cargo build --manifest-path wrapper/cffi/Cargo.toml --release
cargo test --manifest-path wrapper/cffi/Cargo.toml -- --test-threads=1

更严格的本地检查:

cargo fmt -- --check
cargo clippy --all-targets -- -D warnings

更多文档

设计文档入口是 design.md。常用专题: