Welcome to the new Golem Cloud Docs! 👋
Documentation
Go Language Guide
Transactions

High level transactions in Go

On top of the durability controls and retry controls, the golem-go (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-go 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 transcation.WithFallible 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 transaction.WithInfallible function, by passing a closure 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 in the transcation package:

type Operation[I any, O any] interface {
	Execute(I) (O, error)
	Compensate(I, O) error
}

There are two ways to define an operation:

  1. Implement the Operation interface manually

  2. Use the transaction.NewOperation function to create an operation from a pair of closures

func NewOperation[I any, O any](
	execute func(I) (O, error),
	compensate func(I, O) error,
) Operation[I, O]

Executing operations

The defined operations can be executed in fallible or infallible mode:

import "github.com/golemcloud/golem-go/golemhost/transaction"
 
// example operation entity
type Entity struct {
	ID string
}
 
// example transaction result
type Result struct {
	entity1 Entity
	entity2 Entity
}
 
// example operation with compensation
var op := transaction.NewOperation(
	// sample execute - create entity
	func(stepID int64) (Entity, error) {
		return Entity{ID: fmt.Sprintf("entity-%d", stepID)}, nil
	},
	// sample compensate - revert entity
	func(stepID int64, entity Entity) error {
		fmt.Printf("Reverting entity: %s, created at step: %d", entity.ID, stepID)
		return nil
	},
)
 
// with transaction.Fallible errors have to be handled and propagated
result, err := transaction.Fallible(func(tx transaction.FallibleTx) (Result, error) {
	entity1, err := transaction.ExecuteFallible(tx, op, 1)
	if err != nil {
		return Result{}, err
	}
 
	entity2, err := transaction.ExecuteFallible(tx, op, 2)
	if err != nil {
		return Result{}, err
	}
 
	return Result{
		entity1: entity1,
		entity2: entity2,
	}, nil
})
 
// with transaction.Infallible no explicit error handling is needed, as it is handled by Golem retries
result := transaction.Infallible(func(tx transaction.InfallibleTx) Result {
	entity1 := transaction.ExecuteInfallible(tx, op, 1)
	entity2 := transaction.ExecuteInfallible(tx, op, 2)
 
	return Result{
		entity1: entity1,
		entity2: entity2,
	}
})