Cancel Workflow и сотрудничество Activity
Заявку можно отозвать до завершения публикации. Для «Согласования» это отдельное событие, а не обычное завершение Worker. Теперь обработаем cancel Workflow, передадим отмену внешнему HTTP-вызову и освободим бронь. Основной результат урока — понять отмену как согласованный протокол, который требует участия кода и не возвращает автоматически уже выполненные действия.
Полный вариант лежит в 08-cancel.ts. Он сохраняет Signal редактора и Query состояния, но добавляет cancellation policy и cleanup. Автор не выполнял cancel, не прерывал HTTP и не наблюдал состояние броней. Описанный далее исход — ожидаемый результат самостоятельного сценария читателя.
Cancel отличается от остановки процесса
Client использует handle.cancel(). Эта команда отправляет запрос отмены конкретного Workflow. Worker должен обработать его, а программа — выполнить предусмотренный путь. Если Worker временно отсутствует, отмена не превращает его в действующий процесс. Service сохраняет запрос, но cleanup потребует подходящего исполнителя.
Shutdown Worker из третьего урока относится к жизни исполнителя и не означает отмену всех его заявок. Сходным образом закрытие вкладки пользователя не является предметной отменой. Продукту нужна явная команда и политика того, кто имеет право её отправлять. Иначе обычный выход пользователя из интерфейса неожиданно меняет данные.
Terminate — другой инструмент: он завершает Workflow без обычной возможности исполнять cleanup-ветку. Его нельзя подставить вместо cancel только ради быстрого статуса «закрыто». Если процесс уже зарезервировал ресурс, принудительное завершение может оставить его внешнее состояние без согласованного освобождения.
В нашей учебной модели cancel доступен через локальный CLI, а проверка человеческих прав не реализована. Настоящий HTTP-интерфейс должен авторизовать действие по заявке. Workflow ID, известный пользователю, не является секретом или доказательством полномочий.
Доставка отмены активной Activity
Proxy в стадии 08 содержит:
cancellationType: ActivityCancellationType.WAIT_CANCELLATION_COMPLETED,
heartbeatTimeout: '8 seconds',
Политика WAIT_CANCELLATION_COMPLETED выражает ожидание завершения отменяемой Activity. Другие политики имеют иной смысл, поэтому выбор нельзя делать по длине названия. Для cleanup нам полезно дождаться, как активная попытка закончила сотрудничество с отменой, вместо предположения, что HTTP исчез мгновенно.
Activity использует cancellationSignal вместе с пятисекундным пределом HTTP, а также отправляет heartbeat. Для длинной работы heartbeat должен быть периодическим: через него исполнитель сообщает продвижение и получает сведения об отмене. Наш короткий запрос не моделирует длительный импорт, поэтому его нельзя расширять до часа без изменения heartbeat-логики.
Когда AbortSignal прекращает fetch, gateway мог уже изменить состояние. Cancel соединения и cancel бизнес-операции различаются. Код может перестать ждать, но внешняя публикация остаётся. Именно поэтому стабильный operation key и сохранённая квитанция предыдущего урока важны даже для сценария отмены.
Общий механизм описан в документации Cancellation, а области — в Cancellation scopes. Для нашего процесса отмена должна привести к известной cleanup-последовательности, не скрывая неопределённость внешнего эффекта.
Cleanup вне отменяемой области
В полном Workflow операции окружены try/catch. Ключевой фрагмент:
} catch (error) {
if (isCancellation(error)) {
status = 'compensating';
await CancellationScope.nonCancellable(() => releaseSlot(input));
}
throw error;
}
IsCancellation проверяет форму ошибки, связанной с отменой. Затем releaseSlot выполняется в non-cancellable области. Если просто вызвать cleanup в уже отменённой области, новая Activity тоже может сразу унаследовать отмену. Здесь мы явно выражаем, что завершение предметной очистки должно продолжиться несмотря на первоначальный запрос остановки.
После успешного cleanup исходная ошибка выбрасывается снова. Workflow остаётся отменённым, а не превращается в published или rejected. Это важно для интерфейса: освобождённая бронь не означает, что редактор отклонил материал. Пользовательское действие cancel сохраняет собственный результат.
Non-cancellable не означает бесконечно выполняющийся код. ReleaseSlot всё ещё имеет timeout и retry-пределы. Если внешний сервис недоступен, cleanup может завершиться ошибкой. В следующем уроке расширим обработку этого случая и запишем явную потребность в ручном согласовании.
ReserveSlot находится внутри try, чтобы отмена во время брони тоже попала в cleanup. Внешняя система могла создать бронь до получения подтверждения. Мы используем безопасный release, который допускает отсутствие брони. Иначе регистрировать очистку только после успешного ответа недостаточно для потери подтверждения.
Отмена в разных стадиях
Отмена области не отменяет факт предыдущего решения редактора. В истории остаётся Signal, даже если позднее процесс остановили. Для аудита важно сохранить обе причины: человек разрешил публикацию, другой участник запросил остановку. Удаление одного признака ради единственного красивого статуса скрывает последовательность событий.
В продукте правило «до какого момента отмена допустима» определяется отдельно. Наш учебный CLI допускает запрос во время работы, поэтому регламент честно разбирает гонку с публикацией вместо обещания безусловного предотвращения эффекта.
Самый понятный будущий сценарий — cancel в состоянии waiting:
WORKFLOW_STAGE=08-cancel npm run worker
npm run client -- start article-108
npm run client -- query article-108
npm run client -- cancel article-108
npm run client -- result article-108
Ожидается запрос отмены, выполнение releaseSlot и отменённое исполнение. Result-команда может показать ошибку отмены, а не обычный Result объекта. Это соответствует выбранному контракту CLI. Автор не получал такой вывод, поэтому точные текст и время ошибки не заявлены.
Cancel до подтверждения reserveSlot также может вызвать release. Если gateway ещё ничего не создал, операция безопасно оставит отсутствие брони. Если бронь появилась, освобождение уберёт её. Это пример компенсации неопределённого подтверждения: cleanup ориентируется на возможный эффект, а не только на факт успешного await.
Самый сложный случай — cancel во время publish. Gateway мог уже записать publication и удалить бронь. ReleaseSlot не отзывает материал, поэтому отменённый Workflow может сосуществовать с опубликованным объектом. Нельзя сообщить человеку «публикации точно нет» только по cancelled статусу Temporal.
Для такого случая нужен предметный регламент: прочитать внешнюю квитанцию, проверить факт публикации и решить, требуется ли отдельная команда отзыва. В нашей модели отзыв не реализован. Это ограничение сохранено в README и handoff, чтобы следующая глава не обещала универсальное обращение всех эффектов.
После урока отмена имеет определённый путь и не смешивается с shutdown. Но cleanup пока срабатывает только на cancellation. Далее добавим компенсацию окончательных ошибок и ручное согласование, сохранив границу между освобождением брони и отменой публикации.