В прошлом уроке правило было одно: ребёнок forkChild живёт не дольше родителя. Для пульса это ровно то, что нужно. Для отложенного ответа нет: обработчик кадра живёт миллисекунды, а ответ должен уйти через секунду. Этот урок о том, почему правило такое и какие у него есть честные исключения.
Почему ребёнок умирает с родителем
const childLater = (line: string) =>
Effect.forkChild(Effect.andThen(Effect.sleep(1000), Wire.use((w) => w.send(line))));
await Effect.runPromise(childLater('через forkChild').pipe(Effect.provideService(Wire, wire)));
Строка не ушла. childLater запустил ребёнка и кончился, а вместе с ним прервали ребёнка, так и не дождавшегося своей секунды.
Это структурная конкурентность: время жизни файбера вложено в время жизни родителя, как блок кода вложен в функцию. Плата за правило понятна, это наш отложенный ответ. Выгода больше. Прервали смену, и вместе с ней остановилось всё, что она начала. Кончился тест, и после него ничего не бьётся. Пришёл сигнал из урока 58, и остановилась вся программа, а не только главный файбер. Без правила каждый fork пришлось бы отслеживать руками, и каждый забытый остался бы висеть.
Поэтому forkChild выбирают по умолчанию. Исключения ниже должны быть явными.
Три исключения
Effect.forkScoped привязывает ребёнка не к родителю, а к области из контекста. Пульс башни на всю жизнь слоя:
export const beatLayer: Layer.Layer<never, never, Journal> = Layer.effectDiscard(Effect.forkScoped(beat));
Layer.effectDiscard это слой, который ничего не даёт, а только делает что-то при сборке. Сборка запускает пульс в области слоя, и пульс живёт, пока слой не разберут:
yield* Effect.sleep('2500 millis').pipe(Effect.provide(beatLayer));
yield* Effect.sleep('2 seconds');
[ 'пульс 1', 'пульс 2' ]
Два пульса за жизнь слоя, и ни одного после. Без слоя forkScoped требует область в R:
ex62b.ts(3,33): error TS2379: Argument of type 'Effect<Fiber<never, never>, never, Journal | Scope>' is not assignable to parameter of type 'Effect<Fiber<never, never>, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type 'Journal | Scope' is not assignable to type 'never'.
Type 'Journal' is not assignable to type 'never'.
Тот же Scope, что у acquireRelease в уроке 55: файбер, привязанный к области, это ещё один ресурс, который область закроет.
Effect.forkIn(self, scope) привязывает ребёнка к области, которую ты назвал явно. Это forkScoped, только область не из контекста, а из переменной. Нужен, когда работу надо привязать к чужой, более долгой жизни: к области сервера, к области смены, которая началась раньше обработчика.
Effect.forkDetach не привязывает ребёнка ни к чему. Он живёт, пока не кончится сам:
export const sendLater = (line: string, millis: number): Effect.Effect<Fiber.Fiber<void, SendFailed>, never, Wire> =>
Effect.forkDetach(Effect.andThen(Effect.sleep(millis), Wire.use((wire) => wire.send(line))));
[ 'через forkDetach' ]
Отложенный ответ ушёл. sendLater отдаёт файбер: тот, кому важно, может его дождаться или прервать. Больше его не остановит никто.
Сравни все четыре вида в одном месте: родитель запустил пульс и кончился, потом закрыли область.
Под капотом
Когда файбер кончается, любым исходом, исполнитель прерывает всех его детей, запущенных через forkChild, и ждёт их финализаторов. forkScoped и forkIn вместо списка детей кладут в область финализатор «прервать этот файбер», как acquireRelease кладёт возврат ресурса. forkDetach не кладёт никуда. Отсюда и выбор: кто отвечает за файбер, тот и должен его пережить.
Где тут дыра
const shift = Effect.gen(function* () {
yield* Effect.forkDetach(beat);
yield* Effect.sleep('1500 millis');
log.push('смена кончилась');
});
[ 'пульс', 'смена кончилась', 'пульс', 'пульс' ]
Тот же пульс, что у runFork в прошлом уроке: смена кончилась, а он бьётся. Разница одна: forkDetach хотя бы видит контекст родителя. Но остановить его некому. В тестах отвязанные файберы переживают тест и пишут в журнал следующего, а в программе копятся, пока память не кончится. Сигнал из урока 58 их тоже не остановит: они не в дереве главного файбера.
Лечится тем, что forkDetach ставят только туда, где работа правда должна пережить всех, кто рядом, и где кто-то держит файбер. Остальное остаётся forkChild или привязывается к области.
Башня, этап 27
У башни теперь есть пульс на всю жизнь слоя и отложенная строка, которая уходит, даже если тот, кто её отправил, давно кончился.
Практика
Дальше
Ринцвальд сдаёт утренний журнал опроса соседей и зевает.
Как сделать много сразу и не утопить провод, разберём в следующем уроке.