meathook-rs

Capture ephemeral data.
Lose nothing.

Store-backed tiers and file encoders compose in code. No YAML plugin system, no boxed errors.

Read the docs

crash and panic loss follow the outermost store; SIGTERM drains the full stack

built on the satay-rs sans-IO action model

ephemeral → durable

APIs forget. meathook doesn't.

A weather reading at 14:02 is gone from the source by 14:03. The API only ever serves latest. If you don't poll and store it continuously, that reading is lost forever.

now

{"k":"air_temperature","v":29.4,"st":"S24","ts":"14:02:00Z"}

gone in 60s

meathook
data/air_temperature/6 files
  • 2024-06-18/13.parquet
  • 2024-06-18/14.parquet
  • 2024-06-19/14.parquet
  • 2024-06-20/14.parquet
  • 2024-07-18/14.parquet
  • 2024-08-18/14.parquet

A single reading is worthless.
A year of hourly readings is a dataset.

tick · flush · ship

Each pipeline keeps its own rhythm.

Collectors tick every minute, every five, or every hour. Store-backed tiers choose when to flush; the selected encoder writes one file when its window closes.

air_temperature1m
rainfall5m
pm251h
0m15m30m45m60m
flush
window · 1h
data/{pipeline}/{YYYY-MM-DD}/{HH}-{MM}-{SS}-{hash}.{ext}

failure inventory

Durability is a store choice.

store/jsonl.rs

Tier owns windowing, flush policy, and replay over any Store<R>. An outer MemStore trades a bounded volatile window for fewer fsyncs; aJsonlStore-backed tier fsyncs batches when they reach it and retains them until its downstream accepts them. Implement the same trait for SQLite, object storage, or another backend.

failurewhat happensdata lost
SIGKILL / OOM-killJsonlStore replays batches it received; outer MemStore state is goneouter memory window + ≤ 1 torn JSONL record
Task panicsupervisor rebuilds; JsonlStore replays batches it receivedrecords still held in MemStore
Sink outage (HF 5xx)the tier retains each window and retries on its next firingnone
Graceful SIGTERMruntime drains every tier and terminal before exitnone
Backing store lostunflushed records in that Store are gonechoose JsonlStore on a PVC for crash recovery

four slots, four swaps

One stack. Four independent choices.

A tier owns policy, a Store owns records, an Encoder owns bytes, and the terminal owns delivery. Swap one without rewriting the others.

Example stacks rotate through MemStore, JsonlStore, custom Store implementations, ParquetEncoder, JsonEncoder, CsvEncoder, HfSink, and custom terminal sinks. Tiers are optional and may be nested.

footprint

A year-long collector that fits in 14.8 megabytes.

A release binary polling three NEA weather endpoints on a single tokio runtime. I/O-bound between ticks; memory stays flat.

50%0%
50mb0mb
cpu %
peak 0.3% · ~300/core
rss mb
0s5m10m
13.6mbrss avg
0%cpu avg
14.8mbrss peak
0.3%cpu peak
10 minsampled
303samples

sampled live from a release binary running the nea example

example · singapore weather → huggingface

A real collector, not a toy.

The reference consumer polls Singapore's NEA / data.gov.sg realtime weather via the satay-generatednea-rsclient. Three pipelines, each its own tokio task, deduped by key.

air_temperature1m

per-station readings

dedupe key: (station_id, timestamp)

rainfall5m

per-station readings

dedupe key: (station_id, timestamp)

pm251h

regional readings

dedupe key: (region, timestamp)

Each flush ships one encoded file at a deterministic, content-keyed Hive-style path. Replays are safe and distinct payloads never collide.

repo: zeon256/nea-weather
path: data/{pipeline}/{YYYY-MM-DD}/{HH}-{MM}-{SS}-{hash}.parquet

store · encoder · sink

Stores, encoders, and sinks compose.

Implement Store<R>,Encoder, orSink<R>. Start with SinkStack::new(), add tiers in record-flow order, then finish with.terminal(). Configure formats directly on the terminal with.encoder().

1use std::{time::Duration, env};2 3use meathook::{4    FlushPolicy, HfSink, JsonEncoder, JsonlStore, Meathook, MemStore,5    Pipeline, SatayCollector, SinkStack,6};7use satay_reqwest::ReqwestActionExt as _;8use reqwest::Client;9 10#[tokio::main]11async fn main() -> Result<(), meathook::runtime::RuntimeError> {12    let client = Client::new();13    let token = env::var("HF_TOKEN").expect("HF_TOKEN must be set");14 15    Meathook::builder()16        .pipeline(move || {17            let api = nea_rs::Api::new();18            let collector = SatayCollector::new(19                "air_temperature",20                client.clone(),21                move |client| {22                    let api = api.clone();23                    async move { api.air_temperature().send_with(&client).await }24                },25                |response| flatten(response),26            );27 28            // Declaration order is record flow: memory batches for five minutes29            // or 10k records, JSONL fsyncs each batch and forwards hourly, then30            // the terminal uploads it. Disk is reached only when memory fires.31            let sink = SinkStack::new()32                .tier(33                    MemStore::new(),34                    FlushPolicy::new(Duration::from_secs(300), 10_000),35                )36                .tier(37                    JsonlStore::new("/var/lib/meathook/spool/air_temperature"),38                    FlushPolicy::hourly(),39                )40                .terminal(41                    HfSink::new(client.clone(), "you/your-dataset", token.clone())42                        .encoder(JsonEncoder),43                );44 45            Pipeline::new(collector, sink, Duration::from_secs(60))46                .with_key_fn(|r: &MyRecord| (r.station_id.clone(), r.timestamp.clone()))47        })48        .run()49        .await50}

records stay plain structs: #[derive(Serialize, Deserialize)] is enough for every built-in encoder

cargo features

Bring only the I/O you need.

With --no-default-features, the core keeps its traits and JsonEncoder. Cargo features add Parquet, CSV, satay collectors, and Hugging Face delivery.

Feature flags

featuredefaultwhat it enables
parquetParquetEncoder via arrow, parquet, and serde_arrow
csv·CsvEncoder for flat record types
satay·SatayCollector for satay-generated API clients
huggingfaceHfSink and CommitAction; implies parquet and satay

Missing an integration? Request one. Built one? Open a PR.

Start capturing before the API forgets.

docs.rs