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

Fix propagation of interruption from CIO to ZIO #696

Closed

Conversation

kyri-petrou
Copy link
Contributor

@kyri-petrou kyri-petrou commented Jul 25, 2024

/fixes #695

/claim #695

Note that I also changed the code to use runOrFork instead of fork within toEffect. This is to avoid the overhead of forking fibers when the ZIO effect can be evaluated synchronously. Users that convert simple ZIO effects to CIO often will see a very notable performance improvement from this change

Copy link

@steinybot steinybot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for looking into this. A few passing comments/questions.

.addObserver(exit => cb(Right(exit)))
val cancelerEffect = F.delay {
val _ = fiber.interrupt
out match {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cats syntax is imported so you could use out.leftMap

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it but I decided against it as it's going to increase allocations from the Function1 and the closures of local vals captured within the function. I think we can afford a bit of "uglier code" to improve performance, wdut?

case Left(fiber) =>
val completeCb = (exit: Exit[Throwable, A]) => cb(Right(exit))
fiber.unsafe.addObserver(completeCb)
Left(Some(F.async[Unit] { cb =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the finalizer need to wait for the fiber complete?

Copy link
Contributor Author

@kyri-petrou kyri-petrou Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it does. The finalizer is wrapped in an effect so it's only invoked when cancellation is invoked. Is that explaining what you asked or did I miss something?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this finalizer is F.async because we want it to complete only when the ZIO fiber completes. I was wondering if it could just be F.defer and be more of a fire and forget as in:

              Left(Some(F.delay {
                fiber.tellInterrupt(Cause.interrupt(fiber.id))
              }))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh wait now I understood your initial comment. I think we do need to await for it, otherwise we might be leaking resources etc. Also if we didn't await for it, the test that I added would be failing because there is no guarantee that the side effect is run before the CIO completes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does making it async prevent resource leakage? The only way for something to leak is if the ZIO fiber ignores the interupt, which is an entirely different problem. And if that does ever happen by waiting this is now leaking a cats fiber so I think it makes it worse not better.

Also if the CIO is used in a race but the ZIO takes a while to respond to the interrupt then it is holding up the downstream effects unnecessarily. Actually it might be worth seeing whether CIO race waits or not.

As for the test, can't it add an onExit to the ZIO that completes a promise and wait for that? That's what I started doing in my repro.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async part is so that we don't synchronously block till interruption is completed.

Let's forget about interop for a minute. If we were working purely with ZIO or CIO, wouldn't you expect that all finalisers have finished executing when you issue an interrupt signal and await for the fiber to complete? This is what the async part does in a nutshell; it makes sure that everything in the interrupted effect has finalised by the time we resume execution.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good question. I can think of cases where I would want both. Looks like IO.race vs IO.racePair. The former will cancel looser and await its finalisation, the later will not. Similarly with ZIO.race. So yeah you are correct.

fiber.unsafe.addObserver(interruptCb)
fiber.unsafe.removeObserver(completeCb)
fiber.tellInterrupt(Cause.interrupt(fiber.id))
// Allow the interruption to be interrupted

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like finalizers are uncancellable so this will never happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh thanks for pointing this out. I was debating with my own self whether to add it 😬

Only reason I added it was just in case CE adds support for timing out interruption. I'm happy to remove it though to make the code more readable. Wdut?

Copy link

@steinybot steinybot Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards removing it, especially if we don't need the finalizer to be async. It also seems unlikely that they would ever add it because it would be turtles all the way down. Could you then cancel the cancellation of the cancellation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you then cancel the cancellation of the cancellation

My thinking is that some frameworks allow you to set a deadline for cancellation. But yeah I think let's remove it since it's a bit of an overkill for something that's not supported yet

@kyri-petrou
Copy link
Contributor Author

Closing and reopening as an attempt for the /claim tag to work

@kyri-petrou kyri-petrou reopened this Jul 26, 2024
@kyri-petrou
Copy link
Contributor Author

@steinybot closing and reopening didn't seem to work, I'll close it, address the comments and open it as a new PR

@kyri-petrou kyri-petrou deleted the fix-interruption-propagation branch July 27, 2024 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Canceler effect in toEffect does not run
2 participants