cancelWith method
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;
}
}
}
}