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 }