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