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

Гонки и время

middle~20 мин

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

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

Ретранслятор

export interface Relay {
  readonly name: string;
  readonly send: (line: string) => Effect.Effect<void, SendFailed>;
}

Для опыта Мост принимает за 300 миллисекунд, Холм за 100, но врёт, то есть падает SendFailed, Башня 3 принимает за 200. Каждый пишет в журнал, когда начал, когда принял и когда его прервали.

raceAll

Гонка запускает все попытки сразу и отдаёт первый успех:

export const fastestRelay = (relays: readonly Relay[], line: string): Effect.Effect<string, SendFailed> =>
  Effect.raceAll(relays.map((relay) => Effect.as(relay.send(line), relay.name)));
Башня 3 [
  'Мост начал',
  'Холм начал',
  'Башня 3 начал',
  'Башня 3 принял гроза',
  'Мост прерван'
]

Начали все трое. Холм упал первым, на сотой миллисекунде, но гонку не выиграл: побеждает первый успех, а не первый, кто кончился. Башня 3 приняла на двухсотой, и Мост прервали, не дав ему догнать. Если бы упали все трое, гонка упала бы со всеми тремя ошибками.

Для двух участников есть короткая запись: Effect.race(a, b).

timeout

Гонке можно поставить предел по времени. Effect.timeout(self, длительность) падает, если self не успела:

[ 'TimeoutError' ]

Это ошибка в E с тегом TimeoutError, её ловят catchTag, как любую ошибку дела. Если «не успел» не ошибка, а нормальный ответ, бери Effect.timeoutOption: успех это Option.some(значение), не успел это Option.none():

export const relayWithin = (
  relays: readonly Relay[],
  line: string,
  millis: number,
): Effect.Effect<Option.Option<string>, SendFailed> => Effect.timeoutOption(fastestRelay(relays, line), millis);
{ _id: 'Option', _tag: 'None' } [
  'Мост начал',
  'Холм начал',
  'Башня 3 начал',
  'Мост прерван',
  'Башня 3 прерван'
]

Сто пятьдесят миллисекунд: Холм соврал, остальные не успели. Предел времени это тоже гонка, гонка с часами: когда часы выиграли, проигравших прервали. В журнале ни одного «принял», зато оба «прерван»: ни один ретранслятор не продолжает гнать кадр после того, как ответ уже не нужен.

firstSuccessOf

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

export const firstWorking = (relays: readonly Relay[], line: string): Effect.Effect<string, SendFailed> =>
  Effect.firstSuccessOf(relays.map((relay) => Effect.as(relay.send(line), relay.name)));
Башня 3 [ 'Башня 3 начал', 'Башня 3 принял гроза' ]

firstSuccessOf пробует по очереди и останавливается на первом успехе. Башня 3 стояла первой в списке и приняла сразу; до Холма и Моста дело не дошло. Упала бы Башня 3, следующим пошёл бы Холм. По времени это сумма попыток, а не самая быстрая из них.

Под капотом

raceAll запускает каждую попытку отдельным файбером, детьми того, кто позвал. Первый успех кладёт ответ в общую ячейку и прерывает остальных; провалы копятся, пока не упадут все. timeout это race между работой и Effect.sleep: проигравшую сторону прерывают так же. Поэтому всё, что ты знаешь о прерывании из урока 57, работает и здесь: финализаторы проигравших отрабатывают, а uninterruptible кусок проигравшего гонка дождётся.

Где тут дыра

const fastest = Effect.promise(() => Promise.race([send('Мост', 300), send('Башня 3', 200)]));

console.log(await Effect.runPromise(fastest));
await new Promise((resolve) => setTimeout(resolve, 200));
console.log(log);
Башня 3
[ 'Башня 3 отправил', 'Мост отправил' ]

Ответ верный: Башня 3. А Мост всё равно отправил кадр, через сто миллисекунд после того, как ответ уже был. Promise.race выбирает победителя, но не останавливает проигравших: у промиса нет прерывания. Кадр ушёл соседу дважды. Это та же гонка Ринцвальда из босса Гекса, только теперь внутри квитанции. И Effect.promise не поможет: прервать можно квитанцию, а промис внутри неё уже летит.

Лечится тем, что гонку устраивает Effect: попытки это квитанции, и проигравших прерывают по-настоящему.

Башня, этап 29

Башня учится работать с ретрансляторами: через всех сразу с ответом первого честного, то же с пределом по времени, и через запасных по очереди.

Практика

Дальше

Пондер сидит над журналом и что-то пересчитывает на пальцах.

Общее состояние без гонок ждёт в следующем уроке.