typelevel / typelevel/cats-effect

Introduce "inclusive" `race` as new default semantic

Open
#3,456 7 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

:bike: :house: :exclamation: source breaking :exclamation:
Dominant language
Scala
Stars
2.2k
Forks
576
Avg merge
2d 11h
Merged PRs (30d)
18

Description

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).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked Discord discussion and issue #3396, then review the existing race and racePair APIs. Compare the proposed raceInclusive/Ior approach with the breaking-default alternative, including self-cancellation and fiber-isolation semantics. Done requires a decided API direction and maintainer-approved scope; no files or tests are named.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.