async_job 1.0.0
async_job: ^1.0.0 copied to clipboard
Cancel async work in Dart: cooperative cancellation, child tasks, resource cleanup and explicit outcomes, in the manner of structured concurrency. Pure Dart.
1.0.0 #
Breaking changes #
ctx.wait, deprecated in favour ofctx.abandonablein 0.3.0, is removed.HandlerCancelReasonis renamedBodyCancelReason, and itsname, which outcomes and traces print, isbodyinstead ofhandler. The reason says that the body cancelled itself, directly or by letting the cancellation of a child out.
Added #
Job.debugprintsawaited before startthe first timedoneorvalueof aJob.deferredis read while nobody has started it: a wait for such a job does not end until somebody starts or cancels it.
Documentation #
- The README says what each kind of change comes in from 1.0.0 on: a fix in a
patch release, an addition in a minor one, a break in a major one. The engine
protocol of
engine.dartis outside that promise and can change in a minor release; Building on the core tells an engine to holdasync_jobto one minor version. - The README names the analyzer plugin
solo_lintsinInstall. - The description of the package and the first paragraph of the README say what it is for in the words it is searched by: cancelling async work, and structured concurrency.
- The observing page says that the name of a job goes into the error messages
of the core as well, so
keyanddescribehold an identifier rather than personal data. - A new page, With riverpod and bloc, puts a job under a riverpod provider or notifier and a bloc handler, and aborts an HTTP request of http or dio with the job.
- The README says that a new value of an enum or a new subclass of a sealed
class comes in a major release, and that a switch over
CancelReason, which is open, keeps a default case. - The README says that a job is not a
Future:await jobwaits for nothing, and theawait_only_futuresrule ofpackage:lintsflags it. - The streams page says what a second
eachon a single-subscription stream does: the job it makes fails, and the call does not throw. - The time page says that a negative pause counts as zero.
- The error rules page names the
Job.debugline of a deferred job awaited before its start.
0.5.0 #
Breaking changes #
JobContextgainschild, andJobContextBaseimplements it. A class that implementsJobContextitself adds it, a subclass ofJobContextBasewith a member of that name renames it or makes it an override, and an extension onJobContextwith a member namedchildis shadowed without a warning. An engine that makes children of its own, increateEachJobor elsewhere, or keeps rules incheck(), overrideschildwith@useResult: the default is aJob.deferred, which such an engine refuses or which does not ask its rules.
Added #
ctx.child(body, key:, describe:, timeout:)makes an unstarted child that the engine of the job accepts, for code that holds the context and does not know the engine. InJobContextBaseit is aJob.deferred. An engine whose children are jobs of its own overrides it, andctx.eachtakes its child from there.ctx.childstarts and checks nothing:ctx.run,ctx.startandctx.runAlldo the refusing. See Running a child; the step of A deadline for one step is made this way.
Documentation #
- Time moved by the test says how to test a job that does real input and output: fake the timers alone.
- Work in an isolate shows how to run work in an isolate that the job's cancellation kills in the middle of a step.
0.4.0 #
Breaking changes #
JobObserver.onErrortakes{required JobErrorAnswer answer}: what the job does about the error.answered: it asks for an answer, throughJobAnswerer.onUnansweredor the zone.cancellation: it never does, because the error is a cancellation.never: it never does for another reason, such asignoreFailure().later: the outcome that carries the error may still be read, and nothing is promised. For an error no outcome carries, and for the failure of the body of a job that has accepted a cancellation by the time the body throws,Job.isCancellationErroris now asked beforeonError, not after it: anonErrorthat cancels its own job no longer sends that error under the filter, a filter set onceonErrorhas heard the failure is not asked about it, and the error of a filter that throws reaches the zone beforeonErrorhears the error the filter was asked about. The core no longer announces through the@protectednotifyObserver: an override of it hears only what the engine itself sends, andonErroris toldlaterabout each of those, even afterignoreFailure(). See Where each error goes.
Documentation #
- The queue and the rule left Building on the core for a page of their own, An engine of your own.
- A resource handed to another job, cleanup without a call and the order of cleanup left Cleanup for Cleanup rules.
- The way of each kind of error, the debug channel, an error that holds several failures and the rare cases left Errors for Error rules.
0.3.0 #
Breaking changes #
What to write instead, with the 0.2.0 name on the left; each has its entry
below.
0.2.0 |
0.3.0 |
|---|---|
Job.ignore() |
Job.ignoreFailure() |
JobBase.whenDone |
JobBase.untilFinished |
JobBase.debug |
Job.debug |
JobBase, JobContextBase, JobStatus from package:async_job/async_job.dart |
from package:async_job/engine.dart |
Job<T> handed to ctx.run |
ChildJob<T>, or DeferredJob<T> for start() |
ctx.wait |
ctx.abandonable |
JobObserver.onErroris a notice, and the newJobAnswereranswers for an error nobody else has: one no outcome carries, and aFailednobody observes. An observer from0.2.0compiles unchanged, and the errors it used to swallow reach the zone; see Answering for errors.Job.ignore()is renamed toJob.ignoreFailure(), andJobBase.whenDonetoJobBase.untilFinished.- A job is continued once: by one
then, or by handles of the newJob.shared. A secondthen, athenon a job with a handle andsharedon a job with athenthrow aStateError: give each branch a handle ofshared(), see A handle for each branch. ctx.runtakes aChildJob. A handle declaredJob<T>no longer compiles there: declare itChildJob<T>, orDeferredJob<T>where the code callsstart(), and give the jobs of an engineimplements ChildJob<T>.- A class that implements
JoborJobContextitself adds the new members, and a subclass ofJobBaserenames its own member whose name a new one now takes.JobgainswhenFinishedandshared,JobBasegainsuntilFinished, andJobContextgainsabandonable,heldCancellation,pause,runAll,startand thedisposeanddiscardofrun;then,sharedandeachtakekeyanddescribe, in an override too; an extension onJoborJobContextwith the name of a new member is shadowed without a warning. JobBase,JobContextBaseandJobStatusmoved topackage:async_job/engine.dart, andJobBase.debugisJob.debug. See Building on the core.- The protected surface of an engine changed.
JobContextBase.check,JobBase.finishandJobContextBase.startChildare@mustCallSuper;started,finished,adoptedBy,createContext,execute,createEachJobandbeforeChildStartare@visibleForOverriding;cascadeToChildrenand thelevelsetter are gone in favour ofcancelWith,cancelOwnJobandstartChild;inUncancellableSection,heldCancel,creationZoneandrequireSharedare new;createEachJobtakes thekeyanddescribeofctx.each, and an override hands them to the job it makes. - The core announces a
Failedan engine hands tofinish: an engine that callednotifyObserverbeforefinishdrops that call, or the observer hears the error twice. - A cancellation inside a
ParallelWaitErroris a cancellation again: a child cancelled under[ctx.run(a), ctx.run(b)].waitends the parentCancelled, where it ended itFailed. - A body that cancels itself is cancelled the way a cancelled job is: its
children are asked to stop, then its
ctx.onCancelcallbacks run andwhenCancelledfires.
Deprecated #
ctx.waitis deprecated in favour ofctx.abandonableand goes away in the next release.
Changes you will see on upgrade #
Errors that 0.2.0 lost, or told only to the observer, now reach the zone the
job was created in, and a test fails on them:
- the failure of the body of a job that ends
Cancelled, such as an operation stopping at a token the job cancels; - the failure of the body of a job an engine ends by hand;
- the late failure of an action the body left under
ctx.abandonablethrough.timeoutorFuture.any; - an error no outcome carries, of a job that has an observer;
- the failure of a source's own cleanup when
ctx.eachlets go of a stream.
Job.isCancellationError, set once in main, stops the errors a cancellation
causes at onError, job.ignoreFailure() silences the first two for one job,
and an observer that mixes in JobAnswerer answers for the rest; see
Where errors go.
A job that gives up inside a call it walked away from no longer reaches
onError as a failure.
ctx.check() throws the job's Cancelled in the cleanup of a cancelled job,
where 0.2.0 threw a StateError.
onError hears each failure and each cancellation inside a ParallelWaitError
in a call of its own, and the zone gets each failure of one nobody observes on
its own, where 0.2.0 passed on the whole error once.
A child's cancellation that the body lets out prints as
Cancelled(handler: Job(thumbnail): Cancelled(timeout)), where 0.2.0 printed
child thumbnail.
JobBase.children is a copy taken at the call, where 0.2.0 returned a live
view.
With asserts on, an AssertionError reaches the zone when an engine ends two
children of one job with one Cancelled instance.
A Job<void> or a then<void> continuation whose body or callback returns a
job of Job.deferred nobody has started, and a child of ctx.each whose
callback does it for an event, end Failed with an ArgumentError, where
0.2.0 ended them Done.
Several error messages are reworded: a test that matches them by text needs the new wording.
Added #
ctx.startstarts a child the body does not wait for; its outcome stays with whoever holds the child and leaves the parent's alone.ctx.runAllruns children side by side and asks the rest to stop as soon as one of them goes wrong, with the newSiblingCancelReason. See When one failure makes the rest pointless.ctx.pauseis a delay a cancellation ends.ctx.heldCancellationnames the cancellation anuncancellablesection holds back at the moment it is read. See Holding the cancellation back.ctx.runtakesdisposeanddiscard, asctx.abandonabledoes.Job.whenFinishedhears the end of a job without observing its outcome.timeoutgives a job a deadline of its own, after which it endsCancelled(timeout)with the newTimeoutCancelReason.Job.eachmakes a root job that follows a stream.then,sharedandctx.eachtakekeyanddescribeand name the job they make as any job is named; without adescribeit is describedthen,sharedoreach. See A job's name.JobObserver.allmakes one observer of several.JobObserveris anabstract mixin class, so that an observer that extends another class mixes it in.Job.isCancellationErrornames the errors a cancellation causes:onErrorhears them, and nobody is asked to answer for them. See Errors of a cancellation.Job.visitErrorshands each failure and each cancellation inside an error to a callback of its own.startedofCancelled.byis optional and defaults totrue.- The debug channel names a job that dropped the conditional registrations of a
value it handed over, and a job that ended
Donewithout handing over such a value: unless the value is inside the result, nothing will dispose of it.
Fixed #
ctx.runandctx.eachrefuse a class that implementsJobBaseinstead of extending it with theArgumentErrorthey promise, not aNoSuchMethodError.- A value that comes to a call of
ctx.abandonablethe cancellation has already ended is released at once; held until the children had ended, it left a child waiting for the same lock for good. - An error thrown by
JobBase.startedgoes toonError, and the body runs; the job was left running with no body, forever. - Jobs equal by
==are no longer taken for one another: a parent of two such children finished while one of them still ran. - A key or a
describethat throws prints as a mark in the job's name instead of replacing the core's error message. - A child, a cancel callback or a listener leaves its list without a search: thousands of them in one job took time quadratic in their number.
- A body that throws
Cancelledaccepts that cancellation at once: until the children were done,isCancelledansweredfalse, and acancel()put its own reason over the one the body chose. - A finished job lets go of its body, its parent and the cleanups it never ran,
and a job made inside work handed to
ctx.unattendedlets go of the job that started the work: a kept handle held everything they captured. - The remover that
whenCancelledorctx.onCancelreturns lets go of the callback once called or once the job ends: an owner that kept its removers until its own end kept everything the callbacks captured. - The remover that
ctx.onDisposeorctx.onDiscardreturns lets go of the cleanup and of the job once called, once the cleanup has run and once the job has ended without running it: an owner that kept its removers until its own end kept the job and everything the disposers captured. - A job made by
Job(...)inside work handed toctx.unattendedruns in the zone the work was started from, so a bareunawaitederror in it no longer reaches the observer of the job that started the work. ctx.joinreleases the value throughdisposeordiscardwhen the checkpoint after the action throws anything, not only aCancelled.ctx.joinandctx.abandonablecalled from work handed toctx.unattendedno longer hand that work a resource they have already closed.- A value that comes back after the body has ended no longer makes the core
throw
Future already completed. - A value arriving for a job an engine ended by hand is released instead of leaking.
- A tree of jobs that runs out of stack while it is being built ends with the
StackOverflowError, where its root could be left running for good, and a cancellation that runs out of stack in a deep tree still runs theonCancelcallbacks of the jobs it reached. - A call of
ctx.abandonablethe body did not await no longer sends the job's cancellation to the zone when the job accepts it after the body has ended. - Thousands of jobs that wake up together no longer cost about two milliseconds each under JIT.
- A
thenattached to a job whose failure has already been answered for no longer answers for it a second time.
Documentation #
- The README is a starting page, and the reference moved to twelve pages: Outcomes, Cancellation, Time, Children, Groups, Chains, Streams, Cleanup, Observing, Errors, Testing and Building on the core.
on Cancelled { rethrow; }in front of thecatch, which the0.2.0README recommended, logs an operation stopped by its token as a failure; Asking the job callsctx.check()in thecatchinstead.
0.2.0 #
-
Breaking:
ctx.run(child)returnsFuture<T>instead of the child's handle. Useawait ctx.run(child)to await its value, children and cleanup; successful completion checks the parent's cancellation before continuing. Keep the originalchildfor cancellation or outcome inspection. Handle the returned future's errors, or use.ignore()for an intentional concurrent start whose result is unused. Start validation still throws synchronously.ctx.eachcontinues to return its child handle immediately. -
Add protected
JobContextBase.startChildfor domain adoption and start checks shared byrunandeach, without observing the child's result. -
Add
Job.thenwith a separate context per continuation, forward outcome propagation and cancellation in both directions. Cancelling the tail waits for unfinished predecessors and their cleanup.ChainCancelReasonretains each adjacent cancellation incause. Continuations are core root jobs with their own optional observer, independent of domain queues. -
Breaking:
ctx.each(stream, (child, event) { ... })returns a childJob<void>immediately. Await.valuefor a throwing result, inspect.donefor the outcome, or call.cancel()to stop it separately. The callback receives the child's context. The parent waits for the child even when its body does not await it, and observers see the child. -
Cancelling
eachimmediately removes the subscription and stops event delivery, then waits for the running callback before child completion and cleanup. Use child context checkpoints to respond to cancellation; a plainawaitcannot be interrupted. Source cleanup from subscription cancellation is still not awaited. -
Breaking:
eachis aJobContextmethod; theJobStreamextension is removed.JobContextBase.createEachJobis a protected factory for domain engines. As withrun,eachis rejected fromunattendedand after the parent body ends. -
Breaking: reasons are extensible classes:
ManualCancelReason,ParentCancelReasonandHandlerCancelReason. Replace named constants with constructors and inspect types instead of comparing names.CancelReasonis abstract; subclasses can carry arbitrary data. -
job.cancel(reason: reason)accepts a custom reason, preserved by identity in callbacks and outcomes. A body throwingCancelled.bynow preserves its explicit reason as well. -
Parent cancellation and a child's cancellation escaping the body retain the source
CancelledinParentCancelReason.causeandHandlerCancelReason.causerespectively. -
Breaking: replace the
Job.whenCancelledfuture withjob.whenCancelled((cancelled) { ... }). The callback receives the fullCancelledand runs synchronously; registering after cancellation calls it immediately. The returned function unregisters the callback. -
Release unused cancellation listeners when a job finishes without cancellation. Registration does not observe a failed outcome.
-
Route synchronous listener errors through
onError, or the creation zone without an observer, as forctx.onCancel. Async callbacks are not awaited.
0.1.0 #
Initial release. The job kernel taken out of solo before its own first
release.
Job<T>: a cancellable piece of asynchronous work with an outcome, children and a cooperative cancellation. Created byJob(body), which starts on the next microtask, or byJob.deferred(body), which waits forstart().Outcome<T>:Done,Failed,Cancelledwith an openCancelReasonand a publicCancelled.byfor engines built on this one.- A waiting family that says what a cancellation does to a call:
ctx.waitends the waiting and not the work,ctx.joinwaits for all of the action and gives up afterwards,ctx.uncancellableholds the cancellation until the step is over,ctx.unattendeddoes not wait at all and hands the work to the engine,ctx.onCancelhands the cancellation to whatever can really stop, andctx.checkgives up where there is no call to wrap.ctx.eachfollows a stream for as long as the job lives, waiting for an asynchronousonDataand taking the subscription with it whatever the outcome. - A cleanup stack:
ctx.onDisposereleases whatever the outcome,ctx.onDiscardonly when the job does not endDone, andwaitandjointake the same two asdisposeanddiscardfor the value they hand over. The engine unwinds the stack after the children and before the outcome, waiting for every disposer;ctx.disown(value)takes a registration back when the body hands the value over itself. - Children through
ctx.run: the parent finishes after them, a cancellation cascades, and a child that is not cancellable refuses it. JobObserverwithonStart,onFinish,onErrorandonLog; a child inherits the parent's. AFailedoutcome nobody observed goes to the zone that created the job, and so does an error with nowhere else to go when there is no observer — but never aCancelled: a cancellation is a decision somebody made, not a failure, and the observer is the only place it is heard.ctx.unattended(action)for work the body starts and does not wait for: it runs in an error zone of its own, and whatever it leaves uncaught — now, or long after the job is over — reachesonErrorinstead of the process.ctx.runandctx.uncancellableare refused from inside it, and a job created in there reports to the zone the body runs in, not to the observer of the job that started the work.JobBase<T>andJobContextBasefor an engine of a domain to subclass: the protected surface, the virtual checkpointcheck(), and the hooksstarted(),finished()andadoptedBy(). The protected surface also carriesreportToZone, for a domain whose own route for an error with nowhere to go ends with nobody — it keeps aCancelledout of the zone exactly as the core does, so a domain does not write that rule again — andthrowIfUnattended, for a member of its context that must not be called from unattended work.