-
Notifications
You must be signed in to change notification settings - Fork 529
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
Handle self-cancellation when running fibers to avoid deadlocks #1484
Handle self-cancellation when running fibers to avoid deadlocks #1484
Conversation
Wow, I didn't realize this. This is a rather significant oversight in the test harnessing. Working back to your earlier points…
That's definitely not the intended semantic anymore. There was a time where it was what we were shooting for. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really really nice.
seems like it should be expecting F.never instead of F.canceled.
You're correct! Documentation and implementation agree:
case Outcome.Canceled() =>
poll(f.join).onCancel(f.cancel).flatMap {
I believe that the laws should be corrected.
@@ -26,11 +26,13 @@ import scala.util.{Left, Right} | |||
trait AsyncLaws[F[_]] extends GenTemporalLaws[F, Throwable] with SyncLaws[F] { | |||
implicit val F: Async[F] | |||
|
|||
def asyncRightIsSequencedPure[A](a: A, fu: F[Unit]) = | |||
F.async[A](k => F.delay(k(Right(a))) >> fu.as(None)) <-> (fu >> F.pure(a)) | |||
def asyncRightIsUncancelableSequencedPure[A](a: A, fu: F[Unit]) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My rationale for the uncancelable
is that fu
is sequenced when async
is evaluated, but its cancellation status doesn't affect the outcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think it's cancelation status still affects the outcome. fu = F.canceled
would still imply F.canceled
on both sides. Does this actually produce a different test outcome?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, good call, looks like I misinterpreted the failures before. Running the original laws again shows they fail when one side doesn't terminate and the other ends up as canceled
. I'll have to investigate that more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking into this a bit more, I think this change is actually correct if we want to keep the current semantic that the effect returned from the async callback
(Either[Throwable, A] => Unit) => F[Option[F[Unit]]]
is uncancelable (e.g. consider fu = F.canceled >> F.never
).
I think my use of the term "outcome" in the OP caused confusion. fu
does affect the resulting Outcome
, but not the "outcome" in the sense that cancellation is suppressed.
def uncancelableRaceDisplacesCanceled = | ||
F.uncancelable(_ => F.race(F.never[Unit], F.canceled)).void <-> F.canceled | ||
|
||
def uncancelableRacePollCanceledIdentityLeft[A](fa: F[A]) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These no longer seemed pertinent given the current desired semantics of race
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
@djspiewak Ready for another look. I've corrected or removed (justification in comments above) some of the laws that began failing due to distinguishing between non-termination and failure or to the presence of previously ungenerated constructs. There are other failing laws/tests that I've commented out with a The remaining offenders are
the |
Sorry for the delay on getting to this! It's been a hell of a week… Churning through it now. Also thank you for taking this on! |
@@ -26,11 +26,13 @@ import scala.util.{Left, Right} | |||
trait AsyncLaws[F[_]] extends GenTemporalLaws[F, Throwable] with SyncLaws[F] { | |||
implicit val F: Async[F] | |||
|
|||
def asyncRightIsSequencedPure[A](a: A, fu: F[Unit]) = | |||
F.async[A](k => F.delay(k(Right(a))) >> fu.as(None)) <-> (fu >> F.pure(a)) | |||
def asyncRightIsUncancelableSequencedPure[A](a: A, fu: F[Unit]) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think it's cancelation status still affects the outcome. fu = F.canceled
would still imply F.canceled
on both sides. Does this actually produce a different test outcome?
def raceNeverIdentityLeft[A](fa: F[A]) = | ||
F.race(F.never[Unit], fa) <-> fa.map(_.asRight[Unit]) | ||
|
||
def raceNeverIdentityRight[A](fa: F[A]) = | ||
F.race(fa, F.never[Unit]) <-> fa.map(_.asLeft[Unit]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These actually seem like pretty important laws. I take it that they fall apart when fa = F.canceled
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, exactly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can fix this by using .onCancel(F.never)
again. That's not quite an identity, and it loses some generality, but it does allow us to talk about task completion and race
once again.
def uncancelableRaceDisplacesCanceled = | ||
F.uncancelable(_ => F.race(F.never[Unit], F.canceled)).void <-> F.canceled | ||
|
||
def uncancelableRacePollCanceledIdentityLeft[A](fa: F[A]) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
F.uncancelable(p => F.race(fa, p(F.canceled))) <-> F.uncancelable(_ => | ||
fa.map(_.asLeft[Unit])) | ||
def uncancelableRaceNotInherited = | ||
F.uncancelable(_ => F.race(F.never[Unit], F.canceled)).void <-> F.never[Unit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather be a bit more general here:
F.uncancelable(_ => F.race(fa, fb)) <-> F.race(fa, fb).onCancel(F.never)
project/plugins.sbt
Outdated
@@ -1,6 +1,6 @@ | |||
libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.0" | |||
|
|||
addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.18.3") | |||
addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.19.2") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Not having scala steward on branches is pretty painful.
@wemrysi I think we're going to merge this aggressively (since it reveals a bunch of bugs which are currently invisible) and follow-up with further law refinements and fixes. Gucci? |
@djspiewak I was planning to fix the two that you called out in review later today, want to wait until those are done? |
@wemrysi I'm mostly thinking of the fact that we want to get another milestone out with support for 3.0.0-M3, and I'd rather fold your changes into it. Since the follow-ups are bug fixes (#1519) and law adjustments, it feels like we can release the milestone without them, but if you feel it's worth a bit of delay I'm cool with it unless @mpilquist rains fire on us. |
@djspiewak Ok, that's fine. I can address your comments in a followup PR. |
As discussed in #1455, we'd like to handle self-cancellation when running fibers to avoid deadlocks. This introduces
IO#unsafeRunAsyncOutcome
to explicitly expose the case where the running fiber the caller started was canceled. The existingunsafeRun*
variants now raise aCancellationException
if they are canceled, ensuring the callback is always invoked on termination.A result of this change is that cancellation and non-termination are now distinguished in tests. This distinction resulted in a failure of
GenSpawnLaws#uncancelableRaceDisplacesCanceled
which, upon inspection, seems like it should be expectingF.never
instead ofF.canceled
.Looking at the other
uncancelableRace*
laws, they seem to imply the mask status of a fiber is inherited by fibers it spawns, is that the intended semantic? It doesn't appear to be the current semantic ofIO
, as illustrated byGenerally, any time
fa
is canceled, the lhs of those laws will not terminate (by definition ofGenSpawn#race
) whereas the rhs will terminate as canceled (and prior to this PR, the tests considered those outcomes equal). Even if we take that into account viaunsafeRunTimed
, the laws stay thatD <-> B
when actuallyD <-> A
.Thoughts? Have I overlooked something or made poor assumptions?