Fix #4627 resource early cancelation - #4630
Conversation
7fa5573 to
8da0407
Compare
|
I now realize my suggested "obvious fix" is completely incorrect. But I don't think this one's correct either. For example with this branch, this test fails: real("eval - uncancelable continuation") {
def res(d: Deferred[IO, Unit]) = Resource.make(IO.pure(42))(_ => IO.unit).flatMap { _ =>
Resource.eval(IO.uncancelable { _ => d.complete(()).as(99) })
}
val t = for {
d <- IO.deferred[Unit]
ctr <- IO.ref(0)
fib <- IO.uncancelable { poll =>
poll(res(d).allocatedCase).flatMap { _ =>
ctr.update(_ + 1)
}
}.start
_ <- d.get
_ <- fib.cancel
c <- ctr.get
_ <- IO { assertEquals(c, 1) }
} yield ()
t.replicateA_(1000)
}Unless I'm missing something, this is just the original problem with some extra steps, so it probably shouldn't fail |
|
I was suspicious of the I golfed your failing test down to a deterministic one that fails consistently, then narrowed the uses of poll more. I had to do an odd thing ( tickedProperty("combineK - behave like orElse when underlying effect does") {
implicit ticker =>
forAll { (r1: Resource[IO, Int], r2: Resource[IO, Int]) =>
val lhs = r1.orElse(r2)
val rhs = r1 <+> r2
assertEqv(lhs, rhs)
}
}It does not consistently fail, but fails with this seed at least: override def scalaCheckInitialSeed = "EpTk-jCEjNXrCuelFnCg7QRFmJK5gqhF6DEW9EUaFtF="
|
|
Poking at this a bit more, in Resource, Should Poking @armanbilge in case you have any insight, since you were modifying the propagation behavior a few years ago to propagate the same exit case to both sides for |
|
Hmm, |
|
My guess is the masking asymmetry rather than the finalizers. - poll(continue(head(b), tail, rel2))
+ (poll(F.unit) >> continue(head(b), tail, rel2))That turns the continuation from cancelable into masked, with a single cancelation point in front of it. So the two sides now open their cancelation windows in different places, and in a ticked test that's enough to make them disagree. Quick way to confirm: put that one line back to |
|
Ran it, my guess was wrong. Putting poll(continue(...)) back doesn't help, the property still falsifies after 13 tests. It's the fold hunk. Revert just that, the uncancelable wrapper around the interpreter, and keep the allocatedCase changes, and everything goes green: ResourceSuite 216/216 with the test and your seed restored, testsJVM/test 3018/3018. Both new eval tests still pass without it, so that hunk doesn't look necessary for #4627. JVM only, I didn't try JS or Native. |
So, I didn't actually add a test to exercise |
|
I digged a bit more, whether the test is valid, the Eq the property uses says it itself:
It's E.eqv(x.use(F.pure), y.use(F.pure)), so the comparison is the use outcome and nothing else. The exit case passed to a finalizer never reaches it, which means the combineK vs handleErrorWith difference can't be what fails here. What the Eq does see is Succeeded/Errored/Canceled, and wrapping fold in uncancelable changes exactly that? |
|
@stasimus , sure, but I want to make sure fold actually isn't bugged without the wrapping. |
|
I think it does consistently fail, it just needs more cases than the default. ScalaCheck's budget is 100 and munit doesn't override it, while on your branch the default seed falsifies at 152. With seed gEuK2d5wMaG6hAem1iPvdbemcyKX2vZY0Qp6nVhrACO= at case 152: Succeeded(None) is unsafeRun's initial value, so that side never completed. The same pair doesn't diverge on base, and base survives 50k cases across two seeds. Might be worth a look before the test goes. |
i'll check all 3 variants, base, head and head minus fold hunk |
|
I had a go at that. Built three trees, base, your head, and your head with just the fold hunk reverted, and ran four shapes that all interpret through use, so fold's Eval branch: Head and head-minus-hunk agree on all four, so I couldn't get fold to misbehave without the wrapping. Not proof, I only tried shapes I could think of. What did surprise me is B and D. allocatedCase now runs the continuation, but the same resource through use still doesn't, so those two paths disagree on your branch where they agreed on base. Is that intended? |
|
@stasimus , can you share the full code? I'm unclear where |
|
@reardonj please look into reardonj#3 ctr is just a Ref in a helper, same shape as your eval - uncancelable continuation test: IO.uncancelable(poll => poll(allocate).flatMap(_ => ctr.update(_ + 1))) |
461008f to
93d45a0
Compare
Adds uncancelable wrapper to evaluation of resources in allocatedCase so that cancelation can only occur at reasonable points.
93d45a0 to
3af09e6
Compare
If you had the I had assumed that the fix must also happen in fold, since I took the liberty of doing a squash and rebase to get the build more stable and so my incorrect removal of the law test doesn't end up in history |
Adds
uncancelablewrapper to evaluation of resource so that cancelation can only occur at reasonable points.This fix does a slightly more involved poll wrapping of the calls @durban identified as fix targets in #4627 . Passes both the law tests and a new test for the bug.
I'm a little suspicious of thepollwrapping the allocate case as it aught to break the tailrec, butallocatedCasealreadypollsits continues inside it'sAllocatedcase, so I guess it isn't any worse than existing code.EDIT: oh, I see now, the Allocate case was just suspending the whole thing in
bracketFulland returning anyways. Tail recursion only happens in Bind and Pure.