Problem
The current public core traits are Tokio-spawn shaped rather than runtime-neutral:
Collector, Sink, and Store require Send at the trait level.
- Their returned futures also require
+ Send.
- Associated errors require
Send + Sync + 'static.
- Records and dedupe keys are pushed toward
Send + 'static by the core/runtime boundary.
Those constraints are not intrinsic to collection, buffering, window storage, replay, or sink composition. They are requirements of the current Tokio supervisor, which spawns each pipeline onto a multi-thread Tokio runtime.
This makes the core hard to use in thread-per-core/local runtimes such as Glommio, where !Send futures and shard-local state are normal and desirable.
There is also Tokio coupling outside runtime.rs:
Pipeline::run owns the scheduling loop and hardcodes tokio::time, tokio::select!, and tokio_util::sync::CancellationToken.
runtime.rs hardcodes Tokio spawning, signals, timers, and JoinSet.
JsonlStore currently performs blocking std::fs work inside async trait methods; fixing that with tokio::fs or tokio::task::spawn_blocking would make that backend Tokio-specific unless it is split from the runtime-neutral core.
- Cargo dependencies/features are not currently structured around a truly runtime-free core.
Proposed design
Split the crate into a runtime-neutral core plus runtime-specific adapters/features.
Core
Core should contain the dataflow and storage abstractions without executor policy:
Collector
Sink
Store
Segment
WindowMeta
FlushPolicy
Tier
Tee
MemStore
- pipeline state-machine operations such as
tick(...) and flush(...)
Core traits should not require Send unless a specific operation semantically needs it. In particular, local !Send implementations should be valid.
Pipeline should not own Tokio timers, cancellation, or spawning. Runtime adapters should drive the pipeline loop.
Tokio runtime adapter
A Tokio feature/module/crate should contain:
- the
Meathook supervisor
- Tokio signal handling
- Tokio timers
- cancellation token integration
tokio::spawn / JoinSet usage
- Tokio-specific
Send + 'static bounds at the runtime entrypoint
- optionally a
TokioJsonlStore using tokio::fs or spawn_blocking
This is where Send belongs, because Tokio's multi-thread spawning requires it.
Local / thread-per-core runtimes
The core split should allow a Glommio-style runtime adapter to drive the same pipeline logic using local tasks without Send bounds.
This enables shard-local implementations using types such as Rc, RefCell, local executor resources, and runtime-native file/network handles.
Store backends
Avoid treating one JSONL backend as universally async-runtime-correct.
Possible split:
BlockingJsonlStore: uses std::fs; explicitly documented as blocking.
TokioJsonlStore: uses Tokio offloading (tokio::fs or spawn_blocking).
- future
GlommioJsonlStore: uses Glommio-native file APIs / shard-local IO.
The core Store trait should permit each backend; the backend implementation should own its runtime-specific IO strategy.
Trait design options
There are two viable stable approaches, with different tradeoffs.
Option A: associated future GATs
Use associated future types so runtime adapters can add Send bounds only where needed:
pub trait Sink<R> {
type Error: std::error::Error + 'static;
type Ingest<'a>: std::future::Future<Output = Result<(), Self::Error>> + 'a
where
Self: 'a,
R: 'a;
type Flush<'a>: std::future::Future<Output = Result<(), Self::Error>> + 'a
where
Self: 'a;
fn ingest<'a>(
&'a mut self,
meta: &'a WindowMeta,
records: Vec<R>,
) -> Self::Ingest<'a>;
fn flush(&mut self) -> Self::Flush<'_>;
}
Then the Tokio adapter can require:
for<'a> S::Ingest<'a>: Send,
for<'a> S::Flush<'a>: Send,
Tradeoff: on stable Rust, implementations usually need boxed futures unless they write manual future types, because impl Trait in associated types is not stable for the crate's current Rust target.
Option B: keep RPITIT and split local/send trait families
Keep zero-cost impl Future in trait methods, but separate local runtime-neutral traits from Tokio-spawnable traits.
Tradeoff: lower per-call overhead, but duplicated APIs/adapters.
Future Rust direction
Return type notation and/or stable impl Trait in associated types would make the ideal design easier: core traits could use RPITIT without Send, and Tokio adapters could bound returned futures as Send at the spawn boundary.
Benefits
- Makes the core genuinely runtime-neutral / Sans-IO-oriented.
- Allows Glommio and other thread-per-core runtimes to use local
!Send collectors, sinks, stores, and futures.
- Moves
Send/Sync constraints to the actual Tokio spawn boundary.
- Makes Tokio an optional runtime integration instead of a core assumption.
- Clarifies where timers, cancellation, signals, spawning, and runtime-specific IO belong.
- Prevents blocking filesystem fixes in
JsonlStore from accidentally making the core Tokio-specific.
- Creates a path for runtime-native store backends instead of one-size-fits-all async file IO.
- Aligns the crate's architecture with the stated goal of composable core abstractions.
Acceptance criteria
- Core traits compile without Tokio-specific bounds or types.
Pipeline core logic can be driven without Tokio timers or cancellation tokens.
- Tokio runtime integration still supports existing
Meathook behavior with explicit Send bounds.
- Non-
Send local implementations are possible outside the Tokio adapter.
- JSONL store strategy is explicit: blocking, Tokio-offloaded, or runtime-native.
- Cargo features/dependencies reflect the split so users can depend on the core without pulling Tokio runtime integration.
Problem
The current public core traits are Tokio-spawn shaped rather than runtime-neutral:
Collector,Sink, andStorerequireSendat the trait level.+ Send.Send + Sync + 'static.Send + 'staticby the core/runtime boundary.Those constraints are not intrinsic to collection, buffering, window storage, replay, or sink composition. They are requirements of the current Tokio supervisor, which spawns each pipeline onto a multi-thread Tokio runtime.
This makes the core hard to use in thread-per-core/local runtimes such as Glommio, where
!Sendfutures and shard-local state are normal and desirable.There is also Tokio coupling outside
runtime.rs:Pipeline::runowns the scheduling loop and hardcodestokio::time,tokio::select!, andtokio_util::sync::CancellationToken.runtime.rshardcodes Tokio spawning, signals, timers, andJoinSet.JsonlStorecurrently performs blockingstd::fswork inside async trait methods; fixing that withtokio::fsortokio::task::spawn_blockingwould make that backend Tokio-specific unless it is split from the runtime-neutral core.Proposed design
Split the crate into a runtime-neutral core plus runtime-specific adapters/features.
Core
Core should contain the dataflow and storage abstractions without executor policy:
CollectorSinkStoreSegmentWindowMetaFlushPolicyTierTeeMemStoretick(...)andflush(...)Core traits should not require
Sendunless a specific operation semantically needs it. In particular, local!Sendimplementations should be valid.Pipelineshould not own Tokio timers, cancellation, or spawning. Runtime adapters should drive the pipeline loop.Tokio runtime adapter
A Tokio feature/module/crate should contain:
Meathooksupervisortokio::spawn/JoinSetusageSend + 'staticbounds at the runtime entrypointTokioJsonlStoreusingtokio::fsorspawn_blockingThis is where
Sendbelongs, because Tokio's multi-thread spawning requires it.Local / thread-per-core runtimes
The core split should allow a Glommio-style runtime adapter to drive the same pipeline logic using local tasks without
Sendbounds.This enables shard-local implementations using types such as
Rc,RefCell, local executor resources, and runtime-native file/network handles.Store backends
Avoid treating one JSONL backend as universally async-runtime-correct.
Possible split:
BlockingJsonlStore: usesstd::fs; explicitly documented as blocking.TokioJsonlStore: uses Tokio offloading (tokio::fsorspawn_blocking).GlommioJsonlStore: uses Glommio-native file APIs / shard-local IO.The core
Storetrait should permit each backend; the backend implementation should own its runtime-specific IO strategy.Trait design options
There are two viable stable approaches, with different tradeoffs.
Option A: associated future GATs
Use associated future types so runtime adapters can add
Sendbounds only where needed:Then the Tokio adapter can require:
Tradeoff: on stable Rust, implementations usually need boxed futures unless they write manual future types, because
impl Traitin associated types is not stable for the crate's current Rust target.Option B: keep RPITIT and split local/send trait families
Keep zero-cost
impl Futurein trait methods, but separate local runtime-neutral traits from Tokio-spawnable traits.Tradeoff: lower per-call overhead, but duplicated APIs/adapters.
Future Rust direction
Return type notation and/or stable
impl Traitin associated types would make the ideal design easier: core traits could use RPITIT withoutSend, and Tokio adapters could bound returned futures asSendat the spawn boundary.Benefits
!Sendcollectors, sinks, stores, and futures.Send/Syncconstraints to the actual Tokio spawn boundary.JsonlStorefrom accidentally making the core Tokio-specific.Acceptance criteria
Pipelinecore logic can be driven without Tokio timers or cancellation tokens.Meathookbehavior with explicitSendbounds.Sendlocal implementations are possible outside the Tokio adapter.