Skip to Content
DevelopTransactions

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.

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.

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:

/** * 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:

  1. Implement the Compensable interface manually
  2. Use the compensable function 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:

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] })