-
Notifications
You must be signed in to change notification settings - Fork 85
Description
Summary
Introduce experimental cancelation operation that won't take ownership of futures.
Motivation
Currently operations are only canceled when OpFuture is dropped, in a destructive manner. This would set the canceled flag on RawOp to true, try to asynchronously cancel the operation (on completion-io platform) and drivers will silently drop the RawOp when it's returned from kernel (no matter if it's canceled successfully).
But users may want to retrive the buffer or wait for the cancelation to complete, which requires some API to manually trigger the cancellation without consuming the future. One way to do so is to introduce another set of Cancelable io traits, like what minoio does; but I argue that since cancelability and io operations are independent, this would result in combinatorial explosion (i.e., four addtional traits: CancelableAsync{Read,Write}{,At} or multiple addition cancelable functions in the io traits, and exponential inconvinience for users).
Requirement
- Users should be able to cancel
OpCodessubmitted into driver (i.e.,OpFuture) even when it's deeply nested within other futures - Cancelation should not take the ownership of the future. Users has to be able to retrieve the result (include the buffer). Otherwise it's the same as dropping the future.
- Two flavors: Soft cancel (cancel-and-wait) and Hard cancel (now-or-never, failable)
Proposal
Based on #603, I propose introducing:
CancelToken, a shared handle to issue or retrieve cancelationCancelable, a trait that's implemented for futures that can be canceledCancel<F: CompioFuture>, aCompioFuturecombinator that combines anyCompioFuturewith aCancelTokenCompioFuture::with_cancel(self, token: &CancelToken) -> Cancel, wraps currentCompioFuturewithCancel
impl Cancel {
pub fn with<F>(f: F) -> Self
where
F: FnOnce(CancelToken) -> Fut,
Fut: CompioFuture + 'static;
pub(crate) fn new(mut fut: Fut, token: CancelToken) -> Self;
}
trait Cancelable {
fn set_cancel(&mut self, token: &CancelToken);
}
impl<F: Future + 'static> Future for Cancel<F> {
type Output = F::Output;
// ...
}
trait CompioFuture {
// ...
fn with_cancel(self, token: &CancelToken) -> Cancel {
Cancel::new(self, token.clone())
}
}Unstable Specialization
Cancel::new will call try_as_dyn to see if fut implements Cancelable. If it does, pass the CancelToken down. Internally, compio will wrap all publicly-facing API's using Cancel::with so that when user calls .with_cancel, the CancelToken is correctly propagated.
Eager vs Lazy
There are two approaches when user issues a cancelation on when to react:
Approach 1: Passive, Lazy
Cancel command only set a boolean flag on CancelToken, and when any Cancelable future is polled (mainly OpFuture), it checks whether the flag is set, and if so, cancel underlying operations.
Pros: No need for any aditional states being stored in CancelToken.
Cons: Cancelation is delayed to next poll, hurts completion io: if the operation is submitted, the chance of successful cancelation decreases as the poll gets delayed.
Approach 2: Active, Eager
Stores some kind of canceler in the CancelHandle and when user issue the cancelation, the canceler issues async cancel instantly. Monoio takes this approach, and this is the one I prefer.
Pros: Instant cancel issuance, higher chance of successful cancelation
Cons: Shared state in CancelHandle, requires lock, RefCell or maybe linked list
Misc
I'm using american spell, which only takes one l (i.e., cancelable, canceling, cancelation).