[ANN] fuseau 0.1

Dear all,

It brings me a smile to announce that fuseau 0.1 is in the process of being released on opam. Fuseau (“fuh-zo”, french for spindle) is yet another lightweight fiber library for OCaml 5.

This is early days, as emphasized in the 0.1 version number. If you give it a try, expect some bugs, rough edges in the API, and changes in the future. It also doesn’t have great docs.

rationale and overview

With that out of the way, I’d like to give a rationale for this new library. The primary goal of Fuseau is to be simple while providing a modern, nice, comfortable API that relies on OCaml 5’s effects to avoid monadic hell. As such, fibers, the lightweight threads that are cooperatively scheduled by Fuseau, can natively use all usual control structures (loops, exceptions with proper backtraces, etc.).

Of all the libraries in this crop of new OCaml 5-based schedulers, Fuseau is probably the least ambitious. In particular, it does not:

  • attempt to use capabilities for more secure programming
  • use domains in any way (it’s purely single threaded)
  • have a notion of quota or fairness
  • insist on having its own IO event loop.

What Fuseau does is provide fibers, cooperative channels, structured concurrency, sleep, cancellation[1], fiber local storage, and the ability for the scheduler to run alongside an event loop.

The most compelling use for Fuseau right now is via fuseau-lwt, which couples Fuseau’s fiber scheduler with Lwt_engine’s event loop (which can leverage libev). Using fuseau-lwt, it’s fairly easy to use Lwt libraries and await them from a Fuseau fiber (in fact, the biggest example written in Fuseau so far uses ezcurl-lwt or cohttp-lwt-unix to crawl a web site with multiple concurrent connections, and uses moonpool to parse the web pages in parallel in the background.

structured concurrency

Fuseau’s fibers (type: 'a Fiber.t for a fiber returning 'a) form a cancellation tree. When a fiber spawns a child fiber, it has a few ways of doing that but the main one is:

val spawn :
  ?name:string ->
  ?propagate_cancel_to_parent:bool ->
  (unit -> 'a) -> 'a Fiber.t

which creates a new fiber, registers it as a child of the current fiber, and resumes the current fiber. The idea here is that if the parent is cancelled, the child is always cancelled; if propagate_cancel_to_parent is true, then the child failing also cancels the parent.

Whatever happens, a fiber always resolves after all its children have returned. What this means is that, when a fiber’s main function returns or raises, Fuseau doesn’t immediately resolve the fiber; instead it automatically waits for all alive children. If the function fails, the children are cancelled before the wait.

Only when all children have resolved will the parent fiber’s result be properly set. If the fiber’s function was a success, but a child (with ~propagate_cancel_to_parent:true) fails, then the result switches from success to failure.

This means Fuseau doesn’t have switches or graveyards or nurseries. It only has fibers, and the currently active fiber is the nursery for all newly spawn sub-fibers[2].

cancellation and racing multiple operations

Currently there is no primitive in Fuseau to take multiple fibers and wait for the first one to finish, cancelling the others. This is common in Lwt and I don’t like it, because it means we might end up in situations where both fibers actually completed, and end up losing data in the fiber that lost the race.

Instead Fuseau has a select primitive and a notion of atomic event:

module F = Fuseau

F.select [
  When (F.Chan.ev_receive c1, fun x -> …);
  When (F.Chan.ev_send c2 y, ignore);
  When (F.Sleep.ev_timeout 5., fun _ -> failwith "timeout");
]

This mechanism decouples polling each atomic event in turn, checking if it’s able to fire right now. If, in When (ev, f), the event ev fires when polled and returns x, this corresponding branch is taken, and the select tail-calls into f x. For example if F.Chan.ev_receive c1 fires (receiving an item from a channel), then the callback is passed the received value.

If no branch succeeds, then the fiber suspends and registers to each event’s wait function. This means each event is passed a wakeup callback and registers it somewhere (socket readiness, future completion, channel receiver list, etc.).

This mechanism (on which the paint is very fresh!) is extensible (so you can make your own events) and has, imho, cleaner semantics than complex race-and-cancel-the-loser combinators.

domains

As said above, Fuseau doesn’t schedule on multiple cores. Part of it is for simplicity reasons (it makes the scheduler lean and simple); part of it is to facilite interoperability with Lwt.

However, some of the types in Fuseau are thread-safe. In particular:

  • it’s possible to schedule a fiber from another thread (spawn_from_anywhere)
  • many functions to access fibers’ state, cancel fibers, or callbacks used to resume suspender fibers, are thread-safe. This means it’s not that hard to implement a await function on some foreign notion of future.

For CPU bound tasks, there is an optional library fuseau.moonpool that has Fuseau_moonpool.await_fut : 'a Moonpool.Fut.t -> 'a. This can be run from Fuseau to wait for a moonpool computation to be done.

With additional helpers, it’s fairly easy to start a background computation from fuseau, and maybe suspend the current fiber until it’s done:

val spawn : on:Moonpool.Runner.t -> (unit -> 'a) ->
  'a Moonpool.Fut.t
(** An alias to {!Moonpool.Fut.spawn} *)

val spawn_and_await : on:Moonpool.Runner.t -> (unit -> 'a) -> 'a

closing thoughts

The design space for concurrency libraries in OCaml 5 is wide, and I find it interesting that already a lot of alternatives have emerged, each with its own focus. A niche that Fuseau could fill is existing projects that have a lot of Lwt code, and want to migrate it progressively to a (imho) nicer style of concurrency. Another niche is for people or teams that want a reasonably simple fiber library to combine with their event loop (e.g. luv or libev or maybe event a graphical event loop).


  1. cancellation is where most bugs and rough edges are likely to live, unsurprisingly. ↩︎

  2. there are alternatives to spawn that start a fiber under a different parent, or under the root fiber, basically similar to Lwt.async, when it’s needed. ↩︎

28 Likes