Перейти к содержимому

Ошибки и наблюдение

Обработчики состояния и хуки диагностики решают разные задачи. run(onError: ...) вычисляет состояние после ошибки Job. Метод контроллера onError и SoloObserver.onError получают ошибки для логирования или отправки, включая ошибки уборки и брошенных операций.

Контроллер может переопределить onStart, onFinish, onError, onLog и onChange. Например, добавьте хук ошибок в контроллер профиля:

final class ProfileController extends Solo<ProfileState> {
final ProfileApi api;
ProfileController(this.api) : super(const Initial());
// ...задачи из быстрого старта...
@override
void onError(Job<Object?> job, Object error, StackTrace stackTrace) {
reportCrash(error, stackTrace);
}
}

SoloObserver получает те же события от всех контроллеров, а также onCreate и onClose. Установите его при запуске приложения:

final class LoggingObserver extends SoloObserver {
@override
void onStart(SoloBase<Object> solo, Job<Object?> job) =>
print('$job started');
@override
void onFinish(SoloBase<Object> solo, Job<Object?> job) =>
print('$job finished ${job.outcome}');
@override
void onChange(SoloBase<Object> solo, SoloTransition<Object> transition) =>
print('${transition.job ?? 'external'}: ${transition.current}');
}
void main() {
SoloBase.observer = LoggingObserver();
}

Хук изменения получает SoloTransition: состояние до и после, job, чей emit его сделал — null для externalSetState, а у дочерней задачи это она сама, а не корневая, которой она принадлежит, — и revision, растущий на единицу за изменение, так что два перехода упорядочены даже когда хук поменял состояние ещё раз изнутри первого. Он отвечает на вопрос, кто изменил состояние, а снаружи движка это не выводится.

Наблюдатель вызывается перед соответствующим хуком контроллера. Каждый вызов независим; отсутствие super в хуке контроллера не отключает наблюдателя. Ошибка любого из них передаётся в текущую зону Dart, не меняя исход Job, не останавливая очередь и не препятствуя вызову второго хука.

Наблюдатель только наблюдает. Его наличие никак не влияет на то, куда дальше пойдёт ошибка. Чтобы отвечать за ошибки, которым больше некуда идти, поставьте обработчик:

SoloBase.errorHandler = (solo, job, error, stackTrace) =>
Sentry.captureException(error, stackTrace: stackTrace);

Один обработчик на процесс, ставится один раз при старте. Если он есть, такие ошибки идут к нему вместо зоны; если его нет — в зону. Разделение намеренное: отвечать за ошибку — это ответственность, которую берут на себя, а не побочный эффект включения лога.

close() ждёт работающую Job, а Job может не торопиться. Контроллер говорит, чего именно он ждёт:

unawaited(controller.close().timeout(
const Duration(seconds: 5),
onTimeout: () => log('closing is held by ${controller.pending}'),
));

SoloPending называет Job, её phase — тело, дети, уборка, — несомую отмену, открыта ли секция ctx.uncancellable, придерживающая отмену, и создана ли Job с cancellable: false, то есть отклоняет ли она отмены.

Он сообщает, а не ставит диагноз. Долгое ожидание не доказывает забытый ctx.wait: тело внутри внешнего вызова выглядит так же, и неторопливое освобождение ресурса тоже. Чего движок не видит, то SoloPhase.unknown, а не догадка.

Обращение к job.done или job.value, а также вызов job.ignore() помечают исход наблюдённым. Исход Failed, который никто не наблюдает, передаётся в зону создания Job через Zone.handleUncaughtError в дополнение к хукам ошибок. Само наличие наблюдателя не помечает исходы наблюдёнными. Если сообщения об ошибках обрабатываются в другом месте, а вызывающему коду не нужен результат:

profile.load().ignore(); // аналог Future.ignore

Ошибки уборки, колбэков отмены и операций, брошенных через wait, передаются хукам диагностики. Без переопределённого хука ошибок или установленного SoloBase.errorHandler они попадают в зону создания Job. Такая ошибка может прийти уже после завершения Job. Она не заменяет существующий исход отмены. Сам Cancelled исключён из этих маршрутов сообщений об ошибках.

Неперехваченная ошибка job.value или ctx.run(child) остаётся необработанной ошибкой Future по правилам Dart, даже если это Cancelled. Обрабатывайте эти future через await, catchError или ignore() в зависимости от ситуации.

Cancelled реализует Exception, поэтому широкий catch ловит и отмену. Если телу нужна собственная обработка ошибок, сначала пропустите отмену дальше. Этот фрагмент обрабатывает ошибку камеры, сохраняя отмену:

try {
await ctx.join(hw.open);
await ctx.join(() => hw.setZoom(zoom));
} on Cancelled {
rethrow;
} on Object catch (error) {
ctx.emit(Broken(error));
rethrow;
}

После принятой отмены исход Job остаётся Cancelled, даже если тело её перехватило. Но блок catch всё ещё может выполнить ненужную работу, например повторить операцию. Для итогового состояния ошибки предпочтителен параметр onError у run, а не коррекция внутри широкого catch.

// Правило отвечает; чтобы отказать, оно не бросает исключение.
canStart: (state) => state.free > 0,

Где правило всё-таки бросило, решает, кто об этом узнает:

Где бросило Что происходит
Стартовое правило Job проваливается, очередь продолжает работать.
Правило в контрольной точке контекста Ошибку получает тело.
Переоценка после обновления состояния Передаётся для диагностики; работающее тело сама по себе не отменяет.
Проверка, управляющая итоговым обработчиком состояния Обработчик запрещается.

Когда за ошибки переоценки не отвечает ни хук ошибок, ни SoloBase.errorHandler, они попадают в зону создания контроллера. В корневой зоне Dart необработанная ошибка может завершить приложение. Настройте сообщения об ошибках и наблюдение исходов в соответствии с требованиями приложения.

// Работа со своей жизнью: Job её не ждёт и не отменяет, а ошибки всё
// равно доходят до хуков этой Job.
ctx.unattended(() => analytics.send('zoom'));
// Данные приложения для хуков логирования и наблюдателей.
ctx.log(('zoom', zoom));
// И собственная трасса движка, когда нужно посмотреть на очередь.
SoloBase.debug = print;

ctx.unattended(action) запускает работу, которую Job не ждёт и не отменяет. Её ошибки передаются хукам ошибок Job даже после завершения Job; без обработчика действует тот же переход в зону. Используйте его для работы с независимым временем жизни. Запуск детей из этой работы запрещён. Захваченный контекст всё ещё принадлежит исходному Job: emit может работать, пока Job активен, но отклоняется после отмены или завершения. Фоновая операция не продлевает время жизни контекста. Обычный unawaited(future) не предоставляет маршрутизацию ошибок, которую даёт unattended.

ctx.log(data) передаёт данные приложения хукам логирования и наблюдателям как есть, поэтому строку делает тот слушатель, которому она нужна. SoloBase.debug дополнительно трассирует внутренние операции очереди и жизненного цикла контроллера.