Раздел 46 · Effect: Тихая Академия

Прерывание

middle-senior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Ошибку и дефект ты разбирал в EF4. У провала есть третий вид, Interrupt из урока 41, и до сих пор он встречался только в выводе describeExit. Этот урок о нём: откуда приходит прерывание, что оно делает с работой, как защитить кусок, который нельзя оборвать посередине, и как записать, что смену прервали.

Что такое прерывание

Прерывание приходит снаружи квитанции, а не из неё самой. Его посылают, когда результат работы больше не нужен: хозяин программы гасит башню, из двух ответов уже пришёл первый и второй ждать незачем, время на работу вышло. Как такие ситуации собираются в коде, ты увидишь в EF7 и EF8, а пока прерывание посылаем руками: Fiber.interrupt(fiber) для квитанции, запущенной через Effect.runFork.

Квитанцию можно прервать между любыми двумя шагами и на любом ожидании: на Effect.sleep, на ответе провода, на Deferred.await. Работа обрывается, финализаторы в областях исполняются с исходом interrupt, и квитанция кончается причиной Interrupt.

Прерывание не ошибка, и Effect.catch его не ловит:

const caught = await Effect.runPromiseExit(Effect.interrupt.pipe(Effect.catch(() => Effect.succeed('поймал'))));
console.log(caught._tag, caught._tag === 'Failure' ? caught.cause.reasons.map((r) => r._tag) : caught.value);
Failure [ 'Interrupt' ]

Это сделано намеренно. Если бы catch ловил прерывание, обработчик «на всякий случай» превращал бы каждый сигнал «остановись» в «продолжаем как ни в чём не бывало», и погасить такую программу было бы нельзя.

Ответ без копии

Провод для опыта отвечает через сто миллисекунд. Пара строк, ответ соседу и копия в архив, уходит подряд. Прерываем на сто пятидесятой миллисекунде:

const pair = (first: string, second: string) =>
  Wire.use((wire) => Effect.andThen(wire.send(first), wire.send(second)));

const fiber = Effect.runFork(pair('ответ', 'копия').pipe(Effect.provide(slowWire)));
await new Promise((resolve) => setTimeout(resolve, 150));
await Effect.runPromise(Fiber.interrupt(fiber));
// дальше печатаем, что ушло по проводу и сколько ждал Fiber.interrupt
без защиты: [ 'ответ' ] прерывание ждало 0 мс

Ответ ушёл, копия нет: прерывание пришло, пока шла вторая отправка. Ровно то, на что жаловался Ринцвальд.

uninterruptible

Effect.uninterruptible(self) делает кусок неделимым. Прерывание, пришедшее посреди него, не теряется, а ждёт, пока кусок кончится:

export const sendPair = (first: string, second: string): Effect.Effect<void, SendFailed, Wire> =>
  Effect.uninterruptible(Wire.use((wire) => Effect.andThen(wire.send(first), wire.send(second))));
uninterruptible: [ 'ответ', 'копия' ] прерывание ждало 50 мс

Ушли обе строки. Fiber.interrupt ждал пятьдесят миллисекунд, пока кончится вторая отправка, и только потом вернулся: прерывание дождалось конца неделимого куска.

Неделимым кусок должен быть коротким. Каждая миллисекунда внутри uninterruptible это миллисекунда, когда башню нельзя погасить. acquireRelease из урока 55 уже делает неделимыми взятие и возврат: ресурс не может оказаться наполовину взятым.

onInterrupt и ensuring

Две функции добавляют к квитанции действие на выходе.

  • Effect.onInterrupt(self, cleanup) исполняет cleanup, только если self прервали.
  • Effect.ensuring(self, finalizer) исполняет finalizer при любом исходе: успехе, ошибке, дефекте, прерывании. Это finally для квитанции, только он не может забыть сработать.

Смена под охраной пишет об обоих:

export const guarded = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | Journal> =>
  self.pipe(
    Effect.onInterrupt(() => Journal.use((journal) => journal.write('смена прервана'))),
    Effect.ensuring(Journal.use((journal) => journal.write('смена сдана'))),
  );
[ 'смена прервана', 'смена сдана' ]

Чем это отличается от acquireRelease? Ничего не берётся на время, возвращать нечего. ensuring это финализатор без ресурса, как addFinalizer из урока 55, только привязан не к области, а к одной квитанции.

Под капотом

Исполнитель Effect проверяет флажок прерывания между шагами квитанции. Флажок поднят, и шаг не начинается: исполнитель идёт снимать финализаторы. uninterruptible ставит замок: пока он закрыт, исполнитель флажок видит, но не трогает. Как только кусок кончился и замок открылся, первое, что сделает исполнитель, это проверит флажок. Поэтому прерывание не теряется, а откладывается. Внутри неделимого куска можно открыть окно обратно, Effect.interruptible, но это редкий инструмент: его ставят, когда внутри долгого неделимого куска есть безопасное место подождать.

Где тут дыра

const night = Effect.uninterruptible(guarded(withResources(['архив'], namedShift(lines))));

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

Лечится тем, что неделимым делают только то, что нельзя разорвать: пару строк, взятие ресурса. Всё остальное должно останавливаться по первому слову, а порядок наводят финализаторы.

Башня, этап 22

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

Практика

Глубже в архиве

Прерывание и асинхронные исключения в Haskell, и почему там тоже есть маска на неделимый кусок, разобраны в уроке «Исключения и ресурсы».

Дальше

Рыдкулл стоит в дверях с фонарём.

Край программы ждёт в следующем уроке.