meathook-rs
Capture ephemeral data.
Lose nothing.
Store-backed tiers and file encoders compose in code. No YAML plugin system, no boxed errors.
≤ 1 torn record on SIGKILL, none on panic or SIGTERM
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
gone in 60s
- 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.
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.
data/{pipeline}/{YYYY-MM-DD}/{HH}-{MM}-{SS}-{hash}.{ext}Durability is a store choice.
store/jsonl.rs
Tier owns windowing, flush policy, and replay over any Store<R>.MemStore stays process-local;JsonlStore fsyncs records before ingest returns. Implement the same trait for SQLite, object storage, or another backend.
| failure | what happens | data lost |
|---|---|---|
| SIGKILL / OOM-kill | JsonlStore replays leftover segments on the next start | ≤ 1 torn record |
| Task panic | supervisor rebuilds the pipeline and every tier | none |
| Sink outage (HF 5xx) | the tier retains each window and retries on its next firing | none |
| Graceful SIGTERM | runtime drains every tier and terminal before exit | none |
| Backing store lost | unflushed records in that Store are gone | choose JsonlStore on a PVC for crash recovery |
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.
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.
per-station readings
dedupe key: (station_id, timestamp)
per-station readings
dedupe key: (station_id, timestamp)
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}.parquetStores, encoders, and sinks compose.
Implement Store<R>,Encoder, orSink<R>. Compose them with .tier() and.encoder().
1use std::{ time:: Duration , env}; 2 3use meathook::{ 4 FlushPolicy , HfSink , JsonEncoder , JsonlStore , Meathook , MemStore , 5 Pipeline , SatayCollector , SinkExt as _, 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 // Format and storage are independent choices. The outer memory 29 // tier batches for five minutes; the durable tier flushes hourly. 30 let sink = HfSink :: new ( client. clone (), "you/your-dataset" , token. clone ()) 31 . encoder ( JsonEncoder ) 32 . tier ( 33 JsonlStore :: new ( "/var/lib/meathook/spool/air_temperature" ), 34 FlushPolicy :: hourly (), 35 ) 36 . tier ( 37 MemStore :: new (), 38 FlushPolicy :: new ( Duration :: from_secs ( 300 ), 10_000 ), 39 ); 40 41 Pipeline :: new ( collector, sink, Duration :: from_secs ( 60 )) 42 . with_key_fn ( |r : & MyRecord | ( r. station_id . clone (), r. timestamp . clone ())) 43 }) 44 . run () 45 . await 46} records stay plain structs: #[derive(Serialize, Deserialize)] is enough for every built-in encoder
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
| feature | default | what it enables |
|---|---|---|
| parquet | ✓ | ParquetEncoder via arrow, parquet, and serde_arrow |
| csv | · | CsvEncoder for flat record types |
| satay | · | SatayCollector for satay-generated API clients |
| huggingface | ✓ | HfSink and CommitAction; implies parquet and satay |
Missing an integration? Request one. Built one? Open a PR.
Start capturing before the API forgets.
docs.rs