How it works
There is no YAML plugin system: you wire collectors and sinks in code by implementing two traits, and sinks compose like tower layers.
tick(interval) ──► Collector::collect() ──► Vec<Record> ──► Sink::ingest(...)
│
the "sink" is a composed stack of layers, e.g.:
│
Tier(MemStore, flush: 5m | 10k records) │ tier 1 (any number of
Tier(JsonlStore, flush: 1h) │ tier 2 tiers, or none)
HfSink(parquet encode+commit) ▼ terminal
Collectorproduces a batch of row-shaped records per tick.SatayCollectoradapts any satay-generated client (e.g. nea-rs) into a collector.Sinkreceives records. Each layer owns its records until itsFlushPolicyfires (interval elapsed / max records / explicitflush()), then pushes downstream. Buffering is one generic layer,Tier, over a pluggableStorebackend — in-memoryMemStore, write-aheadJsonlStore, or bring your own (SQLite, object storage, ...). Fan-out is just another combinator (Tee).Meathookruns one tokio task per pipeline, respawns panicked pipelines from their factory with exponential backoff, and drains every sink stack on SIGTERM/ctrl-c before exiting.
Errors are concrete thiserror enums end to end — traits carry an associated
Error type, nothing is boxed.
Quick start
use Duration;
use ;
use ReqwestActionExt as _;
async
Records are plain structs — #[derive(Serialize, Deserialize)] is all the
parquet encoder needs (the arrow schema is derived from the type via
serde_arrow).
See examples/nea_weather.rs for the full
reference consumer: three NEA pipelines, TOML config, graceful shutdown.
Durability
A Tier backed by JsonlStore is a write-ahead spool: ingest appends
records as JSON lines to an fsynced segment file before returning, and
segments are deleted only after the downstream sink accepted them. Storage
paths are deterministic per window and content
(data/{pipeline}/{YYYY-MM-DD}/{HH}-{MM}-{SS}-{hash}.parquet): a replayed
window re-encodes to the same bytes and overwrites its own file, while
distinct payloads — sub-hourly windows, or one window drained in chunks by
the max_records valve — never collide.
| Failure | What happens | Data lost |
|---|---|---|
| SIGKILL / OOM-kill | leftover segments replayed on next start | ≤ 1 torn record |
| Task panic | supervisor respawns the pipeline from its factory | none |
| Sink outage (HF 5xx) | segments accumulate on disk, retried each firing | none |
| Graceful SIGTERM | runtime drains every sink stack before exit | none |
| Disk lost | spool gone | everything unflushed — use a PVC on k8s |
HuggingFace sink
HfSink commits one file per window via a hand-written, sans-IO
CommitAction implementing satay_runtime::Action (NDJSON commit payload,
base64-inlined file), sent through satay_reqwest — the same transport path
as the collectors. The wire format is pluggable via HfSink::encoder:
ParquetEncoder is the default, JsonEncoder is always available, and
CsvEncoder sits behind the csv feature. The Hive-style partitioning
keeps the HF dataset viewer happy.
Transient failures (transport errors, 429, 5xx) retry a few times in-sink
with backoff; beyond that, an upstream Tier retains the records and
retries at its next firing. Pipelines committing to the same repo should
share a CommitGate (HfSink::gate) so concurrent flushes — e.g. every
pipeline's final window on shutdown — queue client-side instead of piling
into HF's per-repo commit concurrency queue; give gated clients a request
timeout so a stalled upload cannot hold the gate indefinitely.
Feature flags
| Feature | Default | Implies | What it enables |
|---|---|---|---|
parquet |
✓ | — | Encoder trait + ParquetEncoder (arrow + parquet + serde_arrow) |
csv |
— | — | CsvEncoder: CSV window encoding |
satay |
— | — | SatayCollector: any satay-generated API client as a Collector |
huggingface |
✓ | parquet, satay |
HfSink + sans-IO CommitAction |
With --no-default-features you get the core traits, sink combinators,
write-ahead spool, supervised runtime, and the always-on Encoder trait +
JsonEncoder — no HTTP/IO stack pulled in (transitively satay-free) —
bring your own collector and terminal sink.
Configuration
The library API takes typed values (Duration, PathBuf, FlushPolicy)
via builders; config parsing lives in your binary. The example parses a
small TOML file (examples/meathook.toml):
= "/var/lib/meathook/spool" # PVC mount on k8s
[] # FlushPolicy for each pipeline's spool tier
= "1h"
= 50_000
[]
= "zeon256/sg-weather"
= "main"
[]
= "1m"
Secrets come from the environment only: HF_TOKEN (required by the HF
sink), X_API_KEY (optional for NEA).
Running the example
HF_TOKEN=hf_...
Testing
# parquet round-trip, HF payload shape (sans-IO)
HF_TOKEN=hf_... MEATHOOK_TEST_REPO=you/meathook-test \
Security
Please see SECURITY.md for vulnerability reporting.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.