Rethinking RouteSink

Published on: 2025-09-07 19:45

Written by Alisa Feistel

async

data-structures

project:ruchei

series:streams-and-sinks

This is the first in the series of blog entries on Streams and Sinks. In this one, I describe what ruchei project is currently being up to without going into too much detail (wait for other entries which will have that).

The What

ruchei_route::RouteSink

This basically just a Sink but you need to specify a routing key in addition to the message. You wait until ready to receive a message with a certain key, send that message, then flush it (on that specific key), or close all routes at once.

The Why

We want a universal interface for “collection of connections” and “ZeroMQ-style router”.

Why is it not an extension of Sink (yet)

But, yes, it is kind of an extension?

There is a blanket impl. With time, I’ve learned that this gets in the way more than provides any sort of convenience (especially for other blanket impls).

So, what changed?

LinkedSlab made thinking of complex wake patterns doable.

See ruchei::deal::slab and ruchei::multicast::bufferless_slab for examples of its use (note that multicast one has bugs, which were fixed since then, and were relatively easy to fix).

What is Slab

First, we need to talk about Slab. I believe it to be one of the most powerful data structures in the Rust ecosystem. It effectively gives you a pointer-like access without the associated memory unsafeties. You can implement all sorts of data structures on top of that.

And it seems to perform well enough?

I’d say that but I don’t have concrete enough benchmarks for that claim, so you can ignore it.

What can I do with a Slab?

Things you’d expect from an allocator, plus a bit more.

Why is Slab

Not only do you get a memory safe (compared to pointers) implementation for structures you make, but you also can expose Slab-like interface to the outside (you’ll see that in LinkedSlab) either by giving your users usize directly or some sort of a handle.

What is LinkedSlab

LinkedSlab<T, N>, in addition to base Slab<T> interface gives you N doubly-linked lists with nodes tied to items in the slab. If you remove an item, its node is automatically removed from all the lists. This creates an insertion-sorted subset overlay on the items of the slab.

Why is LinkedSlab

When working on ruchei, we have a lot of combinators which are collections of streams that:

LinkedSlab solves all those.

What Next

With better tools to implement multi-stream combinators, we believe that it should be reasonable to assume all RouteSinks to come with a valid poll_ready_any and poll_flush_all implementation. As such, we can just treat poll_ready_route and poll_flush_route as optimisation-only tools.

RouteSink<Route, Msg>: Sink<(Route, Msg)> seems like a reasonable way forward.

Another addition that I want to see is DealSink<Route, Msg>: RouteSink<Route, Msg>, which also provides poll_ready_some to get the next Route that we can start_send to.

Is LinkedSlab the best we can do?

I think not? parrrate/ruchei#13

Having actual separate nodes and pointers gives you the following: