Repository navigation
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 |
|
@durban , do you have any time to take another look at this? |
|
Sorry, I couldn't follow the discussion, and reading it now, it's unclear to me, why |
I haven't found a way to break it in
Looking closer at the keypool code, I don't think it is actually related to this issue at all. It's Eval is cancelable. |
|
Okay, so, is Intuitively I would've said no. However, it is clearly yes for Interestingly, removing the |
|
Oh, sorry, I've misread (I still don't understand the law failures.) |
It can happen in IO, so why wouldn't it be the case here? I am reading the preservation of interruptability (which probably should have a terminology update in the docs? That's not CE3 terms) as being specifically about Resource.eval. As in, If you life an uncancelable effect into Resource via eval, that Resource must also be uncancelable. An a convenient way to demonstrate that is via timeout, as its contract forbids timing out the uncancelable side. But once you compose that resource with others, cancelation is on the table. The point of how Resources compose is that you should be able to safely cancel inbetween steps and the finalizers will do the right thing.
I get 3 failing tests:
I also don't really understand the law tests insofar that I don't know how they go about reducing forms to try making them the same. |
|
Sorry, what I should've wrote is that (as far as I can see) the law tests never attempt to cancel the 2 sides. So any difference in cancellability should be unobservable. And yet, they fail... |
|
The tests do take arbitrary |
|
Yeah, you're right, indeed the IO generator generates self-cancelling things. |
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.