Тема
Ошибки и компенсация
Компенсация — отдельная операция, которая отменяет бизнес-эффект уже успешного node, если следующий node завершился ошибкой.
Это не database transaction. Frontend вызывает реальные backend endpoints: reserve и затем release.
Пример checkout
ts
// Каждый handler по-прежнему является Query Layer store.
const reserveStore = new MutationStore<Reservation, [ReserveInput]>({
mutationKey: "checkout.reserve",
request: (input, config) => checkoutApi.reserve(input, config),
options: { retry: false },
})
const releaseStore = new MutationStore<void, [string]>({
mutationKey: "checkout.release",
request: (reservationId, config) => {
return checkoutApi.release(reservationId, config)
},
options: { retry: false },
})
const paymentStore = new MutationStore<Payment, [PaymentInput]>({
mutationKey: "checkout.pay",
request: (input, config) => checkoutApi.pay(input, config),
options: { retry: false },
})Workflow:
ts
const checkoutWorkflow = new WorkflowStore({
nodes: [
{
id: "reserve",
// Сначала резервируем товар.
execute: async () => {
const response = await reserveStore.execute(reserveInput)
if (!response) {
throw new Error("Reservation was cancelled")
}
return response.data
},
// Если следующий node упал, снимаем созданный резерв.
compensate: async (value) => {
const reservation = value as Reservation
await releaseStore.execute(reservation.id)
},
},
{
id: "payment",
dependencies: ["reserve"],
execute: async ({ results }) => {
const reservation = results.get("reserve") as Reservation
const response = await paymentStore.execute({
reservationId: reservation.id,
paymentMethodId,
idempotencyKey: crypto.randomUUID(),
})
if (!response) {
throw new Error("Payment was cancelled")
}
return response.data
},
},
],
})Если payment упал:
- workflow запоминает failed node
payment; - берёт успешно completed nodes в обратном порядке;
- вызывает
reserve.compensate(reservation); - собирает ошибки compensation;
- бросает
WorkflowExecutionError.
Обратный порядок
Для цепочки:
text
create-order → reserve-stock → charge-payment → send-emailпри ошибке send-email compensation выполняется обратно:
text
refund-payment → release-stock → cancel-orderКомпенсируются только nodes, у которых есть compensate и которые успели успешно завершиться.
На параллельном уровне обратный порядок соответствует фактическому порядку завершения, а не порядку объявления массива.
WorkflowExecutionError
ts
import {
WorkflowExecutionError,
} from "@dubium/query-layer/orchestration"
try {
await checkoutWorkflow.execute()
} catch (error) {
if (error instanceof WorkflowExecutionError) {
error.failedNodeId
error.cause
error.compensationErrors
}
}| Поле | Что содержит |
|---|---|
failedNodeId | id node, где произошла исходная ошибка. |
cause | Исходную ошибку condition/execute/cancellation. |
compensationErrors | Все ошибки обратных операций; compensation не останавливается после первой. |
WorkflowStore.error содержит тот же error object.
Ошибка compensation
Ошибка release/refund не скрывает исходную ошибку payment. Она добавляется в compensationErrors. Product должен предусмотреть:
- alert/monitoring;
- ручное восстановление оператором;
- retry идемпотентной compensation;
- reconciliation job backend.
Не показывайте пользователю только «payment failed», если резерв снять не удалось.
Compensation должна быть идемпотентной
Checkpoint/reload или нестабильная сеть могут привести к повторному вызову. Backend compensation endpoint должен безопасно отвечать на повтор:
text
DELETE /reservations/:idможет вернуть success, даже если резерв уже снят, либо использовать idempotency key.
Cancellation
workflow.cancel() тоже приводит к error path внутреннего executor. Уже завершённые nodes могут быть компенсированы. WorkflowStore.status станет cancelled.
Активный HTTP внутри node нужно связать с workflow signal через adapter, показанный на странице Узлы и зависимости.
Что не компенсировать
Не каждому read node нужна компенсация:
ts
{
id: "profile",
execute: () => profileStore.fetch(),
}GET не изменил server state. Compensation нужна только для успешно выполненных side effects.
Email/push notification часто невозможно «отправить назад». Проектируйте workflow так, чтобы необратимые nodes шли после критичных компенсируемых шагов, либо переносите orchestration в backend transaction/saga.
Когда frontend compensation не подходит
Выбирайте backend orchestration, если:
- операция должна продолжиться после закрытия браузера;
- требуется строгая финансовая консистентность;
- несколько сервисов должны договориться атомарно;
- secrets доступны только backend;
- workflow длится минуты/часы;
- audit trail обязателен независимо от клиента.
Frontend WorkflowStore полезен для UI-driven процесса, но не заменяет backend saga.