Semantics of canceled futures/promises

Issue #191 new
Amir Kamil created an issue

Impl issue 536 raises the issue of what should happen when a promise reaches a state where it can never be fulfilled. This can happen if a user creates a promise and the drops all references to it without having satisfied the initial dependency. Furthermore, the user can schedule dependent work on the underlying promise cell, which in turn would never execute. As described in impl issue 536, this results in a memory leak in the current implementation. We need to resolve the semantic issues before that leak can be fixed.

It seems that placing a future header in a “canceled” state could be a useful feature for programmers – if, for whatever reason, a program reaches a condition where it no longer needs dependent work to be done, it would be nice to have a way to cancel that work. Ideally, canceling a future header should cause all work that recursively depends on that header to in turn be canceled.

Some semantic questions are:

  1. Should cancellation happen automatically when no user-level references remain to a future header, or should it require an explicit cancel() call?
  2. If an explicit call is provided, should it only be on promises or are should it be allowed on any future?
  3. What happens if a user cancels a future header that is already ready?
  4. What happens if a user schedules new work that depends on an already-canceled future header?
  5. Should we provide a query that checks whether a future/promise has been canceled?

For Q1, if we go with automatic cancellation, it could be the case that the user accidentally drops all references to a future header without intending to cancel it. My preference would be to require explicit cancellation, with assertions in debug mode to detect when a future header loses its references such that it can never be readied.

If we provide an explicit call, I think one of the issues we need to resolve is what would happen if a promise were used in a UPC++ operation and then canceled. Does that cancel the operation? We might need to do quite a bit of implementation work to do this right.

Q2 and Q3 are only relevant for explicit cancellation. So maybe they argue for implicit cancellation instead.

Q4 can happen even with implicit cancellation: the user creates a promise, schedules dependent work on it (e.g. using then()) and saves the resulting future, drops the original promise, and then schedules something on the now-canceled, dependent future.

Currently, the only notion we have that is similar to canceled future headers is default-constructed futures, which are specified to never become ready. The implementation uses a special “nil” header and has an assertion that checks for dependent work being scheduled on it.

Due to the semantics issues that need to be resolved, we are not targeting this for the upcoming 2022.3.0 release.

Official response

  • Dan Bonachea

    My thoughts on this:

    We should provide explicit cancellation on promises, but promise::cancel() should work as the abstract equivalent of promise::require_anonymous(INFINITY) (which obviously cannot be directly expressed in code, but this is the most concise expression of my proposed semantics). Under this formulation:

    1. Cancellation is an explicit operation and never invoked implicitly.

    2. Promise cancellation does not cancel any in-flight operations that might be holding refs (which is good, because that's not something we're able to efficiently/robustly support). However it places the promise into a state where it's guaranteed to never reach fulfillment (dep count 0), regardless of the number of subsequent promise::fulfill*() calls (which are still permitted, as are all other promise operations). As such, futures referencing this header are guaranteed to never become ready.

    3. As a corollary to the above, promise::cancel() would have a precondition of dependency_count >= 1, for the same reason as promise::require_anonymous(). IOW it's an error to request cancellation of a promise that has already reached fulfillment, because that's a meaningless request.

    4. There is no cancellation on future objects, cancellation requires the caller have access to the promise object (for all the same reasons we don't have a future::require_anonymous())

    5. We could mandate (and check in debug mode) that dropping all refs to promise requires either fulfillment (dep count == 0) or cancellation (dep count == INFINITY). I think this is sufficient to fix the leak.

    This still leaves an open question about whether we allow future::then() on a future whose promise has been cancelled. I think we could go either way on this, although if we choose to prohibit it then I think we need to also provide a cancellation query to help the user dynamically avoid the prohibition. If we follow this path, then I think both the prohibition and query probably needs to be transitive to dependent futures - eg:

    promise<> p;
    future<> f1 = p.get_future();
    future<> f2 = f1.then(...);  // ok
    p.cancel();
    assert(f1.is_cancelled()); // the directly cancelled header
    f1.then(...); // this would be an error
    assert(f2.is_cancelled()); // dependent on a cancelled header
    f2.then(...); // this would also be an error
    

Comments (4)

  1. Dan Bonachea

    My thoughts on this:

    We should provide explicit cancellation on promises, but promise::cancel() should work as the abstract equivalent of promise::require_anonymous(INFINITY) (which obviously cannot be directly expressed in code, but this is the most concise expression of my proposed semantics). Under this formulation:

    1. Cancellation is an explicit operation and never invoked implicitly.

    2. Promise cancellation does not cancel any in-flight operations that might be holding refs (which is good, because that's not something we're able to efficiently/robustly support). However it places the promise into a state where it's guaranteed to never reach fulfillment (dep count 0), regardless of the number of subsequent promise::fulfill*() calls (which are still permitted, as are all other promise operations). As such, futures referencing this header are guaranteed to never become ready.

    3. As a corollary to the above, promise::cancel() would have a precondition of dependency_count >= 1, for the same reason as promise::require_anonymous(). IOW it's an error to request cancellation of a promise that has already reached fulfillment, because that's a meaningless request.

    4. There is no cancellation on future objects, cancellation requires the caller have access to the promise object (for all the same reasons we don't have a future::require_anonymous())

    5. We could mandate (and check in debug mode) that dropping all refs to promise requires either fulfillment (dep count == 0) or cancellation (dep count == INFINITY). I think this is sufficient to fix the leak.

    This still leaves an open question about whether we allow future::then() on a future whose promise has been cancelled. I think we could go either way on this, although if we choose to prohibit it then I think we need to also provide a cancellation query to help the user dynamically avoid the prohibition. If we follow this path, then I think both the prohibition and query probably needs to be transitive to dependent futures - eg:

    promise<> p;
    future<> f1 = p.get_future();
    future<> f2 = f1.then(...);  // ok
    p.cancel();
    assert(f1.is_cancelled()); // the directly cancelled header
    f1.then(...); // this would be an error
    assert(f2.is_cancelled()); // dependent on a cancelled header
    f2.then(...); // this would also be an error
    
  2. Amir Kamil reporter

    Proposed implementation in impl PR 489. It prohibits doing a then() or when_all() on a canceled future (this is checked in debug mode) and provides a canceled() (and cancelled() for those who prefer to spell it that way) query.

    A side note: a canceled promise treats fulfill_result() as a no-op with respect to constructing the result, which means that it can be called multiple times on the same canceled promise. This is because the implementation only has a single “canceled” state (and maintains the invariant that a canceled promise does not contain results), as opposed to separate “canceled with results” and “canceled without results” states.

  3. Log in to comment