Skip to content

Introduce "inclusive" race as new default semantic #3456

@djspiewak

Description

@djspiewak

Full discussion here: https://discord.com/channels/632277896739946517/839263556754472990/1078054700932419695

The short version is that race, in its current form, represents a resource leak. If there's a tie (i.e. f.cancel *> f.join does not produce Outcome.Canceled()), then it's ambiguous as to what result should be chosen. Right now, we technically bias to the Left, but this is just an implementation detail made manifest. Even worse, if a race is doubly-successful in this way, it's very possible that a resource was acquired which will now just disappear. In most cases, resources which are acquired and then later exposed to an error or self-cancelation will trigger finalizers, but often not in success cases, so this is particularly dangerous.

In a very real sense, race should never have returned Either. The more correct return type would have been Ior.

We can address this in one of two ways. We can either introduce a new variant, like raceInclusive, which returns Ior and then deprecate race. Alternatively, we can play tricks with Dummy implicits and break everyone's sources:

def race[A, B](fa: F[A], fb: F[B])(implicit D: Dummy): F[Ior[A, B]] = ???
private[effect] def race[A, B](fa: F[A], fb: F[B]): F[Either[A, B]] = ???

I'm very tempted to say this is better because it avoids significant problems and cleans up the API (in some sense), but this is definitely a request for comments.

This is all orthogonal to the fiber isolation semantics of race. This affects a few things, but the most significant being self-cancelation semantics: do we treat self-cancelation as "losing the race", or do we treat it as cancelation and embed that result into the parent fiber? The former semantic leads to issues like #3396, while the latter semantic opens up some interesting possibilities (such as a computation "giving up" and allowing the alternative to complete naturally).

racePair is capable of handling all of this generality, and so for any use-cases which are not extremely common, we should push people in that direction, despite the fact that it's very low-level and somewhat hard to work with correctly. We should focus the pretty parts of the API on the most common semantics, which is likely fiber non-isolation (i.e. timeout semantics).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions