transaction/transaction.go (152 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package transaction // import "github.com/mozilla/OneCRL-Tools/transaction" import ( "sync" "github.com/pkg/errors" ) // A unit of work is any function that can report whether or not it succeeded. // Most idiomatically, this will typically be a closure which captures the state // it is intended to mutate/rollback/close. type Work = func() error type Rollback = func(cause error) error // NOOP is a convenience function for explicitly declaring that no // particular behavior is intended for a specific unit of work. func NOOP() error { return nil } func NOOPRollback(_ error) error { return nil } // A Transactor is any type which can move some state forward via its Commit // function, rollback that state via the Rollback function, and (if necessary) // destruct any resources it may be holding via the Close function. type Transactor interface { Commit() error Rollback(cause error) error Close() error } // A Transaction is the basic unit of work that should encapsulate a single // change in state (to the best of your ability). Idiomatically, this is usually // a struct that contains closures, which have themselves captured the target // pointers for state mutation. // // A Transaction object ITSELF is not thread safe (the setters are not in any way locked). // However, you may wish to lock your own data that is being captured by a given transaction. // In this case, a common pattern is to build a Transaction whose Commit is the capture // of a lock and whose Close is the release of said lock. This may then be given as a // step in a particular Transactions object. For example... // // l := sync.Mutex{} // state := 0 // err := Start(). // Then(NewTransaction(). // WithCommit(func() error { // l.Lock() // return nil // }). // WithClose(func() error { // l.Unlock() // return nil // })). // Then(NewTransaction(). // WithCommit(func() error { // state += 1 // return nil // })). // AutoClose(true).Commit() // type Transaction struct { commit Work rollback Rollback close Work commitRunner sync.Once rollbackRunner sync.Once closeRunner sync.Once } func NewTransaction() *Transaction { return &Transaction{ commit: NOOP, rollback: NOOPRollback, close: NOOP, } } // Sets the inner commit function. // A nil input defaults to NOOP. func (tx *Transaction) WithCommit(commit Work) *Transaction { if commit == nil { tx.commit = NOOP } else { tx.commit = commit } return tx } // Sets the inner rollback function. // A nil input defaults to NOOP. func (tx *Transaction) WithRollback(rollback Rollback) *Transaction { if rollback == nil { tx.rollback = NOOPRollback } else { tx.rollback = rollback } return tx } // Sets the inner close function. // A nil input defaults to NOOP. func (tx *Transaction) WithClose(close Work) *Transaction { if close == nil { tx.close = NOOP } else { tx.close = close } return tx } // Runs the configured commit function. // This action effectively "consumes" the // inner function. func (tx *Transaction) Commit() (err error) { tx.commitRunner.Do(func() { err = tx.commit() }) return err } // Runs the configured rollback function. // This action effectively "consumes" the // inner function. func (tx *Transaction) Rollback(cause error) (err error) { tx.rollbackRunner.Do(func() { err = tx.rollback(cause) }) return err } // Runs the configured close function. // This action effectively "consumes" the // inner function. func (tx *Transaction) Close() (err error) { tx.closeRunner.Do(func() { err = tx.close() }) return err } // A Transactions can encapsulate any number of individual // Transactor interfaces and manage their execution. // // A Transactions is itself a Transactor, meaning that this // relationship is recursive. That is, calling the Commit // method of a Transactions will run all of its composited // Transactors, of which any number of them may be themselves // another Transactions. The same holds true for the Rollback // and Close methods. // // Individual Transactors are committed in a FIFO manner relative // to their additions via the Then method. type Transactions struct { txQueue []Transactor rollbackStack []Transactor autoClose bool autoRollback bool } func Start() *Transactions { return &Transactions{ txQueue: []Transactor{}, rollbackStack: []Transactor{}, } } // AutoClose sets a flag that is checked in Commit. If AutoClose is // true, then the Transactions.Close function is deferred before // any attempts to commit are executed. // // If AutoRollbackonError is set then this closure will be executed // AFTER the rollbacks are attempted (if they are attempted). // // If any errors occur during closure then they will be wrapped up // and reported by the Commit procedure itself. func (txs *Transactions) AutoClose(should bool) *Transactions { txs.autoClose = should return txs } // AutoRollbackOnError sets a flag that is checked in Commit. If // AutoRollbackOnError is true then a function is deferred that checks // the result of Commit. If the returned error is non-nil, then // Transactions.Rollback is called. Else, if error is nil then // no operations is taken. // // If any errors occur during rollback then they will be wrapped up // and reported by the Commit procedure itself. func (txs *Transactions) AutoRollbackOnError(should bool) *Transactions { txs.autoRollback = should return txs } // Then is a fluid interface for building Transactions. // // txs := transaction.Start(). // Then(...). // Then(...). // Then(...) // defer txs.Close() // txs.Commit() func (txs *Transactions) Then(tx Transactor) *Transactions { txs.txQueue = append(txs.txQueue, tx) return txs } // Commit commits all composited transactors in a FIFO manner. // An error is returned immediately upon the failure of a single // commit. func (txs *Transactions) Commit() (err error) { errors := new(wrappedErrors) defer func() { err = errors.inner }() if txs.autoClose { defer func() { errors.add(txs.Close()) }() } if txs.autoRollback { defer func() { if errors.inner != nil { cause := errors.inner errors.add(txs.Rollback(cause)) } }() } for _, tx := range txs.txQueue { txs.rollbackStack = append(txs.rollbackStack, tx) if e := tx.Commit(); e != nil { errors.add(e) break } } return err } // Rollback rolls back any transactor which had its // Commit method called (whether it returned and error or not). // // This rollback is done in a LIFO manner. func (txs *Transactions) Rollback(cause error) error { err := wrappedErrors{} for i := len(txs.rollbackStack) - 1; i >= 0; i-- { err.add(txs.rollbackStack[i].Rollback(cause)) } return err.inner } // Close closes out all composited transactors. // // Closing is done a FIFO manner and is done all // composited transactors if-and-only if their // commit function was called. func (txs *Transactions) Close() error { err := wrappedErrors{} for i := len(txs.rollbackStack) - 1; i >= 0; i-- { err.add(txs.rollbackStack[i].Close()) } return err.inner } // wrappedErrors is a helper struct to encapsulate // the notion that we can have no error, a single // error, or a cascade of errors. type wrappedErrors struct { inner error } func (w *wrappedErrors) add(err error) { if err == nil { return } else if w.inner == nil { w.inner = err } else { w.inner = errors.Wrap(err, w.inner.Error()) } }