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

Ресурсы и освобождение

Job может открыть базу данных, создать подписку или получить другой ресурс. Регистрируйте освобождение в момент получения, чтобы никакая отмена не оставила ресурс позади:

Job<void> load() => run<Idle, void>(
key: 'load',
(ctx) async {
final db = await ctx.join(
Database.open,
dispose: (db) => db.close(),
);
final rows = await ctx.join(db.readAll);
ctx.emit(Loaded(rows));
},
);

wait и join принимают колбэк dispose для ресурса, который принадлежит Job. Здесь база принадлежит операции загрузки, а состоянием контроллера становятся только её строки, поэтому dispose закрывает её при успехе, ошибке и отмене — в том числе когда отмена вовсе не даёт полученному значению дойти до тела. Database, Idle и Loaded(rows) относятся к модели этого приложения.

Для ресурсов, полученных другим способом, ctx.onDispose(cursor.close) регистрирует колбэк освобождения. Он возвращает функцию, которую можно сохранить как removeDisposer, чтобы снять регистрацию без вызова колбэка. Регистрируйте каждое освобождение один раз: добавление ctx.onDispose(db.close) в пример закрыло бы одну базу дважды.

Job ждёт детей, затем выполняет зарегистрированную уборку в обратном порядке регистрации, ожидая каждый колбэк. Уборка заканчивается до итогового обработчика состояния, доставки исхода и освобождения очереди. Обычный try/finally остаётся полезен для локальной работы; зарегистрированная уборка также охватывает время после возврата из тела и до завершения его детей.

// База — результат этой Job, и при успехе ею владеет получатель.
SoloJob<Database> open() => run<Idle, Database>(
key: _Op.open,
(ctx) async {
final db = await ctx.join(
Database.open,
discard: (db) => db.close(),
);
await ctx.run(job<Idle, void>((child) => child.join(db.readAll)));
return db;
},
);

Используйте discard вместо dispose, когда ресурс является результатом Job и при успехе перейдёт во владение получателя. Соответствующий метод регистрации называется ctx.onDiscard. Эти колбэки вызываются, когда Job заканчивается без успешной передачи результата. Они не освобождают ресурс, оставшийся только внутри успешного тела; для этого нужен dispose. Метод ожидания принимает либо dispose, либо discard, но не оба сразу.

Ребёнок в примере и объясняет, зачем это различие: тело уже вернуло базу, но Job ещё не завершена. Если во время этого ожидания придёт отмена, вызывающий код получит Cancelled вместо ресурса, а discard освободит его.

При передаче зарегистрированного ресурса прямо в состояние сначала проверьте Job и удалите регистрацию ресурса через disown. Для базы, полученной с dispose, и состояния, которое примет её во владение:

ctx
..check()
..disown(db)
..emit(Ready(db));

Эти вызовы синхронные. Если check бросит исключение, база останется в ведении уборки. Если emit запишет состояние и затем бросит исключение из-за отмены Job синхронным слушателем, база уже принадлежит состоянию и больше не зарегистрирована для освобождения Job.

Для асинхронной передачи поместите передачу и disown в одну ожидаемую секцию ctx.uncancellable. Внутри секции дождитесь передачи обычным await и сразу вызовите disown. Контрольная точка контекста между ними может бросить отмену по правилам состояния после смены владельца, но до удаления регистрации освобождения.

Метод ожидания решает, когда произойдёт освобождение, а не произойдёт ли оно вообще:

// Брошено при отмене: файл может прийти уже после завершения Job.
// Он всё равно будет удалён, но этого никто не ждёт.
await ctx.wait(openTemp, dispose: (file) => file.delete());
// Удалён до старта следующей Job и до возврата из close().
await ctx.join(openTemp, dispose: (file) => file.delete());

Используйте join, когда освобождение ресурса должно предшествовать следующей Job или закрытию контроллера.

Отмена может прийти после возврата из тела, в том числе во время уборки. Если регистрация discard была пропущена при успешном исходе, она выполняется вторым проходом, когда отмена делает её необходимой. В таком случае она выполняется после уборщиков, обработанных позже, а не в строго обратном порядке.

Ошибки уборки передаются хукам ошибок и наблюдателям; если обработчиков нет, они попадают в зону. Cancelled из уборки не сообщается как ошибка. Не ждите done, value или cancel() этой же Job внутри её уборки и не ждите следующую Job в той же очереди: все они зависят от завершения текущей уборки.