github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/storage/badger/transaction/deferred_block_persist.go (about) 1 package transaction 2 3 import ( 4 "github.com/onflow/flow-go/model/flow" 5 ) 6 7 // DeferredBlockPersistOp is a shorthand notation for an anonymous function that takes the ID of 8 // a fully constructed block and a `transaction.Tx` as inputs and runs some database operations 9 // as part of that transaction. It is a "Promise Pattern", essentially saying: 10 // once we have the completed the block's construction, we persist data structures that are 11 // referenced by the block or populate database indices. This pattern is necessary, because 12 // internally to the protocol_state package we don't have access to the candidate block ID yet because 13 // we are still determining the protocol state ID for that block. 14 type DeferredBlockPersistOp func(blockID flow.Identifier, tx *Tx) error 15 16 // noOpPersist intended as constant 17 var noOpPersist DeferredBlockPersistOp = func(blockID flow.Identifier, tx *Tx) error { return nil } 18 19 // WithBlock adds the still missing block ID information to a `DeferredBlockPersistOp`, thereby converting 20 // it into a `transaction.DeferredDBUpdate`. 21 func (d DeferredBlockPersistOp) WithBlock(blockID flow.Identifier) DeferredDBUpdate { 22 return func(tx *Tx) error { 23 return d(blockID, tx) 24 } 25 } 26 27 // DeferredBlockPersist is a utility for accumulating deferred database interactions that 28 // are supposed to be executed in one atomic transaction. It supports: 29 // - Deferred database operations that work directly on Badger transactions. 30 // - Deferred database operations that work on `transaction.Tx`. 31 // Tx is a storage-layer abstraction, with support for callbacks that are executed 32 // after the underlying database transaction completed _successfully_. 33 // - Deferred database operations that depend on the ID of the block under construction 34 // and `transaction.Tx`. Especially useful for populating `ByBlockID` indices. 35 // 36 // ORDER OF EXECUTION 37 // We extend the process in which `transaction.Tx` executes database operations, schedules 38 // callbacks, and executed the callbacks. Specifically, DeferredDbOps proceeds as follows: 39 // 40 // 0. Record functors added via `AddBadgerOp`, `AddDbOp`, `OnSucceed` ... 41 // • some functors may schedule callbacks (depending on their type), which are executed 42 // after the underlying database transaction completed _successfully_. 43 // • `OnSucceed` is treated exactly the same way: 44 // it schedules a callback during its execution, but it has no database actions. 45 // 1. Execute the functors in the order they were added 46 // 2. During each functor's execution: 47 // • some functors may schedule callbacks (depending on their type) 48 // • record those callbacks in the order they are scheduled (no execution yet) 49 // 3. If and only if the underlying database transaction succeeds, run the callbacks 50 // 51 // DESIGN PATTERN 52 // - DeferredDbOps is stateful, i.e. it needs to be passed as pointer variable. 53 // - Do not instantiate Tx directly. Instead, use one of the following 54 // transaction.Update(db, DeferredDbOps.Pending().WithBlock(blockID)) 55 // transaction.View(db, DeferredDbOps.Pending().WithBlock(blockID)) 56 // operation.RetryOnConflictTx(db, transaction.Update, DeferredDbOps.Pending().WithBlock(blockID)) 57 // 58 // NOT CONCURRENCY SAFE 59 type DeferredBlockPersist struct { 60 isEmpty bool 61 pending DeferredBlockPersistOp 62 } 63 64 // NewDeferredBlockPersist instantiates a DeferredBlockPersist. Initially, it behaves like a no-op until functors are added. 65 func NewDeferredBlockPersist() *DeferredBlockPersist { 66 return &DeferredBlockPersist{ 67 isEmpty: true, 68 pending: noOpPersist, // initially nothing is pending, i.e. no-op 69 } 70 } 71 72 // IsEmpty returns true if and only if there are zero pending database operations. 73 func (d *DeferredBlockPersist) IsEmpty() bool { 74 if d == nil { 75 return true 76 } 77 return d.isEmpty 78 } 79 80 // Pending returns a DeferredBlockPersistOp that comprises all database operations and callbacks 81 // that were added so far. Caution, DeferredBlockPersist keeps its internal state of deferred operations. 82 // Pending() can be called multiple times, but should only be executed in a database transaction 83 // once to avoid conflicts. 84 func (d *DeferredBlockPersist) Pending() DeferredBlockPersistOp { 85 if d == nil { 86 return noOpPersist 87 } 88 return d.pending 89 } 90 91 // AddBadgerOp schedules the given DeferredBadgerUpdate to be executed as part of the future transaction. 92 // For adding multiple DeferredBadgerUpdates, use `AddBadgerOps(ops ...DeferredBadgerUpdate)` if easily possible, as 93 // it reduces the call stack compared to adding the functors individually via `AddBadgerOp(op DeferredBadgerUpdate)`. 94 // This method returns a self-reference for chaining. 95 func (d *DeferredBlockPersist) AddBadgerOp(op DeferredBadgerUpdate) *DeferredBlockPersist { 96 prior := d.pending 97 d.pending = func(blockID flow.Identifier, tx *Tx) error { 98 err := prior(blockID, tx) 99 if err != nil { 100 return err 101 } 102 err = op(tx.DBTxn) 103 if err != nil { 104 return err 105 } 106 return nil 107 } 108 d.isEmpty = false 109 return d 110 } 111 112 // AddBadgerOps schedules the given DeferredBadgerUpdates to be executed as part of the future transaction. 113 // This method returns a self-reference for chaining. 114 func (d *DeferredBlockPersist) AddBadgerOps(ops ...DeferredBadgerUpdate) *DeferredBlockPersist { 115 if len(ops) < 1 { 116 return d 117 } 118 119 prior := d.pending 120 d.pending = func(blockID flow.Identifier, tx *Tx) error { 121 err := prior(blockID, tx) 122 if err != nil { 123 return err 124 } 125 for _, op := range ops { 126 err = op(tx.DBTxn) 127 if err != nil { 128 return err 129 } 130 } 131 return nil 132 } 133 d.isEmpty = false 134 return d 135 } 136 137 // AddDbOp schedules the given DeferredDBUpdate to be executed as part of the future transaction. 138 // For adding multiple DeferredBadgerUpdates, use `AddDbOps(ops ...DeferredDBUpdate)` if easily possible, as 139 // it reduces the call stack compared to adding the functors individually via `AddDbOp(op DeferredDBUpdate)`. 140 // This method returns a self-reference for chaining. 141 func (d *DeferredBlockPersist) AddDbOp(op DeferredDBUpdate) *DeferredBlockPersist { 142 prior := d.pending 143 d.pending = func(blockID flow.Identifier, tx *Tx) error { 144 err := prior(blockID, tx) 145 if err != nil { 146 return err 147 } 148 err = op(tx) 149 if err != nil { 150 return err 151 } 152 return nil 153 } 154 d.isEmpty = false 155 return d 156 } 157 158 // AddDbOps schedules the given DeferredDBUpdates to be executed as part of the future transaction. 159 // This method returns a self-reference for chaining. 160 func (d *DeferredBlockPersist) AddDbOps(ops ...DeferredDBUpdate) *DeferredBlockPersist { 161 if len(ops) < 1 { 162 return d 163 } 164 165 prior := d.pending 166 d.pending = func(blockID flow.Identifier, tx *Tx) error { 167 err := prior(blockID, tx) 168 if err != nil { 169 return err 170 } 171 for _, op := range ops { 172 err = op(tx) 173 if err != nil { 174 return err 175 } 176 } 177 return nil 178 } 179 d.isEmpty = false 180 return d 181 } 182 183 // AddIndexingOp schedules the given DeferredBlockPersistOp to be executed as part of the future transaction. 184 // Usually, these are operations to populate some `ByBlockID` index. 185 // For adding multiple DeferredBlockPersistOps, use `AddIndexingOps(ops ...DeferredBlockPersistOp)` if easily 186 // possible, as it reduces the call stack compared to adding the functors individually via 187 // `AddIndexOp(op DeferredBlockPersistOp)`. 188 // This method returns a self-reference for chaining. 189 func (d *DeferredBlockPersist) AddIndexingOp(op DeferredBlockPersistOp) *DeferredBlockPersist { 190 prior := d.pending 191 d.pending = func(blockID flow.Identifier, tx *Tx) error { 192 err := prior(blockID, tx) 193 if err != nil { 194 return err 195 } 196 err = op(blockID, tx) 197 if err != nil { 198 return err 199 } 200 return nil 201 } 202 d.isEmpty = false 203 return d 204 } 205 206 // AddIndexingOps schedules the given DeferredBlockPersistOp to be executed as part of the future transaction. 207 // Usually, these are operations to populate some `ByBlockID` index. 208 // This method returns a self-reference for chaining. 209 func (d *DeferredBlockPersist) AddIndexingOps(ops ...DeferredBlockPersistOp) *DeferredBlockPersist { 210 if len(ops) < 1 { 211 return d 212 } 213 214 prior := d.pending 215 d.pending = func(blockID flow.Identifier, tx *Tx) error { 216 err := prior(blockID, tx) 217 if err != nil { 218 return err 219 } 220 for _, op := range ops { 221 err = op(blockID, tx) 222 if err != nil { 223 return err 224 } 225 } 226 return nil 227 } 228 d.isEmpty = false 229 return d 230 } 231 232 // OnSucceed adds a callback to be executed after the deferred database operations have succeeded. For 233 // adding multiple callbacks, use `OnSucceeds(callbacks ...func())` if easily possible, as it reduces 234 // the call stack compared to adding the functors individually via `OnSucceed(callback func())`. 235 // This method returns a self-reference for chaining. 236 func (d *DeferredBlockPersist) OnSucceed(callback func()) *DeferredBlockPersist { 237 prior := d.pending 238 d.pending = func(blockID flow.Identifier, tx *Tx) error { 239 err := prior(blockID, tx) 240 if err != nil { 241 return err 242 } 243 tx.OnSucceed(callback) 244 return nil 245 } 246 d.isEmpty = false 247 return d 248 } 249 250 // OnSucceeds adds callbacks to be executed after the deferred database operations have succeeded. 251 // This method returns a self-reference for chaining. 252 func (d *DeferredBlockPersist) OnSucceeds(callbacks ...func()) *DeferredBlockPersist { 253 if len(callbacks) < 1 { 254 return d 255 } 256 257 prior := d.pending 258 d.pending = func(blockID flow.Identifier, tx *Tx) error { 259 err := prior(blockID, tx) 260 if err != nil { 261 return err 262 } 263 for _, c := range callbacks { 264 tx.OnSucceed(c) 265 } 266 return nil 267 } 268 d.isEmpty = false 269 return d 270 }