One of the biggest problems with Linear/Undroppable types is unwinding - you cannot drop that type, so if you unwind, you cannot go forward. Currently there are 2 ideas: first is to abort the process, second is to have some kind of "no unwind" effect.
I think a third approach is possible, for futures. When sync code panics, there is not much you can do, as control flow is not in your hands. But with async it is a little bit different. I think it may be useful if Future that panicked during the poll will become pending, i.e. always return Poll::Pending. Then, with a trigger from the panic hook, it may be cancelled in some way.
So, what if a future containing undroppable types will handle panics like that? And if undroppable type is not held across await points, store it in the Future anyways.
I made a proof of concept crate pending_unwind, but I think it may be possible to add another method for Future that will help recover from the panics with linear types present, maybe involving async Drop?
I can say with confidence that I don’t understand how this API is supposed to be used even after reading your whole post here, the API docs, and the included test cases.
Maybe a good, illustrative example could help?
All I can tell is that this NoUnwind<Fut>’s Future impl looks a lot like this goes against the general property/contract for well-behaved Futures that:
If the future is not able to complete yet, it returns Poll::Pending and arranges for the wake() function to be called when the Future is ready to make more progress.
IIUC: a future that panicked will never be able to make progress, so converting this into Pending-forever doesn’t violate the contract. The idea, I think, is that the panic hook would call nesting_depth and then alert the executor somehow if it is nonzero. The executor would then be charged with handling the situation.
That would probably be a performance suicide. And I'm not sure, what is the problem with joins and selects? If one of the futures is undroppable, their combination is undroppable too. For join it should be reasonable that if one of the futures diverges, the whole combined future will diverge. For selects: if one branch diverged and other returned a value, we wentured deep into the async drop territory
If you return Poll::Pending on panic, then join won't know that the future has panicked and need to call async drop, it would just keep polling it and it would not give the executor a chance to check if an exception has happened, unless it inserts a local thread check into its polling code somewhere, which is also expensive.
Essentially it introduces an implicit control flow that relies on thread-local global variables, instead of using Result, implicit exception handling is always hard to optimize and hard to reason than explicit Result control flow.
This is even harder to reason than throwing exception as it creates a control flow that seems normal, but somehow is actually handling an exception and doing rewind.
Having a poll_no_panick also means that the future can runs its cleanup code itself.
The future that panicks would run its own cleanup code, before propagating it to its parent future to do the unwind.
It essentially eliminates the dependency of libunwind and replace it with rust generated async code, easier to optimize and reason for compiler, libunwind is actually kind of slow, complex, opaque and hard to optimize.
Who will keep polling it? Join is just a proxy that polls each joined future one time when it is polled itself. Or do you mean polling due to progress of other futures? That should be handled via executor. Reporting of the failure, as it have been mentioned earlier :
The idea, I think, is that the panic hook would call nesting_depth and then alert the executor somehow if it is nonzero. The executor would then be charged with handling the situation.
Yes, the control flow is not that simple, but this is not an application logic thing. Results must be used for logic. Panics already can unwind at any point. We aren't returning results with possible panics because it would (1): make writing code a horrible experience (2): pollute abi with unnecessary details, slowing everything down.
Panic in a non-drop future is not a situation Join could adequately handle, so in this idea we offload it to the user and an executor, and each user can decide their own strategy.
For example, the executor can try free everything it could and leak the rest, not poll the future anymore, and before that the user code notifies the "team" that the bug occurred and servers leaked some memory.
If Join won't be polled, futures will not be polled. And even if it is, panicked future will just return Pending, effectively making Join the same Pending future.
That is the problem, async rust is cooperative, if the Join never returns, then the executor would never has a chance to check for panic.
If join does not check for panic, it'd take a long time before Join yields control, as it could be doing something else, where as previously it returns on panic.
This would be a shocking change in behavior, user would expect panic to propagate.
And not to mention that change along is breaking API, no one expects Future to return Pending on panic.
Having a new function that explicitly returns the panic payload would be much better than implicitly breaking existing code.
That's why I said that panic should be reported to the scheduler. And by the way, infinite loop requires something actively waking the executor, and there's nothing like this.