| // Copyright 2019 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cel |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "time" |
| |
| "cel.dev/cel-go/cel/async" |
| "cel.dev/cel-go/common/ast" |
| "cel.dev/cel-go/common/operators" |
| "cel.dev/cel-go/common/overloads" |
| "cel.dev/cel-go/common/types" |
| "cel.dev/cel-go/common/types/ref" |
| "cel.dev/cel-go/interpreter" |
| ) |
| |
| // Program is an evaluable view of an Ast. |
| type Program interface { |
| // Eval returns the result of an evaluation of the Ast and environment against the input vars. |
| // |
| // The vars value may either be an `Activation` or a `map[string]any`. |
| // |
| // If the `OptTrackState`, `OptTrackCost` or `OptExhaustiveEval` flags are used, the `details` response will |
| // be non-nil. Given this caveat on `details`, the return state from evaluation will be: |
| // |
| // * `val`, `details`, `nil` - Successful evaluation of a non-error result. |
| // * `val`, `details`, `err` - Successful evaluation to an error result. |
| // * `nil`, `details`, `err` - Unsuccessful evaluation. |
| // |
| // An unsuccessful evaluation is typically the result of a series of incompatible `EnvOption` |
| // or `ProgramOption` values used in the creation of the evaluation environment or executable |
| // program. |
| Eval(any) (ref.Val, *EvalDetails, error) |
| |
| // ContextEval evaluates the program with a set of input variables and a context object in order |
| // to support cancellation and timeouts. This method must be used in conjunction with the |
| // InterruptCheckFrequency() option for cancellation interrupts to be impact evaluation. |
| // |
| // The vars value may either be an `Activation` or `map[string]any`. |
| // |
| // The output contract for `ContextEval` is otherwise identical to the `Eval` method. |
| ContextEval(context.Context, any) (ref.Val, *EvalDetails, error) |
| |
| // ConcurrentEval evaluates the program concurrently, returning a channel that will receive |
| // the final EvalResult when all asynchronous operations complete, or the context expires. |
| // |
| // The vars value may either be an `Activation` or `map[string]any`. |
| // |
| // Liveness: ConcurrentEval relies on context cancellation to terminate. If an async function |
| // never returns and does not honor its context, and the supplied context has no deadline, the |
| // call will block indefinitely. Always pass a context with a deadline or cancellation. |
| // |
| // Error handling is fail-fast: as soon as a re-evaluation pass yields an error, that error is |
| // returned and any still in-flight async calls are cancelled (their contexts are done) and |
| // their results discarded. Async functions should therefore be free of unwanted side effects |
| // on partial evaluation, or guard them with idempotency/cancellation handling. |
| ConcurrentEval(context.Context, any) <-chan EvalResult |
| } |
| |
| // Activation used to resolve identifiers by name and references by id. |
| // |
| // An Activation is the primary mechanism by which a caller supplies input into a CEL program. |
| type Activation = interpreter.Activation |
| |
| // NewActivation returns an activation based on a map-based binding where the map keys are |
| // expected to be qualified names used with ResolveName calls. |
| // |
| // The input `bindings` may either be of type `Activation` or `map[string]any`. |
| // |
| // Lazy bindings may be supplied within the map-based input in either of the following forms: |
| // - func() any |
| // - func() ref.Val |
| // |
| // The output of the lazy binding will overwrite the variable reference in the internal map. |
| // |
| // Values which are not represented as ref.Val types on input may be adapted to a ref.Val using |
| // the types.Adapter configured in the environment. |
| func NewActivation(bindings any) (Activation, error) { |
| return interpreter.NewActivation(bindings) |
| } |
| |
| // PartialActivation extends the Activation interface with a set of unknown AttributePatterns. |
| type PartialActivation = interpreter.PartialActivation |
| |
| // NoVars returns an empty Activation. |
| func NoVars() Activation { |
| return interpreter.EmptyActivation() |
| } |
| |
| // PartialVars returns a PartialActivation which contains variables and a set of AttributePattern |
| // values that indicate variables or parts of variables whose value are not yet known. |
| // |
| // This method relies on manually configured sets of missing attribute patterns. For a method which |
| // infers the missing variables from the input and the configured environment, use Env.PartialVars(). |
| // |
| // The `vars` value may either be an Activation or any valid input to the NewActivation call. |
| func PartialVars(vars any, |
| unknowns ...*AttributePatternType) (PartialActivation, error) { |
| return interpreter.NewPartialActivation(vars, unknowns...) |
| } |
| |
| // AttributePattern returns an AttributePattern that matches a top-level variable. The pattern is |
| // mutable, and its methods support the specification of one or more qualifier patterns. |
| // |
| // For example, the AttributePattern(`a`).QualString(`b`) represents a variable access `a` with a |
| // string field or index qualification `b`. This pattern will match Attributes `a`, and `a.b`, |
| // but not `a.c`. |
| // |
| // When using a CEL expression within a container, e.g. a package or namespace, the variable name |
| // in the pattern must match the qualified name produced during the variable namespace resolution. |
| // For example, when variable `a` is declared within an expression whose container is `ns.app`, the |
| // fully qualified variable name may be `ns.app.a`, `ns.a`, or `a` per the CEL namespace resolution |
| // rules. Pick the fully qualified variable name that makes sense within the container as the |
| // AttributePattern `varName` argument. |
| func AttributePattern(varName string) *AttributePatternType { |
| return interpreter.NewAttributePattern(varName) |
| } |
| |
| // AttributePatternType represents a top-level variable with an optional set of qualifier patterns. |
| // |
| // See the interpreter.AttributePattern and interpreter.AttributeQualifierPattern for more info |
| // about how to create and manipulate AttributePattern values. |
| type AttributePatternType = interpreter.AttributePattern |
| |
| // EvalDetails holds additional information observed during the Eval() call. |
| type EvalDetails struct { |
| state interpreter.EvalState |
| costTracker *interpreter.CostTracker |
| } |
| |
| // State of the evaluation, non-nil if the OptTrackState or OptExhaustiveEval is specified |
| // within EvalOptions. |
| func (ed *EvalDetails) State() interpreter.EvalState { |
| if ed == nil { |
| return interpreter.NewEvalState() |
| } |
| return ed.state |
| } |
| |
| // ActualCost returns the tracked cost through the course of execution when `CostTracking` is enabled. |
| // Otherwise, returns nil if the cost was not enabled. |
| func (ed *EvalDetails) ActualCost() *uint64 { |
| if ed == nil || ed.costTracker == nil { |
| return nil |
| } |
| cost := ed.costTracker.ActualCost() |
| return &cost |
| } |
| |
| // EvalResult encapsulates the response from a ConcurrentEval call. |
| type EvalResult struct { |
| Val ref.Val |
| EvalDetails *EvalDetails |
| Err error |
| } |
| |
| // prog is the internal implementation of the Program interface. |
| type prog struct { |
| *Env |
| evalOpts EvalOption |
| defaultVars Activation |
| dispatcher interpreter.Dispatcher |
| interpreter interpreter.Interpreter |
| interruptCheckFrequency uint |
| |
| // Intermediate state used to configure the InterpretableDecorator set provided |
| // to the initInterpretable call. |
| plannerOptions []interpreter.PlannerOption |
| regexOptimizations []*interpreter.RegexOptimization |
| |
| // Interpretable configured from an Ast and aggregate decorator set based on program options. |
| interpretable interpreter.InterpretableV2 |
| observable *interpreter.ObservableInterpretable |
| callCostEstimator interpreter.ActualCostEstimator |
| costOptions []interpreter.CostTrackerOption |
| costLimit *uint64 |
| |
| // hasAsync indicates the planned expression contains an asynchronous function call, which can |
| // only be resolved by ConcurrentEval. |
| hasAsync bool |
| |
| // Async evaluation configuration used by ConcurrentEval. |
| drainStrategy async.DrainStrategy |
| asyncObserver async.Observer |
| asyncCompletionBufferSize int |
| asyncMaxConcurrency int |
| } |
| |
| // scanOptTargets walks the AST once and reports whether the Optimize() |
| // decorator (needOpt) and the regex-constant compiler (needRegex) have any |
| // target node present. The conditions are a superset of what each decorator |
| // acts on, so a false negative — skipping a decorator that would have |
| // optimized something — is impossible. |
| func scanOptTargets(root ast.Expr) (needOpt, needRegex bool) { |
| ast.PostOrderVisit(root, ast.NewExprVisitor(func(e ast.Expr) { |
| switch e.Kind() { |
| case ast.ListKind, ast.MapKind: |
| needOpt = true // maybeBuildListLiteral / maybeBuildMapLiteral |
| case ast.CallKind: |
| switch fn := e.AsCall().FunctionName(); { |
| case fn == overloads.Matches: |
| needRegex = true |
| case fn == operators.In || fn == operators.OldIn: |
| needOpt = true // maybeOptimizeSetMembership |
| case overloads.IsTypeConversionFunction(fn): |
| needOpt = true // maybeOptimizeConstUnary |
| } |
| } |
| })) |
| return |
| } |
| |
| // newProgram creates a program instance with an environment, an ast, and an optional list of |
| // ProgramOption values. |
| // |
| // If the program cannot be configured the prog will be nil, with a non-nil error response. |
| func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { |
| // Build the env's function bindings and shared dispatcher once (pure functions of the |
| // env). The dispatcher holding the env's function bindings is identical across every |
| // Program() built from it and read-only during planning — so assemble it once per env |
| // and layer a thin child over it here for per-program Functions() isolation, rather than |
| // re-indexing overloads on every Program() call. |
| sharedDisp, hasAsync, err := e.initDispatcher() |
| if err != nil { |
| return nil, err |
| } |
| disp := interpreter.ExtendDispatcher(sharedDisp) |
| |
| // Ensure the default attribute factory is set after the adapter and provider are |
| // configured. |
| p := &prog{ |
| Env: e, |
| plannerOptions: []interpreter.PlannerOption{}, |
| dispatcher: disp, |
| costOptions: []interpreter.CostTrackerOption{}, |
| drainStrategy: async.DrainReady(100 * time.Microsecond), |
| hasAsync: hasAsync, |
| } |
| |
| // Configure the program via the ProgramOption values. |
| for _, opt := range opts { |
| p, err = opt(p) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| // Set the attribute factory after the options have been set. |
| var attrFactory interpreter.AttributeFactory |
| attrFactorOpts := []interpreter.AttrFactoryOption{ |
| interpreter.EnableErrorOnBadPresenceTest(p.HasFeature(featureEnableErrorOnBadPresenceTest)), |
| } |
| if a.SourceInfo().HasExtension("json_name", ast.NewExtensionVersion(1, 1)) { |
| if !e.HasFeature(featureJSONFieldNames) { |
| return nil, errors.New("the AST extension 'json_name' requires the option cel.JSONFieldNames(true)") |
| } |
| } |
| // Configure the type provider, considering whether the AST indicates whether it supports JSON field names |
| if p.evalOpts&OptPartialEval == OptPartialEval { |
| attrFactory = interpreter.NewPartialAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) |
| } else { |
| attrFactory = interpreter.NewAttributeFactory(e.Container, e.adapter, e.provider, attrFactorOpts...) |
| } |
| interp := interpreter.NewInterpreter(disp, e.Container, e.provider, e.adapter, attrFactory) |
| p.interpreter = interp |
| |
| // Translate the EvalOption flags into InterpretableDecorator instances. |
| plannerOptions := make([]interpreter.PlannerOption, len(p.plannerOptions)) |
| copy(plannerOptions, p.plannerOptions) |
| |
| // Enable interrupt checking if there's a non-zero check frequency |
| if p.interruptCheckFrequency > 0 { |
| plannerOptions = append(plannerOptions, interpreter.InterruptableEval()) |
| } |
| // Enable constant folding first. |
| if p.evalOpts&OptOptimize == OptOptimize { |
| // The Optimize() decorator (set-membership, constant list/map literals, |
| // const type conversions) and the regex-constant compiler each walk |
| // every planned node. When the AST provably contains no node they can |
| // act on, adding them is pure overhead — so gate each on a single AST |
| // scan. The scan condition is a superset of what the decorators touch, |
| // so a decorator is only skipped when its target node is definitely |
| // absent and evaluation is never affected (an ungated regex would |
| // recompile per eval, etc.). |
| addOptimize, addRegex := scanOptTargets(a.Expr()) |
| if addOptimize { |
| plannerOptions = append(plannerOptions, interpreter.Optimize()) |
| } |
| if addRegex { |
| p.regexOptimizations = append(p.regexOptimizations, interpreter.MatchesRegexOptimization) |
| } |
| } |
| // Enable regex compilation of constants immediately after folding constants. |
| if len(p.regexOptimizations) > 0 { |
| plannerOptions = append(plannerOptions, interpreter.CompileRegexConstants(p.regexOptimizations...)) |
| } |
| if limit := p.limits[limitRegexProgramSize]; limit > 0 { |
| plannerOptions = append(plannerOptions, interpreter.RegexProgramSizeLimit(limit)) |
| } |
| |
| // Enable exhaustive eval, state tracking and cost tracking last since they require a factory. |
| if p.evalOpts&(OptExhaustiveEval|OptTrackState|OptTrackCost) != 0 { |
| costOptCount := len(p.costOptions) |
| if p.costLimit != nil { |
| costOptCount++ |
| } |
| costOpts := make([]interpreter.CostTrackerOption, 0, costOptCount) |
| costOpts = append(costOpts, p.costOptions...) |
| if p.costLimit != nil { |
| costOpts = append(costOpts, interpreter.CostTrackerLimit(*p.costLimit)) |
| } |
| // Creating a new cost tracker for each evaluation causes significant work that |
| // needs to be repeated for each evaluation even though the cost tracker is |
| // mostly read-only once constructed. Therefore it gets constructed |
| // once now and later a cheap clone is used for each evaluation. |
| tracker, err := interpreter.NewCostTracker(p.callCostEstimator, costOpts...) |
| if err != nil { |
| return nil, fmt.Errorf("construct cost tracker: %w", err) |
| } |
| trackerFactory := func() (*interpreter.CostTracker, error) { |
| return tracker.Clone() |
| } |
| var observers []interpreter.PlannerOption |
| if p.evalOpts&(OptExhaustiveEval|OptTrackState) != 0 { |
| // EvalStateObserver is required for OptExhaustiveEval. |
| observers = append(observers, interpreter.EvalStateObserver()) |
| } |
| if p.evalOpts&OptTrackCost == OptTrackCost { |
| observers = append(observers, interpreter.CostObserver(interpreter.CostTrackerFactory(trackerFactory))) |
| } |
| // Enable exhaustive eval over a basic observer since it offers a superset of features. |
| if p.evalOpts&OptExhaustiveEval == OptExhaustiveEval { |
| plannerOptions = append(plannerOptions, |
| append([]interpreter.PlannerOption{interpreter.ExhaustiveEval()}, observers...)...) |
| } else if len(observers) > 0 { |
| plannerOptions = append(plannerOptions, observers...) |
| } |
| } |
| return p.initInterpretable(a, plannerOptions) |
| } |
| |
| func (p *prog) initInterpretable(a *ast.AST, plannerOptions []interpreter.PlannerOption) (*prog, error) { |
| // When the AST has been exprAST it contains metadata that can be used to speed up program execution. |
| interpretable, err := p.interpreter.NewInterpretable(a, plannerOptions...) |
| if err != nil { |
| return nil, err |
| } |
| p.interpretable = interpretable |
| if oi, ok := interpretable.(*interpreter.ObservableInterpretable); ok { |
| p.observable = oi |
| } |
| return p, nil |
| } |
| |
| // Eval implements the Program interface method. |
| func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { |
| // Configure error recovery for unexpected panics during evaluation. Note, the use of named |
| // return values makes it possible to modify the error response during the recovery |
| // function. |
| defer func() { |
| if r := recover(); r != nil { |
| switch t := r.(type) { |
| case interpreter.EvalCancelledError: |
| err = t |
| default: |
| err = fmt.Errorf("internal error: %v", r) |
| } |
| } |
| }() |
| // Asynchronous calls cannot be resolved by a single-pass evaluation. Reject before doing any |
| // work (this also covers ContextEval, which delegates here); ConcurrentEval does not call Eval. |
| if p.hasAsync { |
| return nil, nil, errAsyncRequiresConcurrentEval |
| } |
| // Build a hierarchical activation if there are default vars set. |
| var frame *interpreter.ExecutionFrame |
| if f, ok := input.(*interpreter.ExecutionFrame); ok { |
| frame = f |
| } else { |
| frame, err = p.newExecutionFrame(input) |
| if err != nil { |
| return nil, nil, err |
| } |
| defer frame.Close() |
| } |
| if p.observable != nil { |
| det = &EvalDetails{} |
| out = p.observable.ObserveExec(frame, func(observed any) { |
| switch o := observed.(type) { |
| case interpreter.EvalState: |
| det.state = o |
| case *interpreter.CostTracker: |
| det.costTracker = o |
| } |
| }) |
| } else { |
| out = p.interpretable.Exec(frame) |
| } |
| // The output of an internal Eval may have a value (`v`) that is a types.Err. This step |
| // translates the CEL value to a Go error response. This interface does not quite match the |
| // RPC signature which allows for multiple errors to be returned, but should be sufficient. |
| if types.IsError(out) { |
| err = out.(*types.Err) |
| } |
| return |
| } |
| |
| // ContextEval implements the Program interface. |
| func (p *prog) ContextEval(ctx context.Context, input any) (ref.Val, *EvalDetails, error) { |
| if ctx == nil { |
| return nil, nil, fmt.Errorf("context can not be nil") |
| } |
| frame, err := p.newExecutionFrame(input) |
| if err != nil { |
| return nil, nil, err |
| } |
| defer frame.Close() |
| frame.SetContext(ctx, p.interruptCheckFrequency) |
| out, det, errEval := p.Eval(frame) |
| if errEval != nil && errors.Is(errEval, interpreter.InterruptError{}) { |
| return out, det, fmt.Errorf("%w: %w", errEval, context.Cause(ctx)) |
| } |
| return out, det, errEval |
| } |
| |
| // newExecutionFrame creates an ExecutionFrame for the given input without a timeout context. |
| func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error) { |
| frame, err := interpreter.NewExecutionFrame(input) |
| if err != nil { |
| return nil, err |
| } |
| if p.defaultVars != nil { |
| // Update the frame's activation in place. |
| frame.Activation = interpreter.NewHierarchicalActivation(p.defaultVars, frame.Activation) |
| } |
| |
| return frame, nil |
| } |
| |
| // newAsyncFrame creates an ExecutionFrame configured for asynchronous evaluation under the |
| // given context, wiring the observer and concurrency limit from the program options. |
| func (p *prog) newAsyncFrame(ctx context.Context, input any) (*interpreter.ExecutionFrame, error) { |
| frame, err := p.newExecutionFrame(input) |
| if err != nil { |
| return nil, err |
| } |
| if err := frame.SetContext(ctx, p.interruptCheckFrequency); err != nil { |
| frame.Close() |
| return nil, err |
| } |
| frame.SetAsyncObserver(p.asyncObserver) |
| frame.SetAsyncMaxConcurrency(resolveAsyncMaxConcurrency(p.asyncMaxConcurrency)) |
| return frame, nil |
| } |
| |
| // defaultAsyncMaxConcurrency bounds the number of concurrently launched async calls when the |
| // program does not configure AsyncMaxConcurrency. It exists so that a wide fan-out (e.g. an async |
| // call inside a comprehension over a large list) cannot spawn an unbounded number of goroutines. |
| const defaultAsyncMaxConcurrency = 100 |
| |
| // resolveAsyncMaxConcurrency maps the configured concurrency to the effective launch limit: |
| // - 0 (unset): apply defaultAsyncMaxConcurrency. |
| // - >0: use the configured value. |
| // - <0: unlimited (no launch limiter); use only if the caller bounds concurrency another way. |
| func resolveAsyncMaxConcurrency(configured int) int { |
| if configured == 0 { |
| return defaultAsyncMaxConcurrency |
| } |
| return configured |
| } |
| |
| // resolveCompletionBufferSize returns the size of the async completion channel. When unset, it |
| // defaults to the effective launch concurrency so that all in-flight calls can report completion |
| // without blocking. An unbuffered channel would make a completed call hold its launch slot until |
| // the evaluator drained it, throttling effective concurrency to the drain rate. |
| func (p *prog) resolveCompletionBufferSize() int { |
| if p.asyncCompletionBufferSize > 0 { |
| return p.asyncCompletionBufferSize |
| } |
| limit := resolveAsyncMaxConcurrency(p.asyncMaxConcurrency) |
| if limit < 0 { |
| // Unlimited launches: fall back to the default bound for the buffer so it stays finite. |
| return defaultAsyncMaxConcurrency |
| } |
| return limit |
| } |
| |
| // ConcurrentEval implements the Program interface. |
| func (p *prog) ConcurrentEval(ctx context.Context, input any) <-chan EvalResult { |
| resCh := make(chan EvalResult, 1) |
| if ctx == nil { |
| resCh <- EvalResult{Err: errors.New("context can not be nil")} |
| close(resCh) |
| return resCh |
| } |
| |
| go func() { |
| defer close(resCh) |
| // Ensure concurrent eval handles panic / recovery properly |
| defer func() { |
| if r := recover(); r != nil { |
| switch t := r.(type) { |
| case interpreter.EvalCancelledError: |
| resCh <- EvalResult{Err: t} |
| default: |
| resCh <- EvalResult{Err: fmt.Errorf("internal error: %v", r)} |
| } |
| } |
| }() |
| |
| frame, err := p.newAsyncFrame(ctx, input) |
| if err != nil { |
| resCh <- EvalResult{Err: err} |
| return |
| } |
| defer frame.Close() |
| |
| // Completions are signaled to this channel as async calls finish. The asyncCallState |
| // fan-in also selects on ctx.Done(), so the sender will not leak if this loop returns early. |
| completions := make(chan int64, p.resolveCompletionBufferSize()) |
| frame.SetCompletions(completions) |
| |
| for { |
| var out ref.Val |
| var det *EvalDetails |
| |
| if p.observable != nil { |
| det = &EvalDetails{} |
| out = p.observable.ObserveExec(frame, func(observed any) { |
| switch o := observed.(type) { |
| case interpreter.EvalState: |
| det.state = o |
| case *interpreter.CostTracker: |
| det.costTracker = o |
| } |
| }) |
| } else { |
| out = p.interpretable.Exec(frame) |
| } |
| |
| // Communicate errors quickly. |
| if types.IsError(out) { |
| var err error = out.(*types.Err) |
| if errors.Is(err, interpreter.InterruptError{}) { |
| err = fmt.Errorf("%w: %w", err, context.Cause(ctx)) |
| } |
| resCh <- EvalResult{Val: out, EvalDetails: det, Err: err} |
| return |
| } |
| |
| // A concrete (non-unknown) result is final. |
| unk, isUnknown := out.(*types.Unknown) |
| if !isUnknown || !unk.HasUnknownFunction() { |
| resCh <- EvalResult{Val: out, EvalDetails: det, Err: nil} |
| return |
| } |
| |
| // Post-execution dispatch: launch only the async calls required by the unknown result. |
| frame.DispatchPendingAsyncCalls(unk.IDs()) |
| |
| // The result depends on one or more unresolved async calls. Wait for completions and |
| // re-evaluate according to the configured drain strategy. |
| var batch []async.Call |
| |
| // Wait for at least one completion (or cancellation). |
| select { |
| case id := <-completions: |
| if call := frame.AsyncCall(id); call != nil { |
| batch = append(batch, call) |
| } |
| case <-ctx.Done(): |
| resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} |
| return |
| } |
| |
| // Accumulate completions and consult the strategy. |
| var timer *time.Timer |
| reevaluate := false |
| for !reevaluate { |
| active := frame.ActiveAsyncCalls() |
| action := p.drainStrategy.NextAction(batch, active) |
| if action.Reevaluate { |
| break |
| } |
| |
| var timeoutCh <-chan time.Time |
| if action.WaitDuration > 0 { |
| if timer == nil { |
| timer = time.NewTimer(action.WaitDuration) |
| } else { |
| if !timer.Stop() { |
| select { |
| case <-timer.C: |
| default: |
| } |
| } |
| timer.Reset(action.WaitDuration) |
| } |
| timeoutCh = timer.C |
| } |
| |
| select { |
| case id := <-completions: |
| if call := frame.AsyncCall(id); call != nil { |
| batch = append(batch, call) |
| } |
| case <-timeoutCh: |
| reevaluate = true |
| case <-ctx.Done(): |
| if timer != nil { |
| timer.Stop() |
| } |
| resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} |
| return |
| } |
| } |
| if timer != nil { |
| timer.Stop() |
| } |
| } |
| }() |
| |
| return resCh |
| } |
| |
| // errAsyncRequiresConcurrentEval is returned by the synchronous entry points (Eval, ContextEval) |
| // when the expression contains asynchronous function calls, which only ConcurrentEval can resolve. |
| var errAsyncRequiresConcurrentEval = errors.New( |
| "expression contains asynchronous function calls; use ConcurrentEval") |