Idiomatic Futures/tokio usage

I’ve been trying to get my feet wet with futures in rust, and followed various online tutorials and examples. Starting from a pretty typical “chat server” example, I ended up with code like this.

Most of the examples make sense as they are structured and they are illustrating a basic skeleton, but as I try to move code out into libraries/out of a single main function, I wonder if I’m using the proper abstractions at the boundaries. This code is still a toy example, but I am trying to understand how to evolve simple implementations into bigger projects. I have looked at the tokio chat server, as well as other similar examples.

The particular set up here is a chat server where clients connect via websockets. I then introduced concepts such as:

  • Hub which is effectively a registry of connected Clients, although includes things such as an internal Interval Stream which sends heart beats, etc.
  • Client which represent each websocket connection.

Within the hub module (what one might pull out to a library if this were more than a toy example), I wanted to avoid directly relying on tokio, so code that originally might have directly called tokio::spawn I wanted to instead have return Future or Stream objects, so the main fn could handle dispatching them onto the tokio runtime instead.

But code like here feels somewhat forced, and took some poking and prodding to land on. Effectively I am mapping an incoming Stream into a Stream of Futures, and passing that Stream back out to main, where it can be spawned.

In another case, for this Drop impl, to avoid a spawn I am instead doing wait().

Ultimately, I wonder if I’m following intended usage for Futures. Or am I shoehorning in the wrong patterns/concepts? For example, the “consume Stream, produce Stream of Futures” pattern was somewhat motivated in my mind as analogous to iterating over a collection and flat_mapping.

  • Is returning impl Stream and impl Future the appropriate abstraction at a library boundary (where I don’t want to e.g. tokio::spawn internally to the library)?
  • Is it ok to simply wait() in cases such as a Drop impl?
  • Would a more ergonomic approach be to implement Stream or Future for types such as Client or Hub?

I don’t know if I should be comfortable using spawn more liberally/this sort of design ends up placing too much work within a task and reducing the potential of using Futures. The hub_loop definition in particular I am concerned about; by trying to pipeline both the consumer and producer aspects, am I introducing unnecessary serialization or something similar?

My best advice is to write tests. In doing so you will be tightly enforcing what is correct. Then you can experiment and see if some refactoring gives a boost and throw it away if it causes breakage.

An alternative to returning the futures is to take an instance of futures::future::Executor, you can then pass in an instance of tokio::executor::DefaultExecutor when you’re running on Tokio. This is how other libraries like hyper allow configuring where their per-connection tasks run.

Is returning impl Stream and impl Future the appropriate abstraction at a library boundary (where I don’t want to e.g. tokio::spawn internally to the library)?

Yes.

Is it ok to simply wait() in cases such as a Drop impl?

No. Spawn a new future using a handle, or send a message to an existing future. Keep in mind that if your newly spawned future is dropped too, someone probably ran shutdown_now on your runtime, in which case you don’t want to keep spawning new futures.

Prefer to use handles to the executor rather than tokio::spawn.

Thanks for the feedback here; I think this is exactly the approach that I was missing.

By this you mean utilize something like std::sync::mpsc::channel, in order to avoid spawning a future or wait()ing?

The problem is that if drop is run inside a future, you’re running blocking code in a future. Blocking code inside a future is very unhealthy for the performance of your application.

Regarding the channel, you should probably use the one in the futures crate, since sending something to the channel will wake the future.

If possible, it’s best to encourage the user to do cleanup explicitly, because blocking cleanup from Drop is often not easily doable in a good way.

Of course you could try to do the cleanup immediately using this feature, but it can fail, so you need a fallback (or accept that sometimes you won’t do cleanup properly). Also be aware that if you try this, but drop was not run inside a future, it will panic, and since it’s inside a drop, that would abort the program. So with this method, you should probably check if you’re inside a future (note that if you’re not, blocking is fine).