cancelWith method

  1. @protected
  2. @mustCallSuper
void cancelWith(
  1. Cancelled cancelled, {
  2. bool rejectable = true,
})

Cancels the job with cancelled, correcting started to its status.

cancelled belongs to this job alone, as finish explains.

Idempotent. rejectable is what a job created with cancellable: false may refuse, and its refusal is final — nothing is replayed later. Inside JobContext.uncancellable a rejectable cancellation is held instead: the step runs untouched, and the cancellation lands the moment the last section closes — it comes through here a second time then, so an override hears a held cancellation twice. The rules of a domain pass false and go through both.

A subclass extends this member rather than replacing it: solo adds the branch for a job still waiting in its queue and then calls super. An override that forgets the call silently switches cancellation off — for Job.cancel, for the cascade from a parent and for whatever an engine adds.

Implementation

@protected
@mustCallSuper
void cancelWith(Cancelled cancelled, {bool rejectable = true}) {
  Cancelled withStarted(bool started) => cancelled.started == started
      ? cancelled
      : Cancelled.by(
          reason: cancelled.reason,
          started: started,
          description: cancelled.description,
          stackTrace: cancelled.stackTrace,
        );
  switch (_status) {
    case JobStatus.finished:
      return;
    case JobStatus.created:
      _debug(() => 'cancel $this before start: $cancelled');
      finish(withStarted(false));
    case JobStatus.running:
      if (_pendingCancel != null) {
        return;
      }
      if (_committedByGroup) {
        // A branch whose value its group has handed over is still
        // unwinding, and nothing is left for a cancellation to stop: the
        // value is in the caller's hands, and the mark would only make the
        // branch end [Cancelled] with it. Any cancellation, the rules of a
        // domain too, finds it as it would find a child of `run` that
        // handed its value over by ending.
        _debug(() => 'cancel $this: its group has its value');
        return;
      }
      if (!_cancellable && rejectable) {
        _debug(() => 'cancel $this: not cancellable');
        return;
      }
      final marked = withStarted(true);
      if (_uncancellableDepth > 0 && rejectable) {
        // Held, not refused, and the job is not marked: `onCancel`
        // callbacks and the cascade onto children would stop the very
        // step this section protects.
        //
        // A request behind a held deadline is kept apart: the deadline is
        // lifted at the end of the body and the children, and this one
        // must not be lifted with it.
        final held = _heldCancel;
        if (held != null && _isOwnTimeout(held) && !_isOwnTimeout(marked)) {
          _heldBehindTimeout ??= marked;
        }
        _heldCancel ??= marked;
        // Said once the request is held, as the line below is said once
        // the job is marked: see there.
        _debug(() => 'cancel $this: held until the step ends: $marked');
        return;
      }
      // Only a cancellation the job cannot refuse gets here inside a
      // section, and it is accepted over the one the section holds: that
      // one would never land, and `heldCancel` must not keep showing it.
      _heldCancel = null;
      _heldBehindTimeout = null;
      // Marked before the cascade, and only marked: a callback of a child
      // may come back for this job, and the early return above is the only
      // thing that stops it from cascading and marking a second time.
      // What the marking brings with it — `whenCancelled` and the
      // callbacks — still waits for the children, because `solo` pins the
      // order in which a cancellation is seen.
      _pendingCancel = marked;
      // Said after the mark, not before it. `Job.debug` is code of the
      // caller's, and one that cancels this job on hearing the line would
      // come back in here: before the mark it would mark and announce its
      // own reason, and this call would then overwrite the mark with
      // another -- the listeners would hear one cancellation and the job
      // end with a second. After the mark it turns around at the early
      // return above, like a callback of a child does.
      _debug(() => 'cancel $this: $marked');
      try {
        _cascadeToChildren(marked);
      } finally {
        // In a `finally`, because the cascade is recursive and a deep
        // enough tree overflows the stack inside it. Everything the
        // cascade reached is marked by then, and without this the
        // unwinding would leave every one of those jobs marked and
        // unannounced: no `onCancel` runs, nothing is told to stop, and
        // a second `cancel()` turns around at the early return above.
        // The error still reaches whoever asked.
        //
        // Skipped when a callback of a child reached the engine and it ended
        // the job by hand. A no-op by then rather than a defect waiting to
        // happen -- `finish` clears the callbacks and tells the listeners
        // itself -- and kept because the phase a job is in decides who
        // announces its cancellation, and reading that off the status is
        // cheaper than trusting the two to stay in step. The window still has
        // to close: it is `_markCancelled` that closes it, and that path
        // skips it.
        if (_status != JobStatus.finished) {
          _markCancelled(marked);
        } else {
          _outOfStack = false;
        }
      }
  }
}