Skip to content

RFC: User-initiated Cancellation #604

@George-Miao

Description

@George-Miao

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 OpCodes submitted 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 cancelation
  • Cancelable, a trait that's implemented for futures that can be canceled
  • Cancel<F: CompioFuture>, a CompioFuture combinator that combines any CompioFuture with a CancelToken
  • CompioFuture::with_cancel(self, token: &CancelToken) -> Cancel, wraps current CompioFuture with Cancel
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).

Reference

Metadata

Metadata

Assignees

Labels

RFCRequest for Comment, proposals for features or changesenhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions