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

Resolve undefined IO throwing behavior in the presence of async and non-termination #110

Open
jnape opened this issue Nov 11, 2020 · 7 comments

Comments

@jnape
Copy link
Member

jnape commented Nov 11, 2020

Suppose we have the following IO tree:

IO<Object> throwImmediately = IO.throwing(new IllegalStateException("head fell off"));
IO<Unit> parkForever = io((SideEffect) LockSupport::park);
throwImmediately.discardL(parkForever).unsafePerformAsyncIO().join();

What should the result be? Should it throw, or block?

Intuitively, it feels like it should probably throw immediately, and why wouldn't it? Well, because there is asynchrony, and since discardL is just a sugary zip call, and since zip can only execute once both results have been computed, the backing future holding the thrown exception will never be interrogated, so the exception will never propagate.

Investigate whether or not a principled approach to parallel IO can also easily immediately propagate exceptions under the context of asynchrony without violating correctness.

@jnape
Copy link
Member Author

jnape commented Nov 11, 2020

Apparently CompletableFuture.allOf behaves the same way:

CompletableFuture.allOf(CompletableFuture.supplyAsync(() -> {
                throw new IllegalStateException("kaboom");
            }), CompletableFuture.supplyAsync(() -> {
                LockSupport.park();
                return null;
            })).join(); // blocks forever, never throws

@corlaez
Copy link
Contributor

corlaez commented Mar 15, 2021

That code should probably wait forever.
The practical solution that comes to mind is to define a timeout for the IO unsafePerformAsyncIO(miliseconds(300)).

@jnape
Copy link
Member Author

jnape commented Mar 15, 2021

There's certainly a good reason for it to wait forever (and that's currently what would happen), but there's no reason that the operational semantics couldn't be specified by the consumer either. For instance, I could certainly imagine a semantics for IO#zip that, under async execution, forked each parallel branch into its own CompletableFuture, and attached completion callbacks to each that, if fulfilled with an error, attempted to cancel all remaining running futures (potentially also aggregating all other thrown exceptions into the suppressed side of the final Throwable, since multiple async branches could throw simultaneously). At least this strategy would make a best-effort to yield as soon as feasible whilst also attempting to signal the other threads that they need not continue.

To do this properly would require being able to tap into the Executor that an IO may run with, which means it may finally be time to model an ExecutionPlatform for IO. This has been on my radar for years, but I'd like to probably pull IO out of lambda first.

@corlaez
Copy link
Contributor

corlaez commented Mar 16, 2021

I see what you are saying.
In a way this is similar to the short-circuit behavior... where you don't want to calculate everything if one of the calculations fails.

Would Execution Platform be an implementation detail?

Regarding pulling IO out of the library I don’t know why would that be necessary.

could it be the case that we could achieve both executions desired with two different IO classes?

IO (IOWaitAll)
IOCancelAllOnError

Then, you can use a different execution strategy for that class, that allows cancelling all other tasks as soon as we throw on one of them.

I could give it a try for a poc implementation

@corlaez
Copy link
Contributor

corlaez commented Mar 16, 2021

Then the consumer would do:

cancellAllOnThrow(throwImmediately.discardL(parkForever)).unsafePerformAsyncIO().join();

@corlaez
Copy link
Contributor

corlaez commented Mar 19, 2021

@jnape I watched this talk recently, talking about IO and analogous implementations in Scala and how the cats-effect library solves a lot of the previous issues with a bunch of IO classes:

image

I am not sure it will completely solve the issue but it sounds really interesting:

https://www.youtube.com/watch?v=g_jP47HFpWA

@corlaez
Copy link
Contributor

corlaez commented Mar 23, 2021

I just heard this talk that gives an intro into Lawvere theory A categorical view of computational effects - Emily Riehl
It seems to be a principled, mathematical way to think about running monads in parallel (sum) or combine (tensor product), etc.
She made an important note that Lawvere theory only applies to finite monads and thus continuations monad won't work in that framework (and thus treated a little different than finite monads)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants