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 }