github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/storage/badger/transaction/tx.go (about)

     1  package transaction
     2  
     3  import (
     4  	dbbadger "github.com/dgraph-io/badger/v2"
     5  
     6  	ioutils "github.com/onflow/flow-go/utils/io"
     7  )
     8  
     9  // Tx wraps a badger transaction and includes and additional slice for callbacks.
    10  // The callbacks are executed after the badger transaction completed _successfully_.
    11  // DESIGN PATTERN
    12  //   - DBTxn should never be nil
    13  //   - at initialization, `callbacks` is empty
    14  //   - While business logic code operates on `DBTxn`, it can append additional callbacks
    15  //     via the `OnSucceed` method. This generally happens during the transaction execution.
    16  //
    17  // CAUTION:
    18  //   - Tx is stateful (calls to `OnSucceed` change its internal state).
    19  //     Therefore, Tx needs to be passed as pointer variable.
    20  //   - Do not instantiate Tx outside of this package. Instead, use `Update` or `View`
    21  //     functions.
    22  //   - Whether a transaction is considered to have succeeded depends only on the return value
    23  //     of the outermost function. For example, consider a chain of 3 functions: f3( f2( f1(x)))
    24  //     Lets assume f1 fails with an `storage.ErrAlreadyExists` sentinel, which f2 expects and
    25  //     therefore discards. f3 could then succeed, i.e. return nil.
    26  //     Consequently, the entire list of callbacks is executed, including f1's callback if it
    27  //     added one. Callback implementations therefore need to account for this edge case.
    28  //   - not concurrency safe
    29  type Tx struct {
    30  	DBTxn     *dbbadger.Txn
    31  	callbacks []func()
    32  }
    33  
    34  // OnSucceed adds a callback to execute after the batch has been successfully flushed.
    35  // Useful for implementing the cache where we will only cache after the batch of database
    36  // operations has been successfully applied.
    37  // CAUTION:
    38  // Whether a transaction is considered to have succeeded depends only on the return value
    39  // of the outermost function. For example, consider a chain of 3 functions: f3( f2( f1(x)))
    40  // Lets assume f1 fails with an `storage.ErrAlreadyExists` sentinel, which f2 expects and
    41  // therefore discards. f3 could then succeed, i.e. return nil.
    42  // Consequently, the entire list of callbacks is executed, including f1's callback if it
    43  // added one. Callback implementations therefore need to account for this edge case.
    44  func (b *Tx) OnSucceed(callback func()) {
    45  	b.callbacks = append(b.callbacks, callback)
    46  }
    47  
    48  // Update creates a badger transaction, passing it to a chain of functions.
    49  // Only if transaction succeeds, we run `callbacks` that were appended during the
    50  // transaction execution. The callbacks are useful update caches in order to reduce
    51  // cache misses.
    52  func Update(db *dbbadger.DB, f func(*Tx) error) error {
    53  	dbTxn := db.NewTransaction(true)
    54  	err := run(f, dbTxn)
    55  	dbTxn.Discard()
    56  	return err
    57  }
    58  
    59  // View creates a read-only badger transaction, passing it to a chain of functions.
    60  // Only if transaction succeeds, we run `callbacks` that were appended during the
    61  // transaction execution. The callbacks are useful update caches in order to reduce
    62  // cache misses.
    63  func View(db *dbbadger.DB, f func(*Tx) error) error {
    64  	dbTxn := db.NewTransaction(false)
    65  	err := run(f, dbTxn)
    66  	dbTxn.Discard()
    67  	return err
    68  }
    69  
    70  func run(f func(*Tx) error, dbTxn *dbbadger.Txn) error {
    71  	tx := &Tx{DBTxn: dbTxn}
    72  	err := f(tx)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	err = dbTxn.Commit()
    78  	if err != nil {
    79  		return ioutils.TerminateOnFullDisk(err)
    80  	}
    81  
    82  	for _, callback := range tx.callbacks {
    83  		callback()
    84  	}
    85  	return nil
    86  }
    87  
    88  // Fail returns an anonymous function, whose future execution returns the error e. This
    89  // is useful for front-loading sanity checks. On the happy path (dominant), this function
    90  // will generally not be used. However, if one of the front-loaded sanity checks fails,
    91  // we include `transaction.Fail(e)` in place of the business logic handling the happy path.
    92  func Fail(e error) func(*Tx) error {
    93  	return func(tx *Tx) error {
    94  		return e
    95  	}
    96  }
    97  
    98  // WithTx is useful when transaction is used without adding callback.
    99  func WithTx(f func(*dbbadger.Txn) error) func(*Tx) error {
   100  	return func(tx *Tx) error {
   101  		return f(tx.DBTxn)
   102  	}
   103  }