ocaml-multicore / eio

Effects-based direct-style IO for multicore OCaml
Other
548 stars 66 forks source link

Feature request: an equivalent to `Lwt.npick` #558

Closed SGrondin closed 8 months ago

SGrondin commented 1 year ago

Motivation

Under Fiber.first:

Warning: it is always possible that both operations will succeed (and one result will be thrown away). This is because there is a period of time after the first operation succeeds, but before its fiber finishes, during which the other operation may also succeed.

This enables Fiber.first to have a nice, convenient API. 98% of the time, I do not care about the edge case, so the benefits of the API are worth the downside explained in the warning.

But 2% of the time, I do care about the return value of the second fiber that may also have succeeded.

Comparison: Lwt

Lwt.pick

Lwt.pick : ('a Lwt.t) list -> 'a Lwt.t

This function is roughly equivalent to Fiber.any.

Lwt.pick ps returns a promise that is pending until one promise in the list ps becomes resolved. When at least one promise in ps is resolved, Lwt.pick tries to cancel all other promises that are still pending, using Lwt.cancel.

Lwt.npick

Lwt.npick : ('a Lwt.t) list -> ('a list) Lwt.t

Lwt.npick ps is similar to Lwt.pick ps, the difference being that when multiple promises in ps are fulfilled simultaneously (and none are rejected), the result promise is fulfilled with the list of values the promises were fulfilled with. When at least one promise is rejected, Lwt.npick still rejects the result promise with the same exception.

Draft idea

We're optimizing for the 2% case here. Something that behaves just like Fiber.first, but without losing the second successful result (when it succeeds """at the same time""").

Fiber.n_first : (unit -> 'a) -> (unit -> 'a) -> [ `Left of 'a | `Right of 'a | `Both of 'a * 'a ]
Fiber.n_any : (unit -> 'a) list -> 'a list

The way Lwt accomplishes this is by scanning the list for other successful promises before resolving the returned promise.

I had a quick look at the code and it seems doable.

Is this a feature the maintainers would welcome?

patricoferris commented 1 year ago

Hi @SGrondin, thanks for the detailed issue.

Something like this sounds useful to me, as you said, for that smaller amount of the time you might want to do something different in the case that both fibers succeed simultaneously. Without extending fibres to support this, I can't think of a solution that doesn't involve lifting into Eio promises so it seems like a good idea.

It also reminds me a bit of nchoose_split which I needed for a port of vpnkit although that's at the promise-level (in the Eio sense), but that's orthogonal to having this in the Fiber API. Just adding that as the issue jogged my memory.

talex5 commented 1 year ago

Yes, something like this sounds useful. I'd been wondering about having an optional combine argument for this case. e.g. something like:

  val first : ?combine:('a -> 'a -> 'a) -> (unit -> 'a) -> (unit -> 'a) -> 'a

So if we get an additional value while waiting for things to cancel, it does combine acc x to merge it in. The current behaviour would be equivalent to ~combine:fst.

Having a separate function and returning a list might be simpler, though.

What are you planning to use it for?

SGrondin commented 1 year ago

These are a few real world Eio examples where I've used Lwt.npick in the past:

I like your idea! ~combine is more flexible than my Fiber.n_first suggestion, but I think it's probably even more important to add this feature to Fiber.any (where I think a separate function like Fiber.n_any might make more sense than a ~combine reducer).

SGrondin commented 8 months ago

Addressed by #587