Своё поверх ядра
Движком эта страница называет библиотеку со своим состоянием, очередью или
правилами планирования. Свой движок стройте поверх ядра: наследуйте
JobBase<T> для задачи и JobContextBase для контекста, который получает её
тело. Базовые классы держат жизненный цикл, описанный на других страницах:
старт, детей, отмену, уборку
и исход. Наследники добавляют поведение движка через свой
защищённый API. Третий тип, JobStatus, показывает, на каком этапе жизни
задача: created, running или finished.
Все три живут в package:async_job/engine.dart, а не в основном импорте:
приложению, которое только запускает задачи, они не нужны. Движок импортирует
этот файл вместо async_job.dart: engine.dart экспортирует всё, что
экспортирует async_job.dart.
Протокол не входит в семантическое версионирование из README: три типа и их
защищённые члены могут измениться в минорном выпуске async_job, и его список
изменений говорит как, в разделе «Протокол движка». Правка типа из основного
импорта, на которую движок должен ответить, например член JobContext, который
переопределяет его контекст, тоже стоит в этом разделе. Держите async_job
в пределах одной минорной версии, например >=1.0.0 <1.1.0, и выпускайте
движок с каждым минорным выпуском ядра.
Эта страница строит задачу движка:
- «Своя задача»: задача, её контекст и защищённый API, через который до них добирается движок.
- «Отложенный старт»: обычная
Job, которая стартует, когда выберет вызывающий, без всякого движка. - «Закончить задачу руками»: что
finishпропускает из того, что делает отмена.
«Свой движок» строит на этой задаче остальной движок: очередь, которая выполняет задачи по одной, и правило, которое останавливает их при выходе из аккаунта.
Своя задача
Заголовок раздела «Своя задача»Своя задача создаёт контекст, который получает её тело, и запускает тело с ним. Две приватные обёртки позволяют остальному движку запустить задачу и дождаться её. На этой странице их никто не зовёт: их зовёт очередь из раздела «Своя очередь» страницы о движке.
import 'package:async_job/engine.dart';
final class MyJob<T> extends JobBase<T> implements ChildJob<T> { // Отпускается, когда задача кончилась, как ядро отпускает своё: тело // держит всё, что захватило, а задачу держат, чтобы прочесть исход // много позже. Future<T> Function(MyContext ctx)? _body;
MyJob( Future<T> Function(MyContext ctx) body, { super.key, super.observer, super.cancellable, }) : _body = body;
@override JobContextBase createContext() => MyContext(this);
@override Future<T> execute(covariant MyContext ctx) => _body!(ctx);
@override void finished() => _body = null;
// `start` и `untilFinished` защищены: остальной движок вызывает их // через эти обёртки, приватные для его библиотеки. Очередь запускает // задачу позже, из своей зоны, поэтому `_launch` запускает её в зоне, // где задачу создали. void _launch() => creationZone.run(start);
Future<void> get _untilFinished => untilFinished;}
final class MyContext extends JobContextBase { MyContext(super.owner);}Член с @protected доступен только наследнику, а движок обращается к задаче
со стороны, из своей очереди. Обёртки приватные, поэтому движок живёт
в Dart-библиотеке MyJob, и никто другой, у кого есть задача, не запустит её
в обход движка. untilFinished ждёт задачу,
не наблюдая её исход, поэтому
за провал, на который никто не посмотрел, всё равно
отвечают. Ожидание done считалось бы
наблюдением.
Тело запускает задачу вашего движка ребёнком через ctx.run(job), а ctx.run
принимает ChildJob: поэтому MyJob и объявляет implements ChildJob<T>.
Задача, которую тело не может запускать ребёнком, это объявление опускает.
ChildJob не добавляет ни одного члена, и start тоже: задачу запускает
родитель через ctx.run, а ваш движок — через свою очередь. DeferredJob —
это ChildJob с публичным start(), для задачи, которую может запустить
любой, у кого она есть.
Код, у которого есть только контекст, делает ребёнка через ctx.child,
и в JobContextBase это Job.deferred, ребёнок ядра. Движок, чьи дети — его
собственные задачи или чей контекст держит свои правила, переопределяет child
в своём контексте с @useResult и возвращает там свою незапущенную задачу.
ctx.each берёт ребёнка из child тоже. child ничего не запускает и ничего
не проверяет: отказывают ctx.run, ctx.start и ctx.runAll.
start запускает тело в той зоне, откуда его вызвали, а очередь вызывает его
позже, из той зоны, где работает сама. creationZone.run(start) запускает
задачу в зоне, где её создали: тело получает таймеры, значения зоны
и обработчик ошибок того кода, который сделал задачу. Если future этой зоны
падает, ошибка доходит до тела. Если запустить тело под другим
runZonedGuarded, оно о провале не узнает и будет ждать вечно, а с ним
и очередь. Для задачи, созданной внутри работы ctx.unattended, creationZone
указывает на зону, откуда запустили эту работу. start, вызванный внутри этой
работы, сам уходит в ту же зону.
Переопределите started() и finished(), чтобы следить за жизненным циклом:
started()вызывается перед стартом тела, когда задача уже работает.finished()вызывается, когда у задачи появился исход. Он вызывается и у задачи, снятой до того, как её тело начало работу, и перед такимfinished()вызоваstarted()не было.- Ошибка, брошенная
started()илиfinished(), уходит вonErrorи затем в зону, если наблюдатель не перехватит её вonUnanswered. Так идёт всякая ошибка, которой не несёт ни один исход, — о них говорит раздел «Куда идёт каждая ошибка» страницы о правилах ошибок. Задача при этом идёт дальше как шла, а недоделанным остаётся ваш собственный учёт, поэтому держите оба хука короткими и безусловными.
MyJob отпускает своё тело в finished(): иначе тело держало бы всё, что
захватило, пока кто-нибудь держит задачу.
Чтобы движок сам отвечал за ошибку, которая иначе ушла бы в зону, сделайте как
solo. Повесьте своего наблюдателя на каждую задачу, подмешайте в него
JobAnswerer и переопределите его onUnanswered. Куда этот ответ доходит,
а куда нет:
- Ребёнок, принятый без наблюдателя, берёт наблюдателя родителя, поэтому ответ движка доходит и до детей.
- Ребёнок, который пришёл со своим наблюдателем, отвечает через своего.
- Продолжение, которое создаёт
then, не задача вашего движка. Оно принадлежит тому, кто вызвалthen, и у него только тот наблюдатель, которого передали вthen. Провал вашей задачи, который продолжение принимает, этот наблюдатель слышит как провал самого продолжения. - За ошибки в коде самого продолжения отвечает тот, кто вызвал
then, в своей зоне, поэтому своего наблюдателя продолжению не передавайте.
Когда onUnanswered наблюдателя движка некому передать ошибку,
super.onUnanswered(job, error, stackTrace) отправляет её в зону, где задача
создана, а Cancelled туда не пускает. Так происходит с ошибкой любой задачи:
ребёнок, принятый без наблюдателя, может и не быть задачей вашего движка. То же
делает reportToZone для движка, чей ответ живёт вне наблюдателя. Этот метод
защищён, поэтому движок добирается до него через обёртку, как до start,
и только у своих задач.
Метод контекста, который нельзя вызывать из работы, переданной
в ctx.unattended, сначала вызывает throwIfUnattended. Об этой работе
говорит раздел
«Работа, которую задача не ждёт»
страницы об ошибках.
Весь защищённый API описан в справке JobBase и JobContextBase.
Отложенный старт
Заголовок раздела «Отложенный старт»Обычная Job стартует на следующей микротаске. Если момент старта выбирает
вызывающий, берите Job.deferred(body): он возвращает DeferredJob<T>,
который ждёт, пока его запустят. Можно самому вызвать его публичный start(),
отдать задачу очереди или передать её родителю через ctx.run(child), как
в разделе «Запуск ребёнка» страницы о детях.
final job = Job.deferred<void>((ctx) => ctx.abandonable(work));// ... позже, или из своей очередиjob.start();start() запускает тело в той зоне, откуда его вызвали. Очередь, которая
вызывает его позже из своей зоны, должна сохранить зону, где создали задачу,
и запустить задачу там: final zone = Zone.current; рядом с Job.deferred,
затем zone.run(job.start). Причину объясняет раздел
«Своя задача».
Этот рецепт — для Job.deferred, а не для задачи вашего движка.
До creationZone у Job.deferred очередь не дотянется: этот член защищён.
Задача вашего движка стартует через creationZone.run(start), как это делает
_launch.
Закончить задачу руками
Заголовок раздела «Закончить задачу руками»finish заканчивает задачу переданным ему исходом. Движок может взяться
за него, чтобы снять идущую задачу, но для этого он не годится: он пропускает
то, что делает отмена.
| Отмена | finish |
|---|---|
| Ждёт детей и останавливает их | не ждёт и не останавливает |
Вызывает колбэки onCancel |
не вызывает ни одного |
Разматывает стек уборки, вместе с dispose и discard |
не разматывает ничего: открытое телом остаётся открытым |
finish ничего не разматывает, поэтому движок, который заканчивает идущую
задачу руками, сначала освобождает то, что она держит. Сколько уборок finish
оставил невыполненными, говорит только отладочный канал, который включается
строкой Job.debug = print;.
Задача, которая приняла отмену, кончается Cancelled, что бы ни передали
в finish. Done и Failed уступают отмене, которую задача приняла,
а переданное значение не достаётся никому. В силе остаётся только другой
Cancelled, переданный в finish.
У любой задачи, законченной руками, провал тела, брошенный до finish или
после, не попадает ни в один исход, если только движок не передал именно его.
onError его слышит, и дальше он уходит в onUnanswered или в зону, как
провал тела отменённой задачи. ignoreFailure() на задаче останавливает его
на onError.
Два узких случая описаны в dartdoc JobBase.finish:
Done, поданный веткеrunAllпосле того, как группа услышала о провале тела.- Один
Cancelled, поданный двум задачам черезfinishилиcancelWith. ПоэтомуonSignOutиз раздела «Не дожидаясь контрольной точки» страницы о движке строит новыйCancelledпри каждом вызове.
Cancelled, который бросает тело или правило из check(), может быть общим,
в том числе const: задача кончается тем, который ядро строит из него.