Наблюдение
Исход задачи говорит, чем задача кончилась. JobObserver слышит остальное:
когда задача начинается и кончается, что её тело пишет в лог, и ошибки, которых
не несёт ни один исход. Куда эти ошибки уходят и как наблюдатель за них
отвечает, рассказывает страница
«Ошибки».
Наблюдатель
Заголовок раздела «Наблюдатель»Задача загружает число. Её наблюдатель печатает, чем задача кончилась и что тело записало в лог:
import 'package:async_job/async_job.dart';
final class Log extends JobObserver { @override void onFinish(Job<Object?> job) => print('$job: ${job.outcome}');
@override void onLog(Job<Object?> job, Object? message) { final data = message is Object? Function() ? message() : message; print('$job: $data'); }}
final job = Job<int>(key: 'load', observer: Log(), (ctx) async { ctx.log('loading'); return ctx.abandonable(load);});Job(load): loadingJob(load): Done(3)Хуки наблюдателя и когда задача их вызывает:
| Хук | Когда вызывается |
|---|---|
onStart |
тело начинается; только у задачи, которая стартовала |
onFinish |
у задачи есть исход; у каждой задачи, даже у отменённой до старта |
onError |
задача ловит ошибку, кроме собственной отмены |
onLog |
тело вызывает ctx.log(message) |
onUnanswered |
у наблюдателя, который подмешивает JobAnswerer: провал, которого не несёт ни один исход, после onError, и Failed, который никто не наблюдал, на микротаске после конца задачи; см. «Ответ за ошибки» |
Четыре хука JobObserver по умолчанию ничего не делают, поэтому наблюдатель
переопределяет только те, что ему нужны. onUnanswered у JobAnswerer
по умолчанию не пуст: он отдаёт провал зоне,
как если бы JobAnswerer не было. Класс, который уже наследует другой класс,
подмешивает наблюдателя через with JobObserver или
with JobObserver, JobAnswerer и так же переопределяет хуки.
Наблюдателя передаёт при создании задачи тот, кто её запускает. Дочерние
задачи, которые она запускает, наследуют его, если у них нет своего.
Продолжение, созданное через then, получает только наблюдателя, переданного
в then.
Если хук бросит исключение, оно попадёт в текущую зону, и больше ничего
не изменится: задача кончится так же, как кончилась бы, а остальные хуки всё
равно будут вызваны. Cancelled, брошенный хуком, в зону не попадает и задачу
не отменяет. Чтобы отменить задачу из хука, вызовите job.cancel(): задача
отменится так же, как при вызове из любого другого места. Один хук добавляет
шаг: onUnanswered, который бросил, сначала отдаёт в зону ошибку, за которую
его просили ответить, как показано в разделе
«Ответ за ошибки».
Имя задачи
Заголовок раздела «Имя задачи»При создании задаче можно дать ключ key и колбэк describe. Из них
складывается строковое представление задачи — имя, которое печатают строки
наблюдателя:
| Дано | Строковое представление |
|---|---|
key |
Job($key) |
key и describe, который возвращает непустое описание |
Job($key: $description) |
Такой describe без key |
Job($description) |
| Ни того, ни другого | Job() |
Используйте key, чтобы обозначить задачу в логе или в правилах планирования
библиотеки, например в политиках очереди solo. describe добавляет
к обозначению подробности.
Имя попадает всюду, где называется задача: в строки наблюдателей и отладочного
канала и в сообщения ядра об ошибках. Кладите в key и describe
идентификатор, а не персональные данные: номер пользователя, а не адрес его
почты.
Задачи, которые ядро создаёт за вас, тоже принимают key и describe: это
продолжение then, ручка shared и ребёнок ctx.each. Без describe каждую
описывает член, который её создал, поэтому продолжение с одним key печатается
как Job(load: then), а без того и другого — как Job(then).
Если бросает toString ключа или сам describe, на его месте печатается
метка: Job(load: <describe threw StateError>). Ядро называет задачи
в собственных сообщениях об ошибках, и сломанное имя не занимает место
сообщения. Саму ошибку имени ядро всё равно сообщает, в двух местах. Когда ядро
называет дочернюю задачу в отмене родителя, ошибка идёт путём ошибки вне тела,
как показывает таблица в разделе
«Редкие маршруты». Когда задачу называет
строка отладочного канала, ошибка идёт
в текущую зону, как ошибка хука.
Время после отмены
Заголовок раздела «Время после отмены»От отменённой задачи ждут, что она скоро кончится. Та, что работает дальше, занята делом, от которого пользователь уже отказался. Наблюдатель находит такие задачи, засекая, сколько каждая проработала после отмены:
final class SlowCancellations extends JobObserver { final _since = Expando<Stopwatch>('cancellation');
@override void onStart(Job<Object?> job) => job.whenCancelled((_) => _since[job] = Stopwatch()..start());
@override void onFinish(Job<Object?> job) { final watch = _since[job]; if (watch != null) { print('$job ran ${watch.elapsedMilliseconds} ms past its cancellation'); } }}Job.whenCancelled, зарегистрированный в onStart, срабатывает, когда задача
принимает отмену. onFinish срабатывает, когда приходит исход. Время между
ними показывает, сколько задача проработала после отмены, вместе с дочерними
задачами и уборкой. Счёт определяют три вещи:
- Если тело ждёт что-то долгое голым
await, остаток этого ожидания попадает в счёт. Тот же вызов черезctx.abandonableпоказывает 0 мс. - Счёт идёт с момента, когда задача приняла отмену, а не с вызова
cancel(): ни один хук не слышит, что отмену попросили.ctx.uncancellableпридерживает отмену, и задача принимает её, когда секция кончится: секция на 100 мс, отменённая на 10-й миллисекунде, показывает 0 мс, хотя вызвавшийcancelждал 90 мс. - Тело, которое отменяет само себя, бросая
Cancelledили выпуская наружу отмену дочерней задачи, принимает отмену в момент броска, и счёт идёт оттуда.
Сообщение для лога
Заголовок раздела «Сообщение для лога»Тело мигрирует базу. Когда миграция падает, оно пишет в лог пойманную ошибку
error. Тот, кто отлаживает приложение, даёт задаче наблюдателя. В релизной
сборке его нет, и там сообщение не должно стоить ничего.
Первая попытка
Заголовок раздела «Первая попытка»log принимает сообщение, и строка годится в сообщение:
// Первая попытка: строка собирается впустую.ctx.log('migration failed: $error');Без наблюдателя ctx.log ничего не делает, но строка к этому моменту уже
построена: Dart вычисляет аргумент до вызова, поэтому error.toString()
выполняется впустую.
Форматирование остаётся наблюдателю
Заголовок раздела «Форматирование остаётся наблюдателю»ctx.log(() => 'migration failed: $error');Колбэк откладывает построение сообщения. ctx.log передаёт колбэк дальше без
изменений, как любой другой объект, поэтому без наблюдателя ничего не строится.
Если наблюдатель есть, то вызовет ли он колбэк, зависит от его соглашения:
Log выше вызывает колбэк и печатает то, что тот вернул, а наблюдатель,
который печатает каждое сообщение как пришло, напечатал бы сам колбэк. Если
передать один error, форматирование тоже останется наблюдателю, и никакого
соглашения не нужно.