Skip to content

refactor(core): split sans-IO traits from Tokio runtime #13

Description

@zeon256

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions