Правила уборки
«Уборка» выбирает между dispose и discard и говорит, что
может колбэк. Эта страница ведёт правила дальше: ресурс, который передают
из задачи в задачу, колбэк, зарегистрированный без вызова, и то, что встречает
уборка задачи после возврата из тела.
Строки под кодом — то, что он печатает при запуске. База, её кэш и курсор
сообщают, что с ними происходит, cancel — момент, когда пользователь отменяет
задачу, а outcome: — то, чем задача кончилась.
Ресурс, который передают дальше
Заголовок раздела «Ресурс, который передают дальше»Ресурс может открыть одна задача, а получить другая: connect открывает базу,
ready делает на ней миграцию и передаёт базу дальше. У того, кто держит базу
последним, должна остаться возможность её закрыть.
Первая попытка
Заголовок раздела «Первая попытка»connect зарегистрировала освобождение, поэтому база выглядит прикрытой,
куда бы она ни попала. Миграция падает:
// Первая попытка: discard не уходит со значением, и база не закрывается.final connect = Job.deferred<Database>( key: 'connect', (ctx) => ctx.abandonable(Database.open, discard: (database) => database.close()),);
final ready = Job.deferred<Database>((ctx) async { final database = await ctx.run(connect); await ctx.join(() => database.migrate());
return database;});database openeddatabase migration failedoutcome: Failed(MigrationFailed)База остаётся открытой, а закрыть её некому. Вот что происходит:
connectкончаетсяDone: она передала своё значение, поэтому еёdiscardотбрасывается вместе со значением.discardрегистрирует условно. Такую регистрацию решает исход той задачи, которая её сделала, а не той, к которой уходит значение, и получателю её не несёт ничто.- Миграция падает, и
readyкончается с базой на руках и без собственной регистрации.
Единственное исключение из второго пункта, ветка ctx.runAll, описано
в разделе «Отмена после возврата из тела».
Отладочный канал, который включается строкой Job.debug = print;, называет
каждую передачу, которая отбросила регистрацию, зарегистрировал получатель
что-нибудь или нет, так что места, где действует это правило, читаются прямо
из прогона. Для connect выше строка такая:
Job(connect) handed its value over: 1 conditional cleanup droppedВторая попытка
Заголовок раздела «Вторая попытка»Получатель регистрирует полученное на той строке, где он его получает:
// Вторая попытка: контрольная точка в run может потерять значение раньше.final ready = Job.deferred<Database>((ctx) async { final database = await ctx.run(connect); ctx.onDiscard(database.close); await ctx.join(() => database.migrate());
return database;});Упавшая миграция теперь закрывает базу. Но пользователь отменяет ready как
раз тогда, когда connect кончилась, и база снова теряется:
database openedcanceloutcome: Cancelled(manual)Вот что происходит:
connectкончаетсяDoneи по дороге отбрасывает свою регистрацию.- Со значением ребёнка на руках
runпроверяет родителя: не отменён ли он, а когда задачей управляет движок, построенный на ядре, напримерsolo, то и правила этого движка. Проверка бросает исключение, здесь это отмена, и значение уходит вместе с ним. - До строки с
onDiscardдело не доходит, и базу не закрывает никто.
Регистрация по приходу
Заголовок раздела «Регистрация по приходу»final ready = Job.deferred<Database>((ctx) async { final database = await ctx.run( connect, discard: (database) => database.close(), ); await ctx.join(() => database.migrate());
return database;});Та же отмена:
database openedcanceldatabase closedoutcome: Cancelled(manual)Переданная в run, уборка забирает значение в момент его прихода, и отнять его
контрольная точка уже не может. Если точка бросает, run закрывает базу
на месте и ждёт закрытия, прежде чем тело услышит отмену; если проходит,
регистрация ложится на стек ready. Теперь ready закрывает базу, когда
заканчивается чем угодно, кроме значения, а когда завершается успешно, передаёт
её нетронутой получателю, который регистрирует её так же. Кто закроет базу,
решает момент отмены:
Когда отменяют ready |
Вторая попытка | Регистрация по приходу |
|---|---|---|
Пока connect открывает базу |
connect, отменённая каскадом |
connect, отменённая каскадом |
После того как connect кончилась Done, но до того, как run отдаст значение телу |
никто | ready |
Пока базу открывает connect с cancellable: false |
никто: connect всё равно кончается Done |
ready |
Это верно для любой передачи, включая группу: ветка ctx.runAll регистрирует
по приходу каждое значение, полученное от своих детей, как и все. Контрольная
точка ctx.join после действия так же освобождает значение, что бы она
ни бросила, включая правило движка, которое бросило исключение.
Закрывает базу получатель, тот, кто зарегистрировал её по приходу. То же
значение остаётся доступным через Job.value ребёнка, пока кто-нибудь держит
хэндл, но если закрыть его оттуда, оно закроется второй раз. Когда значение
не доходит ни до какого получателя, закрывает тот, у кого хэндл. Один такой
случай — ветка, отказавшаяся от остановки группы (раздел
«Что группа отдаёт обратно»); другой —
цепочка, чей then отменён в ожидании (страница «Цепочки»).
Регистрация без вызова
Заголовок раздела «Регистрация без вызова»Там, где оборачивать нечего, колбэк регистрируют напрямую: ctx.onDispose
и ctx.onDiscard. Правила исхода у них те же, а колбэк может делать
и остальную завершающую работу, например сбрасывать буфер на завершении задачи:
final buffer = StringBuffer();ctx.onDispose(() => sink.add(buffer.toString()));Оба метода возвращают функцию, снимающую регистрацию. Пользуйтесь ей, когда
ресурс уже освобождён или передан; вызвать её повторно или после выполнения
уборки безопасно. abandonable, join и run возвращают сам ресурс,
а не функцию снятия, и сделанную ими регистрацию снимает ctx.disown(value).
Он ищет значение по идентичности и возвращает, нашёл ли его. Поэтому
передавайте ровно тот объект, который вернул вызов.
Первая попытка
Заголовок раздела «Первая попытка»readAll закрывает курсор, когда прочитает всё. Задача регистрирует закрытие,
чтобы курсор был прикрыт, пока его не закроет чтение, и снимает регистрацию
после чтения. Пользователь отменяет задачу во время чтения:
// Первая попытка: отмена во время readAll закрывает курсор дважды.final removeDisposer = ctx.onDispose(cursor.close);await ctx.join(() => cursor.readAll()); // в конце курсор закрываетсяremoveDisposer();cancelcursor readcursor closedcursor closedoutcome: Cancelled(manual)Курсор закрывается дважды. Вот что происходит:
joinждётreadAll, и тот читает и закрывает курсор.- Задачу отменяют во время чтения, и
joinбросает отмену вместо значения, так что строка, которая снимает регистрацию, не выполняется. - Регистрация остаётся на месте, и уборка закрывает курсор во второй раз.
Снятие внутри действия
Заголовок раздела «Снятие внутри действия»final removeDisposer = ctx.onDispose(cursor.close);await ctx.join(() async { await cursor.readAll(); // в конце курсор закрывается removeDisposer();});cancelcursor readcursor closedoutcome: Cancelled(manual)Внутри действия эти два шага не разнести: что бы вызов ни бросил после,
освобождение и снятие регистрации уже случились вместе. То же верно для
значения, зарегистрированного самим вызовом: ctx.disown(value) стоит там, где
меняется владение, а не строкой ниже.
Асинхронная передача и её ctx.disown идут вместе внутри одной ожидаемой
секции ctx.uncancellable:
final transaction = await ctx.join( Transaction.begin, dispose: (transaction) => transaction.rollback(),);await ctx.uncancellable(() async { await transaction.commit(); // голый await, а не ctx.join ctx.disown(transaction);});Шаг — голый await, а не ctx.join, и между ним и ctx.disown не стоит
ничего, что бросает отмену задачи: ни члена контекста, ни await child.value.
Ведь отмену, которую создают правила движка, секция не придерживает.
Регистрация после обычного await
Заголовок раздела «Регистрация после обычного await»Регистрация работает и после обычного await. Обычный await, в отличие
от вызова ctx.run из раздела «Вторая попытка», контрольной
точкой не служит: между вызовом и строкой под ним бросить нечему. Пользователь
отменяет задачу, пока база открывается:
final job = Job<Database>((ctx) async { final database = await Database.open(); ctx.onDiscard(database.close);
return database;});canceldatabase openeddatabase closedoutcome: Cancelled(manual)Отмена, пришедшая во время Database.open(), прервать его не может. Тело
дожидается вызова, регистрирует открытое, и задача заканчивается Cancelled;
onDiscard закрывает базу, потому что значение не дошло ни до какого
вызывающего. Именно такое ожидание называет правило bare_await пакета
async_job_lints: регистрация после него надёжна, но задача кончается
не раньше вызова. Где тело ждёт через ctx.abandonable, ctx.join или
ctx.run, отдайте колбэк этому вызову, как в разделе
«Регистрация по приходу». Каждый из них служит
контрольной точкой, и строка под ним может не выполниться никогда.
Порядок уборки и поздние результаты
Заголовок раздела «Порядок уборки и поздние результаты»Тело вернулось, а дети и уборка у задачи ещё впереди. Она проходит их в таком порядке:
| Шаг | Что делает задача |
|---|---|
| Дети | ждёт их всех: они могут пользоваться её ресурсами |
| Стек уборки | разматывает его с последней регистрации, ожидая каждый колбэк |
| Вторая волна | выполняет discard, который отмена сделала нужным задним числом |
| Конец | исход и освобождение того, что держит движок |
Что может колбэк, пока выполняется, — в конце раздела
«dispose для того, что задача оставляет себе».
Отмена после возврата из тела
Заголовок раздела «Отмена после возврата из тела»Задача может ещё ждать детей или выполнять уборку после return. Тело
возвращает базу, пока ребёнок ещё прогревает кэш, и в этот момент пользователь
отменяет задачу:
final job = Job<Database>((ctx) async { final database = await ctx.join( Database.open, discard: (database) => database.close(), ); // Кэш ждёт не тело, а задача. ctx.start(Job.deferred<void>((c) => c.join(database.warmCache)));
return database;});database openedcancelcache warmeddatabase closedoutcome: Cancelled(manual)Отмена, пришедшая в это время, меняет исход на Cancelled, и база,
зарегистрированная через discard, закрывается вместо того, чтобы вернуться
вызывающему. Здесь отмена приходит, пока задача ждёт кэш, поэтому discard
выполняется на своём месте в стеке. Если же отмена приходит в размотку, когда
стек уже прошёл мимо discard, тот выполняется второй волной. Вторая волна
ставит этот discard после колбэков, зарегистрированных раньше него,
а не в строгом обратном порядке регистрации.
Единственное исключение составляет ветка ctx.runAll. После того как группа
отдала значения вызывающему, discard этой ветки больше не выполняется:
ни второй волной, ни на отмене, пришедшей в саму размотку, ни как-либо ещё.
Значение в руках вызывающего, а discard означает, что значение не досталось
никому. Ради этого группа и держит ветку, тело которой вернуло значение. Держит
она её, пока все ветки не вернут значения и не размотают свои стеки уборки или
пока тело одной из них не кончится чем-то другим. Исхода до тех пор у ветки
нет. Пока группа держит ветку, всё сказанное выше верно как написано: отмена,
дошедшая до ветки, закрывает то, что ветка взяла.
Значение, которое приходит поздно
Заголовок раздела «Значение, которое приходит поздно»Значение, которое вернуло действие, брошенное abandonable, нуждается в уборке
при любом исходе: до тела оно не дошло. Когда выполняется колбэк для него,
зависит от того, когда пришло значение:
| Когда приходит значение | Когда выполняется колбэк |
|---|---|
Пока идёт тело, пока задача ждёт детей или пока ветка ctx.runAll ждёт свою группу перед своим стеком уборки |
сразу, параллельно с остальной задачей |
| Пока задача выполняет стек уборки | в стеке, после колбэка, который выполняется сейчас |
Пока ветка ctx.runAll ждёт свою группу после своего стека уборки |
сразу, после стека, и освобождения ждёт только конец ветки |
| После конца задачи | сразу, поздно и в одиночку, и этого никто не ждёт |
| Пока идёт тело, в вызов, брошенный телом без отмены | в стеке, вместе с размоткой |
Размотки этот колбэк не ждёт. Ребёнку, которого задача ещё ждёт, может понадобиться та же блокировка или тот же слот пула, и значение, отложенное до размотки, не отдало бы их ребёнку никогда. Освобождение, начатое до конца задачи, не даёт ей кончиться, пока не закончится само.
До стека, в первой строке, освобождение идёт параллельно с остальной задачей: с телом, детьми и освобождением другого такого значения. Из колбэков уборки так перекрывается только колбэк позднего значения.
В стеке, во второй строке, освобождение занимает место колбэка, зарегистрированного в момент прихода значения. Колбэк, зарегистрированный позже, может выполняться, пока освобождение ещё идёт. Зарегистрированный раньше, например закрытие пула, из которого взято значение, выполняется только после конца освобождения.
После стека ветки, в третьей строке, освобождение тоже идёт сразу. Ветка
ctx.runAll ждёт группу дважды: перед своим стеком уборки и после него, пока
свои стеки не размотают все ветки. Ко второму ожиданию стек уже размотан,
включая закрытие пула, и освобождения ждёт только конец ветки. Если бы значение
ждало второй волны, оно держало бы слот, нужный уборке другой ветки, которую
ждёт группа, и группа ждала бы вечно.
В последней строке значение не позднее: тело бросило вызов без отмены, поэтому
значение всё равно достаётся тому, кто держит future вызова, а его освобождение
ждёт размотки на стеке. Ядро считает значение поздним с того момента, как
узнаёт о конце тела, а не с его return, поэтому вызов, который выдаёт ресурс,
бросать не стоит.
У вызова, который нарочно оставляют без await, тип пишут явно:
unawaited( ctx.abandonable<Database>( Database.open, dispose: (database) => database.close(), ),);Без <Database> тип выводится из unawaited как void, и dispose
приходится принимать void.