github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/execution/computation/query/executor.go (about)

     1  package query
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/onflow/flow-go/fvm/errors"
    12  
    13  	jsoncdc "github.com/onflow/cadence/encoding/json"
    14  	"github.com/rs/zerolog"
    15  
    16  	"github.com/onflow/flow-go/fvm"
    17  	"github.com/onflow/flow-go/fvm/storage/derived"
    18  	"github.com/onflow/flow-go/fvm/storage/snapshot"
    19  	"github.com/onflow/flow-go/model/flow"
    20  	"github.com/onflow/flow-go/module"
    21  	"github.com/onflow/flow-go/utils/debug"
    22  	"github.com/onflow/flow-go/utils/rand"
    23  )
    24  
    25  const (
    26  	DefaultLogTimeThreshold    = 1 * time.Second
    27  	DefaultExecutionTimeLimit  = 10 * time.Second
    28  	DefaultMaxErrorMessageSize = 1000 // 1000 chars
    29  )
    30  
    31  type Executor interface {
    32  	ExecuteScript(
    33  		ctx context.Context,
    34  		script []byte,
    35  		arguments [][]byte,
    36  		blockHeader *flow.Header,
    37  		snapshot snapshot.StorageSnapshot,
    38  	) (
    39  		[]byte,
    40  		uint64,
    41  		error,
    42  	)
    43  
    44  	GetAccount(
    45  		ctx context.Context,
    46  		addr flow.Address,
    47  		header *flow.Header,
    48  		snapshot snapshot.StorageSnapshot,
    49  	) (
    50  		*flow.Account,
    51  		error,
    52  	)
    53  }
    54  
    55  type QueryConfig struct {
    56  	LogTimeThreshold    time.Duration
    57  	ExecutionTimeLimit  time.Duration
    58  	ComputationLimit    uint64
    59  	MaxErrorMessageSize int
    60  }
    61  
    62  func NewDefaultConfig() QueryConfig {
    63  	return QueryConfig{
    64  		LogTimeThreshold:    DefaultLogTimeThreshold,
    65  		ExecutionTimeLimit:  DefaultExecutionTimeLimit,
    66  		ComputationLimit:    fvm.DefaultComputationLimit,
    67  		MaxErrorMessageSize: DefaultMaxErrorMessageSize,
    68  	}
    69  }
    70  
    71  type QueryExecutor struct {
    72  	config           QueryConfig
    73  	logger           zerolog.Logger
    74  	metrics          module.ExecutionMetrics
    75  	vm               fvm.VM
    76  	vmCtx            fvm.Context
    77  	derivedChainData *derived.DerivedChainData
    78  	rngLock          *sync.Mutex
    79  	entropyPerBlock  EntropyProviderPerBlock
    80  }
    81  
    82  var _ Executor = &QueryExecutor{}
    83  
    84  func NewQueryExecutor(
    85  	config QueryConfig,
    86  	logger zerolog.Logger,
    87  	metrics module.ExecutionMetrics,
    88  	vm fvm.VM,
    89  	vmCtx fvm.Context,
    90  	derivedChainData *derived.DerivedChainData,
    91  	entropyPerBlock EntropyProviderPerBlock,
    92  ) *QueryExecutor {
    93  	if config.ComputationLimit > 0 {
    94  		vmCtx = fvm.NewContextFromParent(vmCtx, fvm.WithComputationLimit(config.ComputationLimit))
    95  	}
    96  	return &QueryExecutor{
    97  		config:           config,
    98  		logger:           logger,
    99  		metrics:          metrics,
   100  		vm:               vm,
   101  		vmCtx:            vmCtx,
   102  		derivedChainData: derivedChainData,
   103  		rngLock:          &sync.Mutex{},
   104  		entropyPerBlock:  entropyPerBlock,
   105  	}
   106  }
   107  
   108  func (e *QueryExecutor) ExecuteScript(
   109  	ctx context.Context,
   110  	script []byte,
   111  	arguments [][]byte,
   112  	blockHeader *flow.Header,
   113  	snapshot snapshot.StorageSnapshot,
   114  ) (
   115  	encodedValue []byte,
   116  	computationUsed uint64,
   117  	err error,
   118  ) {
   119  
   120  	startedAt := time.Now()
   121  	memAllocBefore := debug.GetHeapAllocsBytes()
   122  
   123  	// allocate a random ID to be able to track this script when its done,
   124  	// scripts might not be unique so we use this extra tracker to follow their logs
   125  	// TODO: this is a temporary measure, we could remove this in the future
   126  	if e.logger.Debug().Enabled() {
   127  		e.rngLock.Lock()
   128  		defer e.rngLock.Unlock()
   129  		trackerID, err := rand.Uint32()
   130  		if err != nil {
   131  			return nil, 0, fmt.Errorf("failed to generate trackerID: %w", err)
   132  		}
   133  
   134  		trackedLogger := e.logger.With().Hex("script_hex", script).Uint32("trackerID", trackerID).Logger()
   135  		trackedLogger.Debug().Msg("script is sent for execution")
   136  		defer func() {
   137  			trackedLogger.Debug().Msg("script execution is complete")
   138  		}()
   139  	}
   140  
   141  	requestCtx, cancel := context.WithTimeout(ctx, e.config.ExecutionTimeLimit)
   142  	defer cancel()
   143  
   144  	defer func() {
   145  		prepareLog := func() *zerolog.Event {
   146  			args := make([]string, 0, len(arguments))
   147  			for _, a := range arguments {
   148  				args = append(args, hex.EncodeToString(a))
   149  			}
   150  			return e.logger.Error().
   151  				Hex("script_hex", script).
   152  				Str("args", strings.Join(args, ","))
   153  		}
   154  
   155  		elapsed := time.Since(startedAt)
   156  
   157  		if r := recover(); r != nil {
   158  			prepareLog().
   159  				Interface("recovered", r).
   160  				Msg("script execution caused runtime panic")
   161  
   162  			err = fmt.Errorf("cadence runtime error: %s", r)
   163  			return
   164  		}
   165  		if elapsed >= e.config.LogTimeThreshold {
   166  			prepareLog().
   167  				Dur("duration", elapsed).
   168  				Msg("script execution exceeded threshold")
   169  		}
   170  	}()
   171  
   172  	var output fvm.ProcedureOutput
   173  	_, output, err = e.vm.Run(
   174  		fvm.NewContextFromParent(
   175  			e.vmCtx,
   176  			fvm.WithBlockHeader(blockHeader),
   177  			fvm.WithEntropyProvider(e.entropyPerBlock.AtBlockID(blockHeader.ID())),
   178  			fvm.WithDerivedBlockData(
   179  				e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID()))),
   180  		fvm.NewScriptWithContextAndArgs(script, requestCtx, arguments...),
   181  		snapshot)
   182  	if err != nil {
   183  		return nil, 0, fmt.Errorf("failed to execute script (internal error): %w", err)
   184  	}
   185  
   186  	if output.Err != nil {
   187  		return nil, 0, errors.NewCodedError(
   188  			output.Err.Code(),
   189  			"failed to execute script at block (%s): %s", blockHeader.ID(),
   190  			summarizeLog(output.Err.Error(), e.config.MaxErrorMessageSize),
   191  		)
   192  	}
   193  
   194  	encodedValue, err = jsoncdc.Encode(output.Value)
   195  	if err != nil {
   196  		return nil, 0, fmt.Errorf("failed to encode runtime value: %w", err)
   197  	}
   198  
   199  	memAllocAfter := debug.GetHeapAllocsBytes()
   200  	e.metrics.ExecutionScriptExecuted(
   201  		time.Since(startedAt),
   202  		output.ComputationUsed,
   203  		memAllocAfter-memAllocBefore,
   204  		output.MemoryEstimate)
   205  
   206  	return encodedValue, output.ComputationUsed, nil
   207  }
   208  
   209  func summarizeLog(log string, limit int) string {
   210  	if limit > 0 && len(log) > limit {
   211  		split := int(limit/2) - 1
   212  		var sb strings.Builder
   213  		sb.WriteString(log[:split])
   214  		sb.WriteString(" ... ")
   215  		sb.WriteString(log[len(log)-split:])
   216  		return sb.String()
   217  	}
   218  	return log
   219  }
   220  
   221  func (e *QueryExecutor) GetAccount(
   222  	ctx context.Context,
   223  	address flow.Address,
   224  	blockHeader *flow.Header,
   225  	snapshot snapshot.StorageSnapshot,
   226  ) (
   227  	*flow.Account,
   228  	error,
   229  ) {
   230  	// TODO(ramtin): utilize ctx
   231  	blockCtx := fvm.NewContextFromParent(
   232  		e.vmCtx,
   233  		fvm.WithBlockHeader(blockHeader),
   234  		fvm.WithDerivedBlockData(
   235  			e.derivedChainData.NewDerivedBlockDataForScript(blockHeader.ID())))
   236  
   237  	account, err := e.vm.GetAccount(
   238  		blockCtx,
   239  		address,
   240  		snapshot)
   241  	if err != nil {
   242  		return nil, fmt.Errorf(
   243  			"failed to get account (%s) at block (%s): %w",
   244  			address.String(),
   245  			blockHeader.ID(),
   246  			err)
   247  	}
   248  
   249  	return account, nil
   250  }