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  }