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

Список изменений

  • README стал коротким входом с разделом о ctx.abandonable: «Зачем» и «Развёрнутый пример» переехали на «Зачем задача», а таблица контекста и «Четыре слова» — в «Справочник».
  • README называет в разделе «Установка» плагин анализатора async_job_lints с правилами для кода на async_job; код на solo называет solo_lints, как говорит его раздел «solo».
  • «Срок для одного шага» на странице о времени первым в ветке, которая ловит срок ребёнка, вызывает ctx.check(). Без него задача, у которой собственный timeout вышел до ctx.run, принимала свой Cancelled(timeout) за срок ребёнка и шла дальше: причина срока не говорит, чей он.
  • ctx.wait, устаревший с 0.3.0 в пользу ctx.abandonable, удалён.
  • HandlerCancelReason переименован в BodyCancelReason, а его name, который печатают исходы и трассы, теперь body, а не handler. Эта причина говорит, что тело отменило себя само, прямо или выпустив отмену дочерней задачи.
  • Job.debug печатает awaited before start, когда done или value у Job.deferred впервые читают, пока её никто не запустил: ожидание такой задачи не кончится, пока её не запустят или не отменят.
  • README говорит, в каком выпуске с версии 1.0.0 выходит каждое изменение: исправление в патч-выпуске, добавление в минорном, ломающее в мажорном. Протокол движка из engine.dart в это обещание не входит и может измениться в минорном выпуске; Своё поверх ядра велит движку держать async_job в пределах одной минорной версии.
  • README называет плагин анализатора solo_lints в разделе «Установка».
  • Описание пакета и первый абзац README говорят, для чего он, словами, которыми его ищут: отмена асинхронной работы и структурная конкурентность.
  • Страница о наблюдении говорит, что имя задачи попадает и в сообщения ядра об ошибках, поэтому в key и describe кладут идентификатор, а не персональные данные.
  • Новая страница С riverpod и bloc ставит задачу под провайдер или нотифаер riverpod и обработчик bloc и прерывает вместе с задачей HTTP-запрос http или dio.
  • README говорит, что новое значение перечисления или новый подкласс запечатанного класса выходит в мажорном выпуске и что switch по CancelReason, который открыт, держит ветку по умолчанию.
  • README говорит, что задача не Future: await job ничего не ждёт, и правило await_only_futures из package:lints это отмечает.
  • Страница о стримах говорит, что делает второй each по стриму с одной подпиской: падает задача, которую он создаёт, а сам вызов не бросает.
  • Страница о времени говорит, что отрицательная пауза считается нулевой.
  • Страница правил ошибок называет строку Job.debug об отложенной задаче, которую ждут до её запуска.
  • Нижняя граница SDK — Dart 3.10.0, прежде была 3.6.0.
  • У JobContext появился child, и JobContextBase его реализует. Класс, который сам реализует JobContext, его добавляет; подкласс JobContextBase с членом этого имени переименовывает его или делает переопределением; расширение на JobContext с членом child затеняется без предупреждения. Движок, который делает своих детей, в createEachJob или где-то ещё, или держит правила в check(), переопределяет child с @useResult: по умолчанию это Job.deferred, и такой движок его отвергнет, или ребёнок не спросит его правил.
  • ctx.child(body, key:, describe:, timeout:) делает незапущенного ребёнка, которого примет движок задачи, для кода, у которого есть контекст, но который не знает движка. В JobContextBase это Job.deferred. Движок, чьи дети — его собственные задачи, его переопределяет, и ctx.each берёт ребёнка оттуда же. ctx.child ничего не запускает и не проверяет: отказывают ctx.run, ctx.start и ctx.runAll. См. раздел «Запуск ребёнка»; шаг из раздела «Срок для одного шага» сделан именно так.
  • Раздел «Тест двигает время» говорит, как тестировать задачу с настоящим вводом-выводом: подделывать только таймеры.
  • Раздел «Работа в изоляте» показывает, как выполнить работу в изоляте, который отмена задачи убивает посреди шага.
  • JobObserver.onError принимает {required JobErrorAnswer answer}: что задача делает с ошибкой. answered: просит за неё ответа, через JobAnswerer.onUnanswered или зону. cancellation: не попросит никогда, потому что ошибка — отмена. never: не попросит никогда по другой причине, например из-за ignoreFailure(). later: исход, который несёт ошибку, ещё могут прочитать, и ничего не обещано. Об ошибке, которой не несёт ни один исход, и о провале тела задачи, принявшей отмену к тому моменту, когда тело бросило, Job.isCancellationError теперь спрашивают до onError, а не после: onError, отменивший свою задачу, больше не отправляет эту ошибку под фильтр, о провале, который onError уже услышал, фильтр, поставленный после этого, не спрашивают, а ошибка бросившего фильтра доходит до зоны раньше, чем onError услышит ошибку, о которой спрашивали фильтр. Ядро больше не оповещает через @protected notifyObserver: его переопределение слышит только то, что шлёт сам движок, а onError слышит каждую такую ошибку с later, даже после ignoreFailure(). См. «Куда идёт каждая ошибка».

Что писать вместо прежнего, слева имя в 0.2.0; у каждого есть своя запись ниже.

0.2.0 0.3.0
Job.ignore() Job.ignoreFailure()
JobBase.whenDone JobBase.untilFinished
JobBase.debug Job.debug
JobBase, JobContextBase, JobStatus из package:async_job/async_job.dart из package:async_job/engine.dart
Job<T>, переданный в ctx.run ChildJob<T>, или DeferredJob<T> для start()
ctx.wait ctx.abandonable
  • JobObserver.onError стал уведомлением, а за ошибку, которой больше ни у кого нет, отвечает новый JobAnswerer: за ту, которой не несёт ни один исход, и за Failed, который никто не наблюдает. Наблюдатель из 0.2.0 собирается без изменений, а ошибки, которые он раньше глотал, доходят до зоны; см. «Ответ за ошибки».
  • Job.ignore() переименован в Job.ignoreFailure(), а JobBase.whenDone — в JobBase.untilFinished.
  • Задача продолжается один раз: одним then или ручками нового Job.shared. Второй then, then на задаче с ручкой и shared на задаче с then бросают StateError: дайте каждой ветке ручку shared(), см. «Ручка на каждую ветку».
  • ctx.run принимает ChildJob. Хэндл, объявленный Job<T>, там больше не компилируется: объявите его ChildJob<T>, а там, где код вызывает start(), — DeferredJob<T>, и дайте задачам движка implements ChildJob<T>.
  • Класс, который сам реализует Job или JobContext, добавляет новые члены, а подкласс JobBase переименовывает свой член, чьё имя теперь занято. У Job появились whenFinished и shared, у JobBase — untilFinished, у JobContext — abandonable, heldCancellation, pause, runAll, start и параметры dispose и discard у run; then, shared и each принимают key и describe, и в переопределении тоже; расширение на Job или JobContext с именем нового члена затеняется без предупреждения.
  • JobBase, JobContextBase и JobStatus переехали в package:async_job/engine.dart, а JobBase.debug теперь называется Job.debug. См. «Своё поверх ядра».
  • Защищённый API движка изменился. JobContextBase.check, JobBase.finish и JobContextBase.startChild помечены @mustCallSuper; started, finished, adoptedBy, createContext, execute, createEachJob и beforeChildStart помечены @visibleForOverriding; cascadeToChildren и сеттер level убраны, вместо них cancelWith, cancelOwnJob и startChild; inUncancellableSection, heldCancel, creationZone и requireShared новые; createEachJob принимает key и describe от ctx.each, и переопределение передаёт их задаче, которую создаёт.
  • Ядро сообщает о Failed, который движок передаёт в finish: движок, который вызывал notifyObserver перед finish, убирает этот вызов, иначе наблюдатель услышит ошибку дважды.
  • Отмена внутри ParallelWaitError снова считается отменой: ребёнок, отменённый под [ctx.run(a), ctx.run(b)].wait, заканчивает родителя Cancelled там, где раньше заканчивал его Failed.
  • Тело, которое отменяет само себя, отменяется так же, как отменённая задача: детей просят остановиться, затем выполняются его колбэки ctx.onCancel и срабатывает whenCancelled.
  • ctx.wait устарел, его заменяет ctx.abandonable; старое имя уйдёт в следующем выпуске.

Ошибки, которые 0.2.0 теряла или сообщала только наблюдателю, теперь доходят до зоны, где создана задача, и тест на них падает:

  • провал тела задачи, которая кончилась Cancelled, например операции, остановленной по токену, который отменила задача;
  • провал тела задачи, которую движок кончает руками;
  • поздняя ошибка действия, которое тело бросило под ctx.abandonable через .timeout или Future.any;
  • ошибка, которой не несёт ни один исход, у задачи с наблюдателем;
  • провал собственной уборки источника, когда ctx.each отпускает стрим.

Job.isCancellationError, заданный один раз в main, останавливает ошибки, которые вызывает отмена, на onError, job.ignoreFailure() глушит первые два пункта у одной задачи, а за остальные отвечает наблюдатель, который подмешивает JobAnswerer; см. «Куда уходят ошибки».

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

ctx.check() в уборке отменённой задачи бросает её Cancelled, а 0.2.0 бросала StateError.

onError слышит каждый провал и каждую отмену внутри ParallelWaitError отдельным вызовом, а зона получает каждый провал такой ошибки, которую никто не наблюдает, отдельно; 0.2.0 отдавала всю ошибку один раз.

Отмена ребёнка, которую тело выпустило наружу, печатается как Cancelled(handler: Job(thumbnail): Cancelled(timeout)), а 0.2.0 печатала child thumbnail.

JobBase.children отдаёт копию на момент вызова, а 0.2.0 отдавала живое представление.

С включёнными assert в зону приходит AssertionError, когда движок кончает двух детей одной задачи одним экземпляром Cancelled.

Задача Job<void> или продолжение then<void>, чьё тело или колбэк возвращает незапущенную задачу Job.deferred, и ребёнок ctx.each, чей колбэк делает это на событии, кончаются исходом Failed с ArgumentError; в 0.2.0 они кончались Done.

Несколько сообщений об ошибках сформулированы иначе: тесту, который сверяет их по тексту, нужны новые формулировки.

  • ctx.start запускает ребёнка, которого тело не ждёт; его исход остаётся у того, у кого ребёнок в руках, и не трогает исход родителя.
  • ctx.runAll запускает детей бок о бок и просит остальных остановиться, как только с одним из них что-то пошло не так, с новой причиной SiblingCancelReason. См. «Когда один провал делает остальное бессмысленным».
  • ctx.pause — задержка, которую кончает отмена.
  • ctx.heldCancellation называет отмену, которую придерживает секция uncancellable в момент чтения. См. «Придержать отмену».
  • ctx.run принимает dispose и discard, как ctx.abandonable.
  • Job.whenFinished слышит конец задачи, не наблюдая её исход.
  • timeout даёт задаче собственный срок, после которого она кончается Cancelled(timeout) с новой причиной TimeoutCancelReason.
  • Job.each создаёт корневую задачу, которая следует за стримом.
  • then, shared и ctx.each принимают key и describe и называют созданную задачу так же, как называется любая; без describe её описание — then, shared или each. См. «Имя задачи».
  • JobObserver.all делает из нескольких наблюдателей одного.
  • JobObserver стал abstract mixin class, чтобы наблюдатель, который наследует другой класс, его подмешивал.
  • Job.isCancellationError называет ошибки, которые вызывает отмена: onError их слышит, а отвечать за них не просят никого. См. «Ошибки отмены».
  • Job.visitErrors отдаёт каждый провал и каждую отмену внутри ошибки в свой колбэк.
  • started у Cancelled.by необязателен и по умолчанию равен true.
  • Отладочный канал называет задачу, которая отбросила условные регистрации отданного ею значения, и задачу, которая кончилась Done, не отдав такое значение: если оно не лежит внутри результата, его никто не уберёт.
  • ctx.run и ctx.each отказывают классу, который реализует JobBase, а не наследует его, с обещанным ArgumentError, а не с NoSuchMethodError.
  • Значение, которое приходит в вызов ctx.abandonable, уже прерванный отменой, освобождается сразу; раньше оно удерживалось, пока не кончатся дети, и ребёнок, который ждал ту же блокировку, ждал её вечно.
  • Ошибка, брошенная JobBase.started, идёт в onError, а тело выполняется; раньше задача так и шла без тела, вечно.
  • Задачи, равные по ==, больше не принимаются одна за другую: родитель двух таких детей завершался, пока один из них ещё шёл.
  • Ключ или describe, который бросает, печатается в имени задачи меткой, а не подменяет сообщение ядра об ошибке.
  • Ребёнок, колбэк отмены и слушатель уходят из своего списка без поиска: тысячи таких в одной задаче отнимали время, которое росло как квадрат их числа.
  • Тело, которое бросает Cancelled, принимает эту отмену сразу: пока дети не кончились, isCancelled отвечал false, а cancel() ставил свою причину поверх той, которую выбрало тело.
  • Завершённая задача отпускает своё тело, своего родителя и уборки, которые так и не выполнила, а задача, созданная внутри работы, отданной ctx.unattended, отпускает задачу, которая эту работу запустила: сохранённый хэндл держал всё, что они захватили.
  • Функция снятия, которую возвращает whenCancelled или ctx.onCancel, отпускает колбэк, как только её вызвали или задача кончилась: владелец, который хранил функции снятия до своего конца, держал всё, что захватили колбэки.
  • Функция снятия, которую возвращает ctx.onDispose или ctx.onDiscard, отпускает уборку и задачу, как только её вызвали, как только уборка отработала и как только задача кончилась, так её и не запустив: владелец, который хранил функции снятия до своего конца, держал задачу и всё, что захватила уборка.
  • Задача, созданная через Job(...) внутри работы, отданной ctx.unattended, идёт в зоне, откуда эта работа была запущена, и ошибка голого unawaited в ней больше не доходит до наблюдателя задачи, которая запустила работу.
  • ctx.join освобождает значение через dispose или discard, когда контрольная точка после действия бросает что угодно, а не один Cancelled.
  • ctx.join и ctx.abandonable, вызванные из работы, отданной ctx.unattended, больше не отдают этой работе ресурс, который они уже закрыли.
  • Значение, которое возвращается после конца тела, больше не заставляет ядро бросить Future already completed.
  • Значение, пришедшее для задачи, которую движок закончил руками, освобождается, а не утекает.
  • Дерево задач, у которого на постройке кончился стек, кончается с StackOverflowError, а раньше его корень мог остаться работать навсегда; отмена, у которой в глубоком дереве кончается стек, всё равно выполняет колбэки onCancel тех задач, до которых дошла.
  • Вызов ctx.abandonable, которого тело не ждало через await, больше не отправляет отмену задачи в зону, когда задача принимает её после конца тела.
  • Тысячи задач, которые просыпаются вместе, больше не стоят примерно по две миллисекунды каждая под JIT.
  • Продолжение then, приставленное к задаче, за чей провал уже ответили, больше не отвечает за него второй раз.
  • Ломающее: ctx.run(child) возвращает Future<T> вместо хэндла ребёнка. Пишите await ctx.run(child), чтобы дождаться его значения, детей и уборки; при успешном завершении отмена родителя проверяется до того, как идти дальше. Сохраняйте исходный child, чтобы отменять его или смотреть исход. Обрабатывайте ошибки возвращённой future или пишите .ignore() для намеренного параллельного старта, результат которого не нужен. Проверка при старте по-прежнему бросает синхронно. ctx.each по-прежнему сразу возвращает хэндл своего ребёнка.

  • Добавлен защищённый JobContextBase.startChild для приёма ребёнка движком предметной области и проверок старта, общих для run и each, без наблюдения результата ребёнка.

  • Добавлен Job.then с отдельным контекстом на каждое продолжение, передачей исхода вперёд и отменой в обе стороны. Отмена хвоста ждёт незавершённых предшественников и их уборку. ChainCancelReason сохраняет каждую соседнюю отмену в cause. Продолжения считаются корневыми задачами ядра с собственным необязательным наблюдателем и не зависят от очередей предметной области.

  • Ломающее: ctx.each(stream, (child, event) { ... }) сразу возвращает дочернюю Job<void>. Ждите .value, если нужен результат, который бросает, смотрите .done ради исхода или зовите .cancel(), чтобы остановить её отдельно. Колбэк получает контекст ребёнка. Родитель ждёт ребёнка, даже когда его тело его не ждёт, и наблюдатели видят ребёнка.

  • Отмена each сразу снимает подписку и останавливает доставку событий, а затем ждёт идущий колбэк, прежде чем ребёнок завершится и уберёт за собой. Чтобы отвечать на отмену, пользуйтесь контрольными точками контекста ребёнка; обычный await прервать нельзя. Уборку источника при отмене подписки по-прежнему не ждут.

  • Ломающее: each стал методом JobContext; расширение JobStream убрано. JobContextBase.createEachJob служит защищённой фабрикой для движков предметной области. Как и run, each отклоняется из unattended и после конца тела родителя.

  • Ломающее: причины стали расширяемыми классами: ManualCancelReason, ParentCancelReason и HandlerCancelReason. Замените именованные константы конструкторами и проверяйте типы, а не сравнивайте имена. CancelReason абстрактный; подклассы могут нести любые данные.

  • job.cancel(reason: reason) принимает свою причину, которая сохраняется по идентичности в колбэках и исходах. Тело, бросающее Cancelled.by, теперь тоже сохраняет свою явную причину.

  • Отмена родителя и отмена ребёнка, вышедшая из тела, сохраняют исходный Cancelled в ParentCancelReason.cause и HandlerCancelReason.cause соответственно.

  • Ломающее: future Job.whenCancelled заменена на job.whenCancelled((cancelled) { ... }). Колбэк получает полный Cancelled и выполняется синхронно; регистрация после отмены вызывает его сразу. Возвращённая функция снимает колбэк.

  • Неиспользованные слушатели отмены освобождаются, когда задача кончается без отмены. Регистрация не наблюдает исход с провалом.

  • Синхронные ошибки слушателей идут через onError или, без наблюдателя, в зону создания, как у ctx.onCancel. Асинхронные колбэки не ожидаются.

Первый выпуск. Ядро задач, вынутое из solo до его собственного первого выпуска.

  • Job<T>: отменяемая асинхронная работа с исходом, детьми и кооперативной отменой. Создаётся через Job(body), которая стартует на следующей микротаске, или через Job.deferred(body), которая ждёт start().
  • Outcome<T>: Done, Failed, Cancelled с открытым CancelReason и публичным Cancelled.by для движков, построенных на этом ядре.
  • Семейство ожиданий, которое говорит, что отмена делает с вызовом: ctx.wait кончает ожидание, а не работу, ctx.join ждёт действие целиком и сдаётся после него, ctx.uncancellable удерживает отмену, пока шаг не кончится, ctx.unattended не ждёт вовсе и отдаёт работу движку, ctx.onCancel передаёт отмену тому, что умеет остановиться по-настоящему, а ctx.check сдаётся там, где оборачивать нечего. ctx.each следует за стримом, пока живёт задача, ждёт асинхронный onData и забирает подписку с собой при любом исходе.
  • Стек уборки: ctx.onDispose освобождает при любом исходе, ctx.onDiscard только когда задача кончается не Done, а wait и join принимают те же два как dispose и discard для значения, которое они передают. Движок разматывает стек после детей и до исхода, дожидаясь каждой уборки; ctx.disown(value) забирает регистрацию обратно, когда тело передаёт значение само.
  • Дети через ctx.run: родитель кончается после них, отмена идёт каскадом, а ребёнок, который не отменяется, от неё отказывается.
  • JobObserver с onStart, onFinish, onError и onLog; ребёнок наследует наблюдателя родителя. Исход Failed, который никто не наблюдал, уходит в зону, создавшую задачу, и туда же уходит ошибка, которой больше некуда идти, когда наблюдателя нет. Cancelled туда не уходит никогда: отмену кто-то решил, это не провал, и услышать её можно только в наблюдателе.
  • ctx.unattended(action) для работы, которую тело запускает и не ждёт: она идёт в собственной зоне ошибок, и всё, что она оставит неперехваченным, сейчас или много позже конца задачи, доходит до onError, а не до процесса. ctx.run и ctx.uncancellable изнутри неё отклоняются, а задача, созданная там, сообщает в зону, где идёт тело, а не наблюдателю задачи, которая запустила работу.
  • JobBase<T> и JobContextBase для наследования движком предметной области: защищённый API, виртуальная контрольная точка check() и хуки started(), finished() и adoptedBy(). В защищённом API есть ещё reportToZone для предметной области, у которой собственный путь ошибки, которой некуда идти, ни к кому не приводит (он не пускает Cancelled в зону точно так же, как ядро, и предметной области не приходится писать это правило заново), и throwIfUnattended для члена её контекста, который нельзя вызывать из работы, отданной unattended.