В прошлом уроке башня делала много сразу и ждала всех. Здесь другая задача: сделать несколько попыток сразу и дождаться одной, а остальные остановить. Плюс второй вопрос, которого раньше не было: сколько вообще ждать.
Ретранслятор
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
Башня учится работать с ретрансляторами: через всех сразу с ответом первого честного, то же с пределом по времени, и через запасных по очереди.
Практика
Дальше
Пондер сидит над журналом и что-то пересчитывает на пальцах.
Общее состояние без гонок ждёт в следующем уроке.