Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce "inclusive" race as new default semantic #3456

Open
djspiewak opened this issue Feb 22, 2023 · 7 comments
Open

Introduce "inclusive" race as new default semantic #3456

djspiewak opened this issue Feb 22, 2023 · 7 comments

Comments

@djspiewak
Copy link
Member

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

@Jasper-M
Copy link
Contributor

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

Aren't both of those things related to the former semantic?

@armanbilge
Copy link
Member

do we treat self-cancelation as "losing the race"

In this case, race(canceled, sleep(1.second)) ends after 1 second with rhs as the winner. The lhs "lost" the race. This is the former semantic.

do we treat it as cancelation and embed that result into the parent fiber

In this case, race(canceled, sleep(1.second)) ends in cancelation effectively immediately. The lhs "won" the race. This is the new semantic.

Does that answer your question?

@Jasper-M
Copy link
Contributor

Sorry I didn't phrase my question very well. I was mainly confused about this part:

the latter semantic opens up some interesting possibilities (such as a computation "giving up" and allowing the alternative to complete naturally)

Isn't this an example of a possibility that the former semantic gives you, and not the latter?

@armanbilge
Copy link
Member

Oh, gotcha, I think you are right! Daniel can clarify maybe.

@GreyPlane
Copy link

uh, I just found this issue after I implemented my own sketchy version while my original goal was to achieve a similar short-circuit evaluation semantic of boolean for IO racing but the general concern about the semantic seems very valid, nice to see this API came into cats-effect.

@djx314

This comment was marked as off-topic.

@djx314

This comment was marked as off-topic.

@armanbilge armanbilge modified the milestones: v3.6.0, v3.7.0 Nov 15, 2023
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 19, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 19, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 21, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 21, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 24, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 24, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
biochimia added a commit to biochimia/cats-effect that referenced this issue Apr 29, 2024
`timeout*` methods are implemented in terms of a race between a desired
effect and the timeout. In the case that both effects complete
simultaneously, it could happen that the timeout would win the race, a
`TimeoutException` be raised, and the outcome of the desired effect
lost.

As is noted in typelevel#3456, this is a general problem with the `race*`
methods, and can't be addressed in the general case without breaking the
current interfaces.

This change is a more narrow take on the problem specifically focusing
on the `timeout` and `timeoutTo` methods. As these methods inherently
wait for both racing effects to complete, the implementation is changed
to always take into account the outcome of the desired effect, only
raising a `TimeoutException` if the timeout won the race *and* the
desired effect was effectively canceled. Similarly, errors from the
desired effect are preferentially propagated over the generic
`TimeoutException`.

The `timeoutAndForget` methods are left unchanged, as they explicitly
avoid waiting for the losing effect to finish.

This change allows for `timeout` and `timeoutTo` methods to be safely
used on effects that acquire resources, such as `Semaphore.acquire`,
ensuring that successful outcomes are always propagated back to the
user.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants