High-level transactions
On top of the durability controls and retry controls, the SDKs also provide high level functions for defining transactions supporting compensation actions in case of getting reverted.
Although Golem’s automatic retry policies and low-level atomic regions provide a lot of power automatically, a set of external operations such as HTTP requests often needs to be executed transactionally; if one operation fails, compensation actions may need to undo earlier successful operations.
The SDK provides support for two different types of transactions:
- fallible transactions are only dealing with domain errors
- infallible transactions compensate domain errors and rewind for an immediate retry
Fallible transactions
Many times external operations (such as HTTP calls to remote hosts) need to be executed transactionally. If some operations failed, the transaction needs to be rolled back; compensation actions need to undo whatever the already successfully performed operations did.
A fallible transaction deals with domain errors. Successful steps register compensations. Propagating a domain error out of the transaction body triggers these compensations in reverse order; handling the error inside the body does not automatically trigger rollback. The failing step has no registered compensation. TypeScript, Rust and Scala stop at the first compensation error and report partial rollback. Effect’s withFallibleCompensation continues the remaining compensations and reports the first compensation error.
TypeScript
In TypeScript, a fallible saga can be executed using the async fallibleSaga function, by
passing an async callback that can execute operations on the open transaction (see below).
Infallible transactions
In an infallible transaction, a propagated domain error runs compensations and then rewinds for an immediate, retry-policy-independent restart. Unexpected failures follow host recovery semantics and do not guarantee compensation. In Effect, interruption triggers compensation: the fallible helper propagates the interruption, while the infallible helper rewinds.
TypeScript
In TypeScript, an infallible saga can be executed using the async infallibleSaga function, by
passing an async callback that can execute operations on the open transaction (see below).
Operations
Both transaction types require the definition of operations.
It is defined with the following interface:
TypeScript
/**
* Represents an atomic operation of the transaction which has a rollback action.
*
* Implement this interface and use it within a saga block.
* Compensable steps can also be constructed with `compensable`.
*/
export interface Compensable<In, Out, Err> {
/**
* The action to execute.
* @param input - The input to the operation.
* @returns A promise resolving to the result of the operation.
*/
execute(input: In): Promise<Result<Out, Err>>
/**
* Compensation to perform in case of failure.
* Compensations should not throw errors.
* @param input - The input to the operation.
* @param result - The result of the operation.
* @returns A promise resolving to the result of the compensation.
*/
compensate(input: In, result: Out): Promise<Result<void, Err>>
}There are two ways to define a compensable step:
- Implement the
Compensableinterface manually - Use the
compensablefunction to create one from a pair of closures
export function compensable<In, Out, Err>(
execute: (input: In) => Promise<Result<Out, Err>>,
compensate: (input: In, result: Out) => Promise<Result<void, Err>>,
): Compensable<In, Out, Err>Executing operations
The defined operations can be executed in fallible or infallible mode:
TypeScript
import { compensable, fallibleSaga, infallibleSaga, Result } from "@golemcloud/golem-ts-sdk"
// example operation with compensation
const op = compensable<number, string, string>(
async (idx) => {
// the operation / side effect
return Result.ok("id-" + idx)
},
async (idx, id) => {
// compensation
console.log(`reverting ${id}, ${idx}`)
return Result.ok(undefined)
}
)
// With fallibleSaga, errors are propagated using Result.
const resultFallible = await fallibleSaga(async tx => {
const firstId = await tx.execute(op, 1)
if (firstId.isErr()) return firstId
const secondId = await tx.execute(op, 2)
if (secondId.isErr()) return secondId
return Result.ok([firstId.val, secondId.val])
})
const resultInfallible = await infallibleSaga(async tx => {
const firstId = await tx.execute(op, 1)
const secondId = await tx.execute(op, 2)
return [firstId, secondId]
})