Fix issue #4489#4625
Conversation
|
Small update: the explanation I originally wrote for the "regression" is incorrect. I'm still investigating. (This is also the reason this is still a draft PR.) |
|
Update: I now believe the "regression" is not really a regression, but stems from the peculiar behavior of For future reference, this is the test that fails on this branch: "timeout finalizer (#4059, onCancel)" in real {
val test = IO.ref(false).flatMap { ref =>
val program = Resource
.eval(IO.uncancelable { poll =>
ref.set(true) *> poll(IO.sleep(10.millis)).onCancel(ref.set(false))
})
.timeout(10.millis)
.use_
program.attempt.flatMap {
case Left(_) =>
ref.get.ifM(IO.raiseError(new Exception("not released")), IO.unit)
case Right(_) =>
IO.unit // no timeout
}
}
test.parReplicateA_(10000).as(ok)
}This essentially the test here, just with the cases flipped (I think that's a mistake in the original), and uncancelable used tactically. It still fails. |
| def start( | ||
| implicit | ||
| F: Concurrent[F]): Resource[F, Fiber[Resource[F, *], Throwable, A @uncheckedVariance]] = { | ||
| final case class State( |
There was a problem hiding this comment.
This is an unrelated change, but local case classes have a strange encoding with a LazyRef, so I've taken the opportunity to clean it up. (It's moved to the companion object.)
|
I think the test that is only testing Left should be testing both sides, no? |
|
You're right, done in 3b7ddb0. |
This was obviously a can of worms, but I think I've finally fixed #4489. All three issues identified there:
timeout finalizer (start/release race)andracePair finalizer (start/release race)successfully reproduce this issue (on my machine). These tests are based on the reproducers by @armanbilge.racePair finalizer variant (start/release race)here reproduces this issue successfully, the tests hangs (it is based on my own reproducer in the original issue).timeout/timeoutToalways return the outcome of the effect #4059. I've added two tests,propagate successful result from a completed effectandpropagate error from a completed effect. These are variants of the tests originally added by Maketimeout/timeoutToalways return the outcome of the effect #4059, but here specialized toResource. (The approach of Resource runs its finalizers when timed out #4617 seems to make these tests fail/hang.)First 2.:
Resource#startpreviously changed the state in aRefin 2 separate steps (aguarantee, then aflatMap). This leaves an opportunity forfinalizeOuterto run between these 2 steps. The fix is to do the thing atomically (a singlemodifyin aguaranteeCase). This fixes the hanging test specifically added for this issue.Interestingly, the "finalizer running late" issue (1.) doesn't seem to be caused by the unintuitive behavior of
Resource. (I remember believing last year that that was the cause, but I don't remember what convinced me of that.) Investigation now revealed, that the cause is...GenConcurrent#racePair. It previously didn'tjointhe winner fiber; the winner fiber just completed aDeferredin aguaranteeCase. But what could possibly still run after thatguaranteeCase? I'm glad you asked. (Narrator: "nobody asked.") The state-modifying code ofResource#start(mentioned in the previous paragraph). Due toracePairnotjoining the winner, that state-modfying code sometimes runs concurrently with anything afterracePair. The fix is forracePairtojointhe winner. Technically, this change also fixes 2., not just 1. (because thenfinalizeOuterbecomes ordered after the state-modification. But I still think it's good to do the other fix too; atomicity is good. (Obviously, changing a fundamental thing likeracePairis risky, so I'm interested in thoughts about whether this will bite us somewhere else. But intuitively it seems logically correct to me, thatracePairwould join the winner.)An open question: a #4059 regression test, as formulated by @djspiewak here does not pass with this branch. (I've added a variant here,
timeout finalizer (#4059), which does pass.)The reason for that seems completely different: some existing code inI did not dare change that logic, as the wholeResource#startconverts aSucceededto aCanceled.finalizeOnCompleteandconfirmedFinalizeOnCompleteseems very intentional. What I tried to do here was to preserve the existing logic, and make it tighter wrt cancellability and atomicity. (This is arguably a regression in this branch. Although, arguably, the)onCancelis in the "wrong place" in that test.Besides these fixes, there is another one: an additional
.uncancelableinResource#start. I was not able to actually write a test that definitively shows that is necessary, but it seems to me that it could be a problem, so I've added it.Note: this is against
series/3.6.x, as #4489 was originally reported for that. I'm not sure if we still care about 3.6.x. If not, I can retarget this for 3.7.x. If yes, we should nevertheless merge this forward, and do a 3.7.1. (After proper review, of course.)