631 lines
18 KiB
Go
631 lines
18 KiB
Go
package stf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
appmanager "cosmossdk.io/core/app"
|
|
appmodulev2 "cosmossdk.io/core/appmodule/v2"
|
|
corecontext "cosmossdk.io/core/context"
|
|
"cosmossdk.io/core/event"
|
|
"cosmossdk.io/core/gas"
|
|
"cosmossdk.io/core/header"
|
|
"cosmossdk.io/core/log"
|
|
"cosmossdk.io/core/router"
|
|
"cosmossdk.io/core/store"
|
|
"cosmossdk.io/core/transaction"
|
|
stfgas "cosmossdk.io/server/v2/stf/gas"
|
|
"cosmossdk.io/server/v2/stf/internal"
|
|
)
|
|
|
|
// Identity defines STF's bytes identity and it's used by STF to store things in its own state.
|
|
var Identity = []byte("stf")
|
|
|
|
// STF is a struct that manages the state transition component of the app.
|
|
type STF[T transaction.Tx] struct {
|
|
logger log.Logger
|
|
|
|
msgRouter Router
|
|
queryRouter Router
|
|
|
|
doPreBlock func(ctx context.Context, txs []T) error
|
|
doBeginBlock func(ctx context.Context) error
|
|
doEndBlock func(ctx context.Context) error
|
|
doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error)
|
|
|
|
doTxValidation func(ctx context.Context, tx T) error
|
|
postTxExec func(ctx context.Context, tx T, success bool) error
|
|
|
|
branchFn branchFn // branchFn is a function that given a readonly state it returns a writable version of it.
|
|
makeGasMeter makeGasMeterFn
|
|
makeGasMeteredState makeGasMeteredStateFn
|
|
}
|
|
|
|
// NewSTF returns a new STF instance.
|
|
func NewSTF[T transaction.Tx](
|
|
logger log.Logger,
|
|
msgRouterBuilder *MsgRouterBuilder,
|
|
queryRouterBuilder *MsgRouterBuilder,
|
|
doPreBlock func(ctx context.Context, txs []T) error,
|
|
doBeginBlock func(ctx context.Context) error,
|
|
doEndBlock func(ctx context.Context) error,
|
|
doTxValidation func(ctx context.Context, tx T) error,
|
|
doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error),
|
|
postTxExec func(ctx context.Context, tx T, success bool) error,
|
|
branch func(store store.ReaderMap) store.WriterMap,
|
|
) (*STF[T], error) {
|
|
msgRouter, err := msgRouterBuilder.Build()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build msg router: %w", err)
|
|
}
|
|
queryRouter, err := queryRouterBuilder.Build()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build query router: %w", err)
|
|
}
|
|
|
|
return &STF[T]{
|
|
logger: logger,
|
|
msgRouter: msgRouter,
|
|
queryRouter: queryRouter,
|
|
doPreBlock: doPreBlock,
|
|
doBeginBlock: doBeginBlock,
|
|
doEndBlock: doEndBlock,
|
|
doValidatorUpdate: doValidatorUpdate,
|
|
doTxValidation: doTxValidation,
|
|
postTxExec: postTxExec, // TODO
|
|
branchFn: branch,
|
|
makeGasMeter: stfgas.DefaultGasMeter,
|
|
makeGasMeteredState: stfgas.DefaultWrapWithGasMeter,
|
|
}, nil
|
|
}
|
|
|
|
// DeliverBlock is our state transition function.
|
|
// It takes a read only view of the state to apply the block to,
|
|
// executes the block and returns the block results and the new state.
|
|
func (s STF[T]) DeliverBlock(
|
|
ctx context.Context,
|
|
block *appmanager.BlockRequest[T],
|
|
state store.ReaderMap,
|
|
) (blockResult *appmanager.BlockResponse, newState store.WriterMap, err error) {
|
|
// creates a new branchFn state, from the readonly view of the state
|
|
// that can be written to.
|
|
newState = s.branchFn(state)
|
|
hi := header.Info{
|
|
Hash: block.Hash,
|
|
AppHash: block.AppHash,
|
|
ChainID: block.ChainId,
|
|
Time: block.Time,
|
|
Height: int64(block.Height),
|
|
}
|
|
// set header info
|
|
err = s.setHeaderInfo(newState, hi)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to set initial header info, %w", err)
|
|
}
|
|
|
|
exCtx := s.makeContext(ctx, appmanager.ConsensusIdentity, newState, internal.ExecModeFinalize)
|
|
exCtx.setHeaderInfo(hi)
|
|
consMessagesResponses, err := s.runConsensusMessages(exCtx, block.ConsensusMessages)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to execute consensus messages: %w", err)
|
|
}
|
|
|
|
// reset events
|
|
exCtx.events = make([]event.Event, 0)
|
|
// pre block is called separate from begin block in order to prepopulate state
|
|
preBlockEvents, err := s.preBlock(exCtx, block.Txs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if err = isCtxCancelled(ctx); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// reset events
|
|
exCtx.events = make([]event.Event, 0)
|
|
|
|
// begin block
|
|
var beginBlockEvents []event.Event
|
|
if !block.IsGenesis {
|
|
// begin block
|
|
beginBlockEvents, err = s.beginBlock(exCtx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
// check if we need to return early
|
|
if err = isCtxCancelled(ctx); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// execute txs
|
|
txResults := make([]appmanager.TxResult, len(block.Txs))
|
|
// TODO: skip first tx if vote extensions are enabled (marko)
|
|
for i, txBytes := range block.Txs {
|
|
// check if we need to return early or continue delivering txs
|
|
if err = isCtxCancelled(ctx); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi)
|
|
}
|
|
// reset events
|
|
exCtx.events = make([]event.Event, 0)
|
|
// end block
|
|
endBlockEvents, valset, err := s.endBlock(exCtx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &appmanager.BlockResponse{
|
|
Apphash: nil,
|
|
ConsensusMessagesResponse: consMessagesResponses,
|
|
ValidatorUpdates: valset,
|
|
PreBlockEvents: preBlockEvents,
|
|
BeginBlockEvents: beginBlockEvents,
|
|
TxResults: txResults,
|
|
EndBlockEvents: endBlockEvents,
|
|
}, newState, nil
|
|
}
|
|
|
|
// deliverTx executes a TX and returns the result.
|
|
func (s STF[T]) deliverTx(
|
|
ctx context.Context,
|
|
state store.WriterMap,
|
|
tx T,
|
|
execMode transaction.ExecMode,
|
|
hi header.Info,
|
|
) appmanager.TxResult {
|
|
// recover in the case of a panic
|
|
var recoveryError error
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
recoveryError = fmt.Errorf("panic during transaction execution: %s", r)
|
|
s.logger.Error("panic during transaction execution", "error", recoveryError)
|
|
}
|
|
}()
|
|
// handle error from GetGasLimit
|
|
gasLimit, gasLimitErr := tx.GetGasLimit()
|
|
if gasLimitErr != nil {
|
|
return appmanager.TxResult{
|
|
Error: gasLimitErr,
|
|
}
|
|
}
|
|
|
|
if recoveryError != nil {
|
|
return appmanager.TxResult{
|
|
Error: recoveryError,
|
|
}
|
|
}
|
|
|
|
validateGas, validationEvents, err := s.validateTx(ctx, state, gasLimit, tx)
|
|
if err != nil {
|
|
return appmanager.TxResult{
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
execResp, execGas, execEvents, err := s.execTx(ctx, state, gasLimit-validateGas, tx, execMode, hi)
|
|
return appmanager.TxResult{
|
|
Events: append(validationEvents, execEvents...),
|
|
GasUsed: execGas + validateGas,
|
|
GasWanted: gasLimit,
|
|
Resp: execResp,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// validateTx validates a transaction given the provided WritableState and gas limit.
|
|
// If the validation is successful, state is committed
|
|
func (s STF[T]) validateTx(
|
|
ctx context.Context,
|
|
state store.WriterMap,
|
|
gasLimit uint64,
|
|
tx T,
|
|
) (gasUsed uint64, events []event.Event, err error) {
|
|
validateState := s.branchFn(state)
|
|
hi, err := s.getHeaderInfo(validateState)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
validateCtx := s.makeContext(ctx, appmanager.RuntimeIdentity, validateState, transaction.ExecModeCheck)
|
|
validateCtx.setHeaderInfo(hi)
|
|
validateCtx.setGasLimit(gasLimit)
|
|
err = s.doTxValidation(validateCtx, tx)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
consumed := validateCtx.meter.Limit() - validateCtx.meter.Remaining()
|
|
|
|
return consumed, validateCtx.events, applyStateChanges(state, validateState)
|
|
}
|
|
|
|
// execTx executes the tx messages on the provided state. If the tx fails then the state is discarded.
|
|
func (s STF[T]) execTx(
|
|
ctx context.Context,
|
|
state store.WriterMap,
|
|
gasLimit uint64,
|
|
tx T,
|
|
execMode transaction.ExecMode,
|
|
hi header.Info,
|
|
) ([]transaction.Msg, uint64, []event.Event, error) {
|
|
execState := s.branchFn(state)
|
|
|
|
msgsResp, gasUsed, runTxMsgsEvents, txErr := s.runTxMsgs(ctx, execState, gasLimit, tx, execMode, hi)
|
|
if txErr != nil {
|
|
// in case of error during message execution, we do not apply the exec state.
|
|
// instead we run the post exec handler in a new branchFn from the initial state.
|
|
postTxState := s.branchFn(state)
|
|
postTxCtx := s.makeContext(ctx, appmanager.RuntimeIdentity, postTxState, execMode)
|
|
postTxCtx.setHeaderInfo(hi)
|
|
|
|
postTxErr := s.postTxExec(postTxCtx, tx, false)
|
|
if postTxErr != nil {
|
|
// if the post tx handler fails, then we do not apply any state change to the initial state.
|
|
// we just return the exec gas used and a joined error from TX error and post TX error.
|
|
return nil, gasUsed, nil, errors.Join(txErr, postTxErr)
|
|
}
|
|
// in case post tx is successful, then we commit the post tx state to the initial state,
|
|
// and we return post tx events alongside exec gas used and the error of the tx.
|
|
applyErr := applyStateChanges(state, postTxState)
|
|
if applyErr != nil {
|
|
return nil, 0, nil, applyErr
|
|
}
|
|
return nil, gasUsed, postTxCtx.events, txErr
|
|
}
|
|
// tx execution went fine, now we use the same state to run the post tx exec handler,
|
|
// in case the execution of the post tx fails, then no state change is applied and the
|
|
// whole execution step is rolled back.
|
|
postTxCtx := s.makeContext(ctx, appmanager.RuntimeIdentity, execState, execMode) // NO gas limit.
|
|
postTxCtx.setHeaderInfo(hi)
|
|
postTxErr := s.postTxExec(postTxCtx, tx, true)
|
|
if postTxErr != nil {
|
|
// if post tx fails, then we do not apply any state change, we return the post tx error,
|
|
// alongside the gas used.
|
|
return nil, gasUsed, nil, postTxErr
|
|
}
|
|
// both the execution and post tx execution step were successful, so we apply the state changes
|
|
// to the provided state, and we return responses, and events from exec tx and post tx exec.
|
|
applyErr := applyStateChanges(state, execState)
|
|
if applyErr != nil {
|
|
return nil, 0, nil, applyErr
|
|
}
|
|
|
|
return msgsResp, gasUsed, append(runTxMsgsEvents, postTxCtx.events...), nil
|
|
}
|
|
|
|
// runTxMsgs will execute the messages contained in the TX with the provided state.
|
|
func (s STF[T]) runTxMsgs(
|
|
ctx context.Context,
|
|
state store.WriterMap,
|
|
gasLimit uint64,
|
|
tx T,
|
|
execMode transaction.ExecMode,
|
|
hi header.Info,
|
|
) ([]transaction.Msg, uint64, []event.Event, error) {
|
|
txSenders, err := tx.GetSenders()
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
msgs, err := tx.GetMessages()
|
|
if err != nil {
|
|
return nil, 0, nil, err
|
|
}
|
|
msgResps := make([]transaction.Msg, len(msgs))
|
|
|
|
execCtx := s.makeContext(ctx, nil, state, execMode)
|
|
execCtx.setHeaderInfo(hi)
|
|
execCtx.setGasLimit(gasLimit)
|
|
for i, msg := range msgs {
|
|
execCtx.sender = txSenders[i]
|
|
resp, err := s.msgRouter.InvokeUntyped(execCtx, msg)
|
|
if err != nil {
|
|
return nil, 0, nil, fmt.Errorf("message execution at index %d failed: %w", i, err)
|
|
}
|
|
msgResps[i] = resp
|
|
}
|
|
|
|
consumed := execCtx.meter.Limit() - execCtx.meter.Remaining()
|
|
return msgResps, consumed, execCtx.events, nil
|
|
}
|
|
|
|
func (s STF[T]) preBlock(
|
|
ctx *executionContext,
|
|
txs []T,
|
|
) ([]event.Event, error) {
|
|
err := s.doPreBlock(ctx, txs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, e := range ctx.events {
|
|
ctx.events[i].Attributes = append(
|
|
e.Attributes,
|
|
event.Attribute{Key: "mode", Value: "PreBlock"},
|
|
)
|
|
}
|
|
|
|
return ctx.events, nil
|
|
}
|
|
|
|
func (s STF[T]) runConsensusMessages(
|
|
ctx *executionContext,
|
|
messages []transaction.Msg,
|
|
) ([]transaction.Msg, error) {
|
|
responses := make([]transaction.Msg, len(messages))
|
|
for i := range messages {
|
|
resp, err := s.msgRouter.InvokeUntyped(ctx, messages[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
responses[i] = resp
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
func (s STF[T]) beginBlock(
|
|
ctx *executionContext,
|
|
) (beginBlockEvents []event.Event, err error) {
|
|
err = s.doBeginBlock(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, e := range ctx.events {
|
|
ctx.events[i].Attributes = append(
|
|
e.Attributes,
|
|
event.Attribute{Key: "mode", Value: "BeginBlock"},
|
|
)
|
|
}
|
|
|
|
return ctx.events, nil
|
|
}
|
|
|
|
func (s STF[T]) endBlock(
|
|
ctx *executionContext,
|
|
) ([]event.Event, []appmodulev2.ValidatorUpdate, error) {
|
|
err := s.doEndBlock(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
events, valsetUpdates, err := s.validatorUpdates(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ctx.events = append(ctx.events, events...)
|
|
|
|
for i, e := range ctx.events {
|
|
ctx.events[i].Attributes = append(
|
|
e.Attributes,
|
|
event.Attribute{Key: "mode", Value: "BeginBlock"},
|
|
)
|
|
}
|
|
|
|
return ctx.events, valsetUpdates, nil
|
|
}
|
|
|
|
// validatorUpdates returns the validator updates for the current block. It is called by endBlock after the endblock execution has concluded
|
|
func (s STF[T]) validatorUpdates(
|
|
ctx *executionContext,
|
|
) ([]event.Event, []appmodulev2.ValidatorUpdate, error) {
|
|
valSetUpdates, err := s.doValidatorUpdate(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return ctx.events, valSetUpdates, nil
|
|
}
|
|
|
|
// Simulate simulates the execution of a tx on the provided state.
|
|
func (s STF[T]) Simulate(
|
|
ctx context.Context,
|
|
state store.ReaderMap,
|
|
gasLimit uint64,
|
|
tx T,
|
|
) (appmanager.TxResult, store.WriterMap) {
|
|
simulationState := s.branchFn(state)
|
|
hi, err := s.getHeaderInfo(simulationState)
|
|
if err != nil {
|
|
return appmanager.TxResult{}, nil
|
|
}
|
|
txr := s.deliverTx(ctx, simulationState, tx, internal.ExecModeSimulate, hi)
|
|
|
|
return txr, simulationState
|
|
}
|
|
|
|
// ValidateTx will run only the validation steps required for a transaction.
|
|
// Validations are run over the provided state, with the provided gas limit.
|
|
func (s STF[T]) ValidateTx(
|
|
ctx context.Context,
|
|
state store.ReaderMap,
|
|
gasLimit uint64,
|
|
tx T,
|
|
) appmanager.TxResult {
|
|
validationState := s.branchFn(state)
|
|
gasUsed, events, err := s.validateTx(ctx, validationState, gasLimit, tx)
|
|
return appmanager.TxResult{
|
|
Events: events,
|
|
GasUsed: gasUsed,
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
// Query executes the query on the provided state with the provided gas limits.
|
|
func (s STF[T]) Query(
|
|
ctx context.Context,
|
|
state store.ReaderMap,
|
|
gasLimit uint64,
|
|
req transaction.Msg,
|
|
) (transaction.Msg, error) {
|
|
queryState := s.branchFn(state)
|
|
hi, err := s.getHeaderInfo(queryState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
queryCtx := s.makeContext(ctx, nil, queryState, internal.ExecModeSimulate)
|
|
queryCtx.setHeaderInfo(hi)
|
|
queryCtx.setGasLimit(gasLimit)
|
|
return s.queryRouter.InvokeUntyped(queryCtx, req)
|
|
}
|
|
|
|
// RunWithCtx is made to support genesis, if genesis was just the execution of messages instead
|
|
// of being something custom then we would not need this. PLEASE DO NOT USE.
|
|
// TODO: Remove
|
|
func (s STF[T]) RunWithCtx(
|
|
ctx context.Context,
|
|
state store.ReaderMap,
|
|
closure func(ctx context.Context) error,
|
|
) (store.WriterMap, error) {
|
|
branchedState := s.branchFn(state)
|
|
stfCtx := s.makeContext(ctx, nil, branchedState, internal.ExecModeFinalize)
|
|
return branchedState, closure(stfCtx)
|
|
}
|
|
|
|
// clone clones STF.
|
|
func (s STF[T]) clone() STF[T] {
|
|
return STF[T]{
|
|
logger: s.logger,
|
|
msgRouter: s.msgRouter,
|
|
queryRouter: s.queryRouter,
|
|
doPreBlock: s.doPreBlock,
|
|
doBeginBlock: s.doBeginBlock,
|
|
doEndBlock: s.doEndBlock,
|
|
doValidatorUpdate: s.doValidatorUpdate,
|
|
doTxValidation: s.doTxValidation,
|
|
postTxExec: s.postTxExec,
|
|
branchFn: s.branchFn,
|
|
makeGasMeter: s.makeGasMeter,
|
|
makeGasMeteredState: s.makeGasMeteredState,
|
|
}
|
|
}
|
|
|
|
// executionContext is a struct that holds the context for the execution of a tx.
|
|
type executionContext struct {
|
|
context.Context
|
|
|
|
// unmeteredState is storage without metering. Changes here are propagated to state which is the metered
|
|
// version.
|
|
unmeteredState store.WriterMap
|
|
// state is the gas metered state.
|
|
state store.WriterMap
|
|
// meter is the gas meter.
|
|
meter gas.Meter
|
|
// events are the current events.
|
|
events []event.Event
|
|
// sender is the causer of the state transition.
|
|
sender transaction.Identity
|
|
// headerInfo contains the block info.
|
|
headerInfo header.Info
|
|
// execMode retains information about the exec mode.
|
|
execMode transaction.ExecMode
|
|
|
|
branchFn branchFn
|
|
makeGasMeter makeGasMeterFn
|
|
makeGasMeteredStore makeGasMeteredStateFn
|
|
|
|
msgRouter router.Service
|
|
queryRouter router.Service
|
|
}
|
|
|
|
// setHeaderInfo sets the header info in the state to be used by queries in the future.
|
|
func (e *executionContext) setHeaderInfo(hi header.Info) {
|
|
e.headerInfo = hi
|
|
}
|
|
|
|
// setGasLimit will update the gas limit of the *executionContext
|
|
func (e *executionContext) setGasLimit(limit uint64) {
|
|
meter := e.makeGasMeter(limit)
|
|
meteredState := e.makeGasMeteredStore(meter, e.unmeteredState)
|
|
|
|
e.meter = meter
|
|
e.state = meteredState
|
|
}
|
|
|
|
// TODO: too many calls to makeContext can be expensive
|
|
// makeContext creates and returns a new execution context for the STF[T] type.
|
|
// It takes in the following parameters:
|
|
// - ctx: The context.Context object for the execution.
|
|
// - sender: The transaction.Identity object representing the sender of the transaction.
|
|
// - state: The store.WriterMap object for accessing and modifying the state.
|
|
// - gasLimit: The maximum amount of gas allowed for the execution.
|
|
// - execMode: The corecontext.ExecMode object representing the execution mode.
|
|
//
|
|
// It returns a pointer to the executionContext struct
|
|
func (s STF[T]) makeContext(
|
|
ctx context.Context,
|
|
sender transaction.Identity,
|
|
store store.WriterMap,
|
|
execMode transaction.ExecMode,
|
|
) *executionContext {
|
|
valuedCtx := context.WithValue(ctx, corecontext.ExecModeKey, execMode)
|
|
return newExecutionContext(
|
|
valuedCtx,
|
|
s.makeGasMeter,
|
|
s.makeGasMeteredState,
|
|
s.branchFn,
|
|
sender,
|
|
store,
|
|
execMode,
|
|
s.msgRouter,
|
|
s.queryRouter,
|
|
)
|
|
}
|
|
|
|
func newExecutionContext(
|
|
ctx context.Context,
|
|
makeGasMeterFn makeGasMeterFn,
|
|
makeGasMeteredStoreFn makeGasMeteredStateFn,
|
|
branchFn branchFn,
|
|
sender transaction.Identity,
|
|
state store.WriterMap,
|
|
execMode transaction.ExecMode,
|
|
msgRouter Router,
|
|
queryRouter Router,
|
|
) *executionContext {
|
|
meter := makeGasMeterFn(gas.NoGasLimit)
|
|
meteredState := makeGasMeteredStoreFn(meter, state)
|
|
|
|
return &executionContext{
|
|
Context: ctx,
|
|
unmeteredState: state,
|
|
state: meteredState,
|
|
meter: meter,
|
|
events: make([]event.Event, 0),
|
|
sender: sender,
|
|
headerInfo: header.Info{},
|
|
execMode: execMode,
|
|
branchFn: branchFn,
|
|
makeGasMeter: makeGasMeterFn,
|
|
makeGasMeteredStore: makeGasMeteredStoreFn,
|
|
msgRouter: msgRouter,
|
|
queryRouter: queryRouter,
|
|
}
|
|
}
|
|
|
|
// applyStateChanges applies the state changes from the source store to the destination store.
|
|
// It retrieves the state changes from the source store using GetStateChanges method,
|
|
// and then applies those changes to the destination store using ApplyStateChanges method.
|
|
// If an error occurs during the retrieval or application of state changes, it is returned.
|
|
func applyStateChanges(dst, src store.WriterMap) error {
|
|
changes, err := src.GetStateChanges()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return dst.ApplyStateChanges(changes)
|
|
}
|
|
|
|
// isCtxCancelled reports if the context was canceled.
|
|
func isCtxCancelled(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|