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:
-
Implement the
Operation
interface manually -
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,
}
})