High level transactions in JavaScript
On top of the durability controls and retry controls, the golem-ts (opens in a new tab) library also provides a 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, many times a set of external operations such as HTTP requests needs to be executed transactionally; if one of the operations fails, the whole transaction need to be rolled back by executing some compensation actions.
The golem-ts
library provides support for two different types of transactions:
- fallible transactions are only dealing with domain errors
- infallible transactions must always succeed, and Golem applies its active retry policy to it
Fallible transactions
Many times external operations (such as HTTP calls to remote hosts) need to be executed transactionally. If some of the operations failed the transaction need to be rolled back - compensation actions need to undo whatever the already successfully performed operations did.
A fallible transaction only deals with domain errors. Within the transaction every operation that succeeds gets recorded. If an operation fails, all the recorded operations get compensated in reverse order before the transaction block returns with a failure.
A fallible transaction can be executed using the fallibleTransaction
function, by passing a closure that can execute operations on the open transaction (see below).
Infallible transactions
An infallible transaction must always succeed - in case of a failure or interruption, it gets retried. If there is a domain error, the compensation actions are executed before the retry.
An infallible transaction can be executed using the infallibleTransaction
function, by passing a closure that can execute operations on the open transaction (see below).
Operations
Both transaction types require the definition of operations.
As documentation, here is the TypeScript interface that operations has to match:
/**
* Represents an atomic operation of the transaction which has a rollback action.
*
* Implement this interface and use it within a `transaction` block.
* Operations can also be constructed from closures using `operation`.
*/
export interface Operation<In, Out, Err> {
/**
* The action to execute.
* @param input - The input to the operation.
* @returns The result of the operation.
*/
execute(input: In): 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 The result of the compensation.
*/
compensate(input: In, result: Out): Result<void, Err>
}
In JavaScript there are two ways to define an operation that matches the above interface:
- Implement the
Operation
interface manually
const op = {
execute(input) {
// execute operation, then return success
return Result.ok({ id: "value" })
// or error
// return Result.err("error");
},
compensate(input, result) {
// revert operation, then return success
return Result.unit()
// or error
// return Result.err("error");
},
}
- Use the
operation
function to create an operation from a pair of closures
const op = operation(
input => {
// execute operation, then return success
return Result.ok({ id: "value" })
// or error
// return Result.err("error");
},
(input, result) => {
return Result.unit()
// or error
// return Result.err("error");
}
)
Executing operations
The defined operations can be executed in fallible or infallible mode:
import { fallibleTransaction, infallibleTransaction, operation, Result } from "@golemcloud/golem-ts"
// example operation with compensation
const op = operation(
idx => {
// the operation / side effect
return Result.ok("id-" + idx)
},
(id, idx) => {
// compensation
console.log(`reverting ${id}, ${idx}`)
return Result.unit()
}
)
// with fallibleTransaction errors have to be handled and propagated using the Result type
const resultFallible = fallibleTransaction(tx => {
return tx
.execute(op, 1)
.flatMap(firstId => tx.execute(op, 2).map(secondId => [firstId, secondId]))
})
// with infallibleTransaction no explicit error handling is needed, as it is handled by Golem retries
const resultInfallible = infallibleTransaction(tx => {
const firstId = tx.execute(op, 1)
const secondId = tx.execute(op, 1)
return [firstId, secondId]
})