Skip to content

Instantly share code, notes, and snippets.

@jiahaog
Created June 22, 2017 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jiahaog/eb7dc4bf111e6f31fcc709b8a4199cc4 to your computer and use it in GitHub Desktop.
Save jiahaog/eb7dc4bf111e6f31fcc709b8a4199cc4 to your computer and use it in GitHub Desktop.
Simulating transactions in Go
package main
// This file attempts to simulate trasactions, that can be used to run tasks in a chain and
// rollback everything if the chain fails. For example, this can be used to simulate a database
// transaction to clean up dirty data.
// While I am aware that this abstraction is not truely a atomic transaction as it does not
// obtain locks on the affected objects, it is sufficient for our use cases to cleanup dirty data
const txLogTag = "transaction"
// Transform specifies a series of functions that have a "Up" step and a "Down" step in case of
// rollback.
// For Up methods, it will be executed with the previous result of the transform in the chain
// (if any), and will return a result which is stored and will be used as a parameter to the
// Down(arg) method of the same transform if rolling back.
// The returned result of Down() txMethods is ignored
type Transform struct {
Up txMethod
Down txMethod
// Name is used for logging in case it fails
Name string
// result is used to keep track of results from the transform, and will
// be used the argument to Down() if a rollback is needed
result interface{}
}
// A txMethod is a up or down step in a transform.
type txMethod func(interface{}) (interface{}, error)
type transaction struct {
transforms []*Transform
name string
}
// RunTX takes all transforms in the transaction and runs them sequentially. The result of the last
// transform is returned, and the error (if any) of the failing transform (when doing the happy
// path). In the case of a failure to rollback the transforms, and an inconsistent state is
// reached, the rollback chain will be stopped immediately (subsequent rollbacks will not be
// executed), and the error will be logged
func RunTX(name string, transforms ...*Transform) (interface{}, error) {
tx := createTX(name, transforms...)
return tx.Run()
}
func createTX(name string, transforms ...*Transform) *transaction {
var transformSlice []*Transform
for _, transform := range transforms {
transformSlice = append(transformSlice, transform)
}
return &transaction{transforms, name}
}
func (t *transaction) Run() (interface{}, error) {
// prevArg tracks the output from the previous transform that will be passed as a
// parameter to the next transform (when doing the happy path)
var prevArg interface{}
// errorWhenFailed keeps track of the main error that causes the happy path to fail
var errorWhenFailed error
var completed []*Transform
// Run happy path
for _, transform := range t.transforms {
result, err := transform.Up(prevArg)
// Stop chain if it fails
if err != nil {
errorWhenFailed = err
break
}
// save output of transform (in case we have to rollback)
transform.result = result
prevArg = result
completed = append(completed, transform)
}
// If allizwell, return result of last transform
if errorWhenFailed == nil {
return prevArg, nil
}
// Rollback everything that has been completed
for i := len(completed) - 1; i >= 0; i-- {
toRollback := completed[i]
_, err := toRollback.Down(toRollback.result)
logging.Debug(txLogTag, "ROLLBACK: [Transaction: %v][Transform: %v]", t.name, toRollback.Name)
if err != nil {
logging.Error(txLogTag, "Error rolling back, inconsistent state reached in transaction %v. transform %v failed.", t.name, toRollback.Name)
}
}
return nil, errorWhenFailed
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment