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

Уборка

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

final lock = await ctx.join(Lock.acquire, dispose: (lock) => lock.release());
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
await ctx.join(() => database.migrate());
return database;
Что задача держит Член Когда выполняется
То, что она оставляет себе, из вызова: блокировка, временный файл, подписка dispose у abandonable, join или run при любом исходе
То, что тело возвращает или отдаёт наружу, из вызова discard у abandonable, join или run если задача кончилась не Done
То, что она оставляет себе, когда оборачивать нечего ctx.onDispose при любом исходе
То, что тело возвращает или отдаёт наружу, когда оборачивать нечего ctx.onDiscard если задача кончилась не Done
Значение из abandonable, join или run, которое переходит в другие руки ctx.disown(value) никогда: снимает регистрацию, которую сделал вызов

Если передать в один вызов и dispose, и discard, будет ArgumentError.

Эта страница выбирает между dispose и discard и закрывает значение, которое задача не отдаёт. «Правила уборки» продолжают отсюда: ресурс, который передают из задачи в задачу, ctx.onDispose и ctx.onDiscard, когда оборачивать нечего, порядок уборки и значение, которое приходит поздно.

Строки под кодом ниже — то, что он печатает при запуске. Блокировка и база сообщают, что с ними происходит, а outcome: — то, чем задача кончилась.

Задача берёт блокировку для своей работы и открывает базу, которую возвращает тому, кто её попросил. Блокировка нужна работе задачи, база нужна вызывающему, если до него дойдёт; ни то, ни другое не должно жить дольше, чем нужно. Задача завершается успешно.

Оба ресурса уходят вместе с одной задачей, поэтому один и тот же колбэк выглядит подходящим для обоих:

// Первая попытка: при успехе блокировка не освобождается.
final lock = await ctx.join(Lock.acquire, discard: (lock) => lock.release());
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
return database;
lock acquired
database opened
outcome: Done(database)

Блокировка так и не освобождается. Вот что происходит:

  1. discard выполняется, только когда задача кончилась не Done.
  2. Задача, которая возвращает базу, кончается Done(database), поэтому ни один discard не выполняется.
  3. База остаётся открытой для вызывающего, как и должна, а блокировка остаётся захваченной на всё время жизни процесса.

Отменённая или упавшая задача освобождает и то, и другое; блокировка утекает только на том пути, ради которого задача и написана. Тест, который отменяет задачу, ничего не заметит: на этом пути колбэк как раз выполняется.

final lock = await ctx.join(Lock.acquire, dispose: (lock) => lock.release());
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
return database;
lock acquired
database opened
lock released
outcome: Done(database)
Ресурс Колбэк Задача завершилась успешно Задача упала или отменена
То, что задача оставляет себе: блокировка, временный файл, подписка dispose освобождается освобождается
То, что тело возвращает или отдаёт наружу discard передаётся открытым освобождается

Ветка ctx.runAll освобождает позже, чем кончается её тело, поэтому блокировка, которую делят две ветки, может подвесить группу. Как выглядит зависание и как брать такую блокировку, показывает раздел «Ветки, которые ждут друг друга» страницы о группах.

Каждый колбэк ожидается, поэтому задача кончается не раньше последнего из них. Кто ждёт задачу, ждёт и их: контроллер solo завершает close() только после уборки задачи, которую отменил.

Задача держит зарегистрированные колбэки на своём стеке уборки. Они выполняются после конца тела, и отмена их не прерывает: ни та, что уже принята, ни та, что придёт в саму размотку. check, abandonable, join, pause, uncancellable, run, start, runAll, each и onCancel принадлежат телу и здесь бросают StateError, поэтому колбэк должен ждать свой ресурс напрямую. В отменённой задаче check бросает вместо него её Cancelled, как и после конца задачи. Поэтому брошенное телом действие, которое опрашивает check, чтобы остановиться, получает ту отмену, которой ждёт.

Два значения освобождаются, не дожидаясь стека: то, на котором бросает контрольная точка в момент его прихода, и то, которое приходит поздно. Оба случая — на странице о правилах уборки, в разделах «Регистрация по приходу» и «Значение, которое приходит поздно».

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

Задача открывает базу для вызывающего и отдаёт её, только если схема актуальна. Иначе она возвращает null, и вызывающий сначала делает миграцию. В этот раз схема устарела.

// Первая попытка: база, которую задача не отдаёт, остаётся открытой.
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
if (!await ctx.join(database.isCurrent)) {
return null;
}
return database;
database opened
outcome: Done(null)

База так и не закрывается, и её никто не держит. Вот что происходит:

  1. discard выполняется, только когда задача кончилась не Done.
  2. return null кончает задачу Done(null). discard смотрит, чем кончилась задача, а не куда ушла база, поэтому не выполняется.

Отладочный канал, который включается строкой Job.debug = print;, называет значение, без которого задача кончилась:

Job() ended Done(null) without the value of 1 conditional cleanup (database): unless it is inside the result, nothing will dispose of it
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
if (!await ctx.join(database.isCurrent)) {
ctx.disown(database);
await database.close();
return null;
}
return database;
database opened
database closed
outcome: Done(null)

Тело, которое не отдаёт значение, закрывает его само, а регистрацию сначала забирает назад. Пока база закрывается, может прийти отмена и кончить задачу Cancelled; без ctx.disown discard тогда закрыл бы базу второй раз. ctx.disown ищет значение по идентичности и возвращает, нашёл ли его, поэтому передавайте ему ровно тот объект, который вернул вызов.