diff --git a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java index 457511523..c494ff252 100644 --- a/runtime/src/main/java/dev/cel/runtime/UnknownContext.java +++ b/runtime/src/main/java/dev/cel/runtime/UnknownContext.java @@ -55,7 +55,7 @@ private UnknownContext( ImmutableList unresolvedAttributes, ImmutableMap resolvedAttributes) { this.unresolvedAttributes = unresolvedAttributes; - variableResolver = resolver; + this.variableResolver = resolver; this.resolvedAttributes = resolvedAttributes; } @@ -76,6 +76,17 @@ public static UnknownContext create( createExprVariableResolver(resolver), ImmutableList.copyOf(attributes), ImmutableMap.of()); } + /** Extends an existing {@code UnknownContext} by adding more attribute patterns to it. */ + public UnknownContext extend(Collection attributePatterns) { + return new UnknownContext( + this.variableResolver(), + ImmutableList.builder() + .addAll(this.unresolvedAttributes) + .addAll(attributePatterns) + .build(), + this.resolvedAttributes); + } + /** Adapts a CelVariableResolver to the legacy impl equivalent GlobalResolver. */ private static GlobalResolver createExprVariableResolver(CelVariableResolver resolver) { return (String name) -> resolver.find(name).orElse(null); diff --git a/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java index 98e0c4eb6..c4e36204c 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/async/AsyncProgramImpl.java @@ -14,12 +14,14 @@ package dev.cel.runtime.async; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.Futures.transformAsync; import static com.google.common.util.concurrent.Futures.whenAllSucceed; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; @@ -55,6 +57,7 @@ final class AsyncProgramImpl implements CelAsyncRuntime.AsyncProgram { // Safety limit for resolution rounds. private final int maxEvaluateIterations; + private final UnknownContext startingUnknownContext; private final Program program; private final ListeningExecutorService executor; private final ImmutableMap resolvers; @@ -63,15 +66,26 @@ final class AsyncProgramImpl implements CelAsyncRuntime.AsyncProgram { Program program, ListeningExecutorService executor, ImmutableMap resolvers, - int maxEvaluateIterations) { + int maxEvaluateIterations, + UnknownContext startingUnknownContext) { this.program = program; this.executor = executor; this.resolvers = resolvers; this.maxEvaluateIterations = maxEvaluateIterations; + // The following is populated from CelAsyncRuntime. The impl is immutable, thus safe to reuse as + // a starting context. + this.startingUnknownContext = startingUnknownContext; } - private Optional lookupResolver(CelAttribute attribute) { + private Optional lookupResolver( + Iterable resolvableAttributePatterns, CelAttribute attribute) { // TODO: may need to handle multiple resolvers for partial case. + for (CelResolvableAttributePattern entry : resolvableAttributePatterns) { + if (entry.attributePattern().isPartialMatch(attribute)) { + return Optional.of(entry.resolver()); + } + } + for (Map.Entry entry : resolvers.entrySet()) { if (entry.getKey().isPartialMatch(attribute)) { @@ -102,10 +116,14 @@ private ListenableFuture> allAsMapOnSuccess( } private ListenableFuture resolveAndReevaluate( - CelUnknownSet unknowns, UnknownContext ctx, int iteration) { + CelUnknownSet unknowns, + UnknownContext ctx, + Iterable resolvableAttributePatterns, + int iteration) { Map> futureMap = new LinkedHashMap<>(); for (CelAttribute attr : unknowns.attributes()) { - Optional maybeResolver = lookupResolver(attr); + Optional maybeResolver = + lookupResolver(resolvableAttributePatterns, attr); maybeResolver.ifPresent((resolver) -> futureMap.put(attr, resolver.resolve(executor, attr))); } @@ -120,13 +138,21 @@ private ListenableFuture resolveAndReevaluate( // need to be configurable in the future. return transformAsync( allAsMapOnSuccess(futureMap), - (result) -> evalPass(ctx.withResolvedAttributes(result), unknowns, iteration), + (result) -> + evalPass( + ctx.withResolvedAttributes(result), + resolvableAttributePatterns, + unknowns, + iteration), executor); } private ListenableFuture evalPass( - UnknownContext ctx, CelUnknownSet lastSet, int iteration) { - Object result = null; + UnknownContext ctx, + Iterable resolvableAttributePatterns, + CelUnknownSet lastSet, + int iteration) { + Object result; try { result = program.advanceEvaluation(ctx); } catch (CelEvaluationException e) { @@ -143,14 +169,29 @@ private ListenableFuture evalPass( return immediateFailedFuture( new CelEvaluationException("Max Evaluation iterations exceeded: " + iteration)); } - return resolveAndReevaluate((CelUnknownSet) result, ctx, iteration); + return resolveAndReevaluate( + (CelUnknownSet) result, startingUnknownContext, resolvableAttributePatterns, iteration); } return immediateFuture(result); } @Override - public ListenableFuture evaluateToCompletion(UnknownContext ctx) { - return evalPass(ctx, CelUnknownSet.create(ImmutableSet.of()), 0); + public ListenableFuture evaluateToCompletion( + CelResolvableAttributePattern... resolvableAttributes) { + return evaluateToCompletion(ImmutableList.copyOf(resolvableAttributes)); + } + + @Override + public ListenableFuture evaluateToCompletion( + Iterable resolvableAttributePatterns) { + UnknownContext newAsyncContext = + startingUnknownContext.extend( + ImmutableList.copyOf(resolvableAttributePatterns).stream() + .map(CelResolvableAttributePattern::attributePattern) + .collect(toImmutableList())); + + return evalPass( + newAsyncContext, resolvableAttributePatterns, CelUnknownSet.create(ImmutableSet.of()), 0); } } diff --git a/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel index e84c1534f..56fa42a02 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/async/BUILD.bazel @@ -13,6 +13,7 @@ package( ASYNC_RUNTIME_SOURCES = [ "AsyncProgramImpl.java", + "CelResolvableAttributePattern.java", "CelAsyncRuntimeImpl.java", "CelAsyncRuntime.java", "CelAsyncRuntimeBuilder.java", diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java index 113b84bc1..a8a2b789e 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntime.java @@ -36,17 +36,13 @@ @ThreadSafe public interface CelAsyncRuntime { - /** - * Initialize a new async context for iterative evaluation. - * - *

This maintains the state related to tracking which parts of the environment are unknown or - * have been resolved. - */ - UnknownContext newAsyncContext(); - /** AsyncProgram wraps a CEL Program with a driver to resolve unknowns as they are encountered. */ interface AsyncProgram { - ListenableFuture evaluateToCompletion(UnknownContext ctx); + ListenableFuture evaluateToCompletion( + CelResolvableAttributePattern... resolvableAttributes); + + ListenableFuture evaluateToCompletion( + Iterable resolvableAttributes); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java index 8b7dda2ef..5ebe7363e 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeBuilder.java @@ -1,5 +1,4 @@ // Copyright 2022 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 @@ -17,23 +16,36 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.async.CelAsyncRuntime.AsyncProgram; import java.util.concurrent.ExecutorService; /** Builder interface for {@link CelAsyncRuntime}. */ public interface CelAsyncRuntimeBuilder { - public static final int DEFAULT_MAX_EVALUATE_ITERATIONS = 10; + int DEFAULT_MAX_EVALUATE_ITERATIONS = 10; /** Set the CEL runtime for running incremental evaluation. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setRuntime(CelRuntime runtime); + CelAsyncRuntimeBuilder setRuntime(CelRuntime runtime); - /** Add attributes that are declared as Unknown, without any resolver. */ + /** + * Add attributes that are declared as Unknown, without any resolver. + * + * @deprecated Use {@link AsyncProgram#evaluateToCompletion(CelResolvableAttributePattern...)} + * instead to propagate the unknown attributes along with the resolvers into the program. + */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder addUnknownAttributePatterns(CelAttributePattern... attributes); + @Deprecated + CelAsyncRuntimeBuilder addUnknownAttributePatterns(CelAttributePattern... attributes); - /** Marks an attribute pattern as unknown and associates a resolver with it. */ + /** + * Marks an attribute pattern as unknown and associates a resolver with it. + * + * @deprecated Use {@link AsyncProgram#evaluateToCompletion(CelResolvableAttributePattern...)} + * instead to propagate the unknown attributes along with the resolvers into the program. + */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder addResolvableAttributePattern( + @Deprecated + CelAsyncRuntimeBuilder addResolvableAttributePattern( CelAttributePattern attribute, CelUnknownAttributeValueResolver resolver); /** @@ -42,10 +54,10 @@ public CelAsyncRuntimeBuilder addResolvableAttributePattern( *

This is a safety mechanism for expressions that chain dependent unknowns (e.g. via the * conditional operator or nested function calls). * - *

Implementations should default to {@value DEFAULT_MAX_EVALUATION_ITERATIONS}. + *

Implementations should default to {@value DEFAULT_MAX_EVALUATE_ITERATIONS}. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setMaxEvaluateIterations(int n); + CelAsyncRuntimeBuilder setMaxEvaluateIterations(int n); /** * Sets the variable resolver for simple CelVariable names (e.g. 'x' or 'com.google.x'). @@ -67,7 +79,7 @@ public CelAsyncRuntimeBuilder addResolvableAttributePattern( * resolvers. */ @CanIgnoreReturnValue - public CelAsyncRuntimeBuilder setExecutorService(ExecutorService executorService); + CelAsyncRuntimeBuilder setExecutorService(ExecutorService executorService); - public CelAsyncRuntime build(); + CelAsyncRuntime build(); } diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java index fe3655294..e39c2f9c7 100644 --- a/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java +++ b/runtime/src/main/java/dev/cel/runtime/async/CelAsyncRuntimeImpl.java @@ -55,8 +55,7 @@ private CelAsyncRuntimeImpl( this.maxEvaluateIterations = maxEvaluateIterations; } - @Override - public UnknownContext newAsyncContext() { + private UnknownContext newAsyncContext() { return UnknownContext.create(variableResolver, unknownAttributePatterns); } @@ -66,7 +65,8 @@ public AsyncProgram createProgram(CelAbstractSyntaxTree ast) throws CelEvaluatio runtime.createProgram(ast), executorService, unknownAttributeResolvers, - maxEvaluateIterations); + maxEvaluateIterations, + newAsyncContext()); } static Builder newBuilder() { diff --git a/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java b/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java new file mode 100644 index 000000000..8e243476b --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/async/CelResolvableAttributePattern.java @@ -0,0 +1,36 @@ +// Copyright 2025 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 +// +// https://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 dev.cel.runtime.async; + +import com.google.auto.value.AutoValue; +import dev.cel.runtime.CelAttributePattern; + +/** + * CelResolvableAttributePattern wraps {@link CelAttributePattern} to represent a CEL attribute + * whose value is initially unknown and needs to be resolved. It couples the attribute pattern with + * a {@link CelUnknownAttributeValueResolver} that can fetch the actual value for the attribute when + * it becomes available. + */ +@AutoValue +public abstract class CelResolvableAttributePattern { + public abstract CelAttributePattern attributePattern(); + + public abstract CelUnknownAttributeValueResolver resolver(); + + public static CelResolvableAttributePattern of( + CelAttributePattern attribute, CelUnknownAttributeValueResolver resolver) { + return new AutoValue_CelResolvableAttributePattern(attribute, resolver); + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java index b6675f75c..744f41ee1 100644 --- a/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java +++ b/runtime/src/test/java/dev/cel/runtime/async/CelAsyncRuntimeImplTest.java @@ -38,7 +38,6 @@ import dev.cel.runtime.CelAttributeParser; import dev.cel.runtime.CelAttributePattern; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.UnknownContext; import dev.cel.runtime.async.CelAsyncRuntime.AsyncProgram; import java.time.Duration; import java.util.concurrent.CancellationException; @@ -92,11 +91,9 @@ public void asyncProgram_basicUnknownResolution() throws Exception { .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); Object result = future.get(2, SECONDS); // Assert @@ -105,11 +102,11 @@ public void asyncProgram_basicUnknownResolution() throws Exception { } @Test - public void asyncProgram_basicAsyncResovler() throws Exception { + public void asyncProgram_basicAsyncResolver() throws Exception { // Arrange - final SettableFuture var1 = SettableFuture.create(); - final SettableFuture var2 = SettableFuture.create(); - final SettableFuture var3 = SettableFuture.create(); + SettableFuture var1 = SettableFuture.create(); + SettableFuture var2 = SettableFuture.create(); + SettableFuture var3 = SettableFuture.create(); Cel cel = CelFactory.standardCelBuilder() @@ -125,15 +122,6 @@ public void asyncProgram_basicAsyncResovler() throws Exception { CelAsyncRuntime asyncRuntime = CelAsyncRuntimeFactory.defaultAsyncRuntime() .setRuntime(cel) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)) - .addResolvableAttributePattern( - CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), - CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3)) .setExecutorService(Executors.newSingleThreadExecutor()) .build(); @@ -141,11 +129,19 @@ public void asyncProgram_basicAsyncResovler() throws Exception { cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = + program.evaluateToCompletion( + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var1"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var1)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var2"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var2)), + CelResolvableAttributePattern.of( + CelAttributePattern.fromQualifiedIdentifier("com.google.var3"), + CelUnknownAttributeValueResolver.fromAsyncResolver((attr) -> var3))); assertThrows(TimeoutException.class, () -> future.get(1, SECONDS)); var1.set("first"); var2.set("second"); @@ -160,9 +156,9 @@ public void asyncProgram_basicAsyncResovler() throws Exception { @Test public void asyncProgram_honorsCancellation() throws Exception { // Arrange - final SettableFuture var1 = SettableFuture.create(); - final SettableFuture var2 = SettableFuture.create(); - final SettableFuture var3 = SettableFuture.create(); + SettableFuture var1 = SettableFuture.create(); + SettableFuture var2 = SettableFuture.create(); + SettableFuture var3 = SettableFuture.create(); Cel cel = CelFactory.standardCelBuilder() @@ -194,11 +190,9 @@ public void asyncProgram_honorsCancellation() throws Exception { cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); var1.set("first"); future.cancel(true); assertThrows(CancellationException.class, () -> future.get(1, SECONDS)); @@ -214,7 +208,7 @@ interface ResolverFactory { public void asyncProgram_concurrency( @TestParameter(valuesProvider = RepeatedTestProvider.class) int testRunIndex) throws Exception { - final Duration taskDelay = Duration.ofMillis(500); + Duration taskDelay = Duration.ofMillis(500); // Arrange Cel cel = CelFactory.standardCelBuilder() @@ -254,11 +248,9 @@ public void asyncProgram_concurrency( cel.compile("var1 == 'first' && var2 == 'second' && var3 == 'third'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); // Total wait is 2 times the worker delay. This is a little conservative for the size of the // threadpool executor above, but should prevent flakes. @@ -305,11 +297,9 @@ public void asyncProgram_elementResolver() throws Exception { cel.compile("listVar[0] == 'el0' && listVar[1] == 'el1' && listVar[2] == 'el2'").getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - // empty starting context - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); Object result = future.get(1, SECONDS); // Assert @@ -362,10 +352,9 @@ public void asyncProgram_thrownExceptionPropagatesImmediately() throws Exception .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); // Assert ExecutionException e = assertThrows(ExecutionException.class, () -> future.get(2, SECONDS)); @@ -416,10 +405,9 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluator() throws Excepti .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); // Assert ExecutionException e = assertThrows(ExecutionException.class, () -> future.get(2, SECONDS)); @@ -468,10 +456,9 @@ public void asyncProgram_returnedExceptionPropagatesToEvaluatorIsPruneable() thr .getAst(); AsyncProgram program = asyncRuntime.createProgram(ast); - UnknownContext context = asyncRuntime.newAsyncContext(); // Act - ListenableFuture future = program.evaluateToCompletion(context); + ListenableFuture future = program.evaluateToCompletion(); Object result = future.get(2, SECONDS); // Assert