diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel
index 17089eb82..2fb948cea 100644
--- a/publish/BUILD.bazel
+++ b/publish/BUILD.bazel
@@ -32,6 +32,7 @@ RUNTIME_TARGETS = [
"//runtime/src/main/java/dev/cel/runtime:async_call",
"//runtime/src/main/java/dev/cel/runtime:async_drain_strategy",
"//runtime/src/main/java/dev/cel/runtime:async_observer",
+ "//runtime/src/main/java/dev/cel/runtime:async_options",
"//runtime/src/main/java/dev/cel/runtime:base",
"//runtime/src/main/java/dev/cel/runtime:interpreter",
"//runtime/src/main/java/dev/cel/runtime:late_function_binding",
diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel
index fbdbb1107..e1acc4261 100644
--- a/runtime/BUILD.bazel
+++ b/runtime/BUILD.bazel
@@ -12,6 +12,7 @@ java_library(
":async_call",
":async_drain_strategy",
":async_observer",
+ ":async_options",
":descriptor_message_provider",
":evaluation_exception",
":function_overload",
@@ -417,3 +418,13 @@ cel_android_library(
name = "async_observer_android",
exports = ["//runtime/src/main/java/dev/cel/runtime:async_observer_android"],
)
+
+java_library(
+ name = "async_options",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_options"],
+)
+
+cel_android_library(
+ name = "async_options_android",
+ exports = ["//runtime/src/main/java/dev/cel/runtime:async_options_android"],
+)
diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
index 145341889..9518e1601 100644
--- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
+++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel
@@ -821,6 +821,7 @@ java_library(
tags = [
],
deps = [
+ ":async_options",
":descriptor_type_resolver",
":dispatcher",
":evaluation_exception",
@@ -871,6 +872,7 @@ java_library(
tags = [
],
deps = [
+ ":async_options",
":descriptor_message_provider",
":descriptor_type_resolver",
":dispatcher",
@@ -926,6 +928,7 @@ java_library(
],
deps = [
":activation",
+ ":async_options",
":evaluation_exception",
":evaluation_listener",
":function_binding",
@@ -950,6 +953,7 @@ java_library(
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
+ "@maven//:org_jspecify_jspecify",
],
)
@@ -1363,6 +1367,36 @@ cel_android_library(
],
)
+java_library(
+ name = "async_options",
+ srcs = ["CelAsyncEvaluationOptions.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_drain_strategy",
+ ":async_observer",
+ "//:auto_value",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
+cel_android_library(
+ name = "async_options_android",
+ srcs = ["CelAsyncEvaluationOptions.java"],
+ tags = [
+ ],
+ deps = [
+ ":async_drain_strategy_android",
+ ":async_observer_android",
+ "//:auto_value",
+ "@maven//:com_google_code_findbugs_annotations",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
+
java_library(
name = "program",
srcs = ["Program.java"],
@@ -1374,6 +1408,7 @@ java_library(
":partial_vars",
":variable_resolver",
"@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
],
)
@@ -1388,6 +1423,7 @@ cel_android_library(
":partial_vars_android",
":variable_resolver",
"@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven_android//:com_google_guava_guava",
],
)
diff --git a/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java
new file mode 100644
index 000000000..19609e923
--- /dev/null
+++ b/runtime/src/main/java/dev/cel/runtime/CelAsyncEvaluationOptions.java
@@ -0,0 +1,141 @@
+// Copyright 2026 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;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.concurrent.ThreadSafe;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Options for configuring asynchronous CEL evaluation. */
+@AutoValue
+@ThreadSafe
+public abstract class CelAsyncEvaluationOptions {
+
+ private static final int DEFAULT_MAX_CONCURRENCY = 100;
+ private static final int DEFAULT_MAX_ITERATIONS = 1_000;
+
+ /**
+ * Maximum number of concurrent async function calls in-flight simultaneously. A value <= 0
+ * indicates unbounded concurrency.
+ */
+ public abstract int maxConcurrency();
+
+ /** Strategy governing when to trigger re-evaluation after async call completions. */
+ public abstract CelAsyncDrainStrategy drainStrategy();
+
+ /** Safety cap on the maximum number of AST re-evaluation passes before aborting. */
+ public abstract int maxIterations();
+
+ /**
+ * Returns the custom configured {@link ScheduledExecutorService}, if present.
+ *
+ *
If absent, {@link #resolveScheduledExecutorService()} falls back to an internal, shared
+ * single-threaded daemon scheduler.
+ */
+ public abstract Optional scheduledExecutorService();
+
+ /** Returns the configured lifecycle observer, if present. */
+ public abstract Optional observer();
+
+ /**
+ * Resolves the {@link ScheduledExecutorService} used for debounce timers, falling back to a
+ * shared, lazily initialized single-threaded daemon scheduler (named {@code
+ * cel-async-debounce-*}) if not custom-configured.
+ *
+ * The scheduler is used exclusively as an alarm clock to trigger continuation wakeups; it does
+ * not execute CEL evaluation tasks.
+ */
+ public ScheduledExecutorService resolveScheduledExecutorService() {
+ return scheduledExecutorService().orElse(DefaultDebounceSchedulerHolder.INSTANCE);
+ }
+
+ public abstract Builder toBuilder();
+
+ /**
+ * Returns a new {@link Builder} initialized with standard default options:
+ *
+ *
+ * Maximum concurrency: 100 in-flight calls
+ * Maximum iterations: 1,000 evaluation passes
+ * Drain strategy: {@link CelAsyncDrainStrategy#drainReady()} (100-microsecond debounce
+ * window)
+ * Scheduled executor service: A shared, lazily initialized single-threaded daemon scheduler
+ * used exclusively for debounce timer wakeups.
+ *
+ */
+ public static Builder newBuilder() {
+ return new AutoValue_CelAsyncEvaluationOptions.Builder()
+ .setMaxConcurrency(DEFAULT_MAX_CONCURRENCY)
+ .setDrainStrategy(CelAsyncDrainStrategy.drainReady())
+ .setMaxIterations(DEFAULT_MAX_ITERATIONS);
+ }
+
+ /**
+ * Returns a new {@link Builder} initialized with standard default options.
+ *
+ * Equivalent to calling {@link #newBuilder()}.
+ */
+ public static Builder builder() {
+ return newBuilder();
+ }
+
+ /**
+ * Returns a {@link CelAsyncEvaluationOptions} instance with the {@link #newBuilder() default
+ * configuration}.
+ */
+ public static CelAsyncEvaluationOptions defaultOptions() {
+ return newBuilder().build();
+ }
+
+ private static final class DefaultDebounceSchedulerHolder {
+ private static final AtomicLong counter = new AtomicLong();
+ private static final ScheduledExecutorService INSTANCE =
+ Executors.newSingleThreadScheduledExecutor(
+ r -> {
+ Thread t = new Thread(r);
+ t.setName("cel-async-debounce-" + counter.getAndIncrement());
+ t.setDaemon(true);
+ return t;
+ });
+ }
+
+ /** Builder for {@link CelAsyncEvaluationOptions}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setMaxConcurrency(int maxConcurrency);
+
+ public abstract Builder setDrainStrategy(CelAsyncDrainStrategy drainStrategy);
+
+ public abstract Builder setMaxIterations(int maxIterations);
+
+ /**
+ * Sets a custom {@link ScheduledExecutorService} for debounce timers.
+ *
+ *
If not set, defaults to an internal, shared single-threaded daemon scheduler.
+ */
+ public abstract Builder setScheduledExecutorService(
+ ScheduledExecutorService scheduledExecutorService);
+
+ public abstract Builder setObserver(CelAsyncObserver observer);
+
+ public abstract CelAsyncEvaluationOptions build();
+ }
+
+ // Package-private constructor prevents extension outside package while allowing AutoValue.
+ CelAsyncEvaluationOptions() {}
+}
diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java
index 1e7fdcac8..e9c6ca20a 100644
--- a/runtime/src/main/java/dev/cel/runtime/CelRuntime.java
+++ b/runtime/src/main/java/dev/cel/runtime/CelRuntime.java
@@ -14,6 +14,7 @@
package dev.cel.runtime;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import javax.annotation.concurrent.ThreadSafe;
@@ -42,6 +43,12 @@ interface Program extends dev.cel.runtime.Program {
/** Evaluate the expression using {@code message} fields as the source of input variables. */
Object eval(Message message) throws CelEvaluationException;
+ /**
+ * Evaluate the expression asynchronously using {@code message} fields as the source of input
+ * variables.
+ */
+ ListenableFuture evalAsync(Message message);
+
/**
* Trace evaluates a compiled program without any variables and invokes the listener as
* evaluation progresses through the AST.
diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java
index 00f6e3bf7..feacf5e37 100644
--- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java
+++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeBuilder.java
@@ -14,6 +14,7 @@
package dev.cel.runtime;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
@@ -214,6 +215,22 @@ public interface CelRuntimeBuilder {
@CanIgnoreReturnValue
CelRuntimeBuilder setContainer(CelContainer container);
+ /**
+ * Sets options to use for asynchronous evaluation.
+ *
+ * If not configured, defaults to {@link CelAsyncEvaluationOptions#defaultOptions()}.
+ */
+ @CanIgnoreReturnValue
+ CelRuntimeBuilder setAsyncEvaluationOptions(CelAsyncEvaluationOptions asyncEvaluationOptions);
+
+ /**
+ * Sets the executor to use for asynchronous evaluation.
+ *
+ *
This executor is required when evaluating expressions asynchronously via {@link
+ * Program#evalAsync}.
+ */
+ @CanIgnoreReturnValue
+ CelRuntimeBuilder setAsyncExecutor(ListeningExecutorService asyncExecutor);
/** Build a new instance of the {@code CelRuntime}. */
@CheckReturnValue
diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java
index 5cda25800..857434ba2 100644
--- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java
+++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeImpl.java
@@ -20,6 +20,8 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.DescriptorProtos;
@@ -95,6 +97,16 @@ public abstract class CelRuntimeImpl implements CelRuntime {
@AutoValue.CopyAnnotations
abstract @Nullable ExtensionRegistry extensionRegistry();
+ // CelAsyncEvaluationOptions is an immutable value object configuring asynchronous evaluation.
+ @SuppressWarnings("Immutable")
+ @AutoValue.CopyAnnotations
+ abstract CelAsyncEvaluationOptions asyncEvaluationOptions();
+
+ // The executor service is an externally managed, thread-safe asynchronous execution pool.
+ @SuppressWarnings("Immutable")
+ @AutoValue.CopyAnnotations
+ abstract Optional asyncExecutor();
+
@Override
public Program createProgram(CelAbstractSyntaxTree ast) throws CelEvaluationException {
return toRuntimeProgram(planner().plan(ast));
@@ -162,6 +174,44 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException {
return program.eval(partialVars);
}
+ @Override
+ public ListenableFuture evalAsync() {
+ return program.evalAsync();
+ }
+
+ @Override
+ public ListenableFuture evalAsync(Map mapValue) {
+ return program.evalAsync(mapValue);
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ Map mapValue, CelFunctionResolver lateBoundFunctionResolver) {
+ return program.evalAsync(mapValue, lateBoundFunctionResolver);
+ }
+
+ @Override
+ public ListenableFuture evalAsync(Message message) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by this Program implementation.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(CelVariableResolver resolver) {
+ return program.evalAsync(resolver);
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
+ return program.evalAsync(resolver, lateBoundFunctionResolver);
+ }
+
+ @Override
+ public ListenableFuture evalAsync(PartialVars partialVars) {
+ return program.evalAsync(partialVars);
+ }
+
@Override
public Object trace(CelEvaluationListener listener) throws CelEvaluationException {
return ((PlannedProgram) program)
@@ -253,7 +303,8 @@ public static Builder newBuilder() {
.setFunctionBindings(ImmutableMap.of())
.setStandardFunctions(CelStandardFunctions.newBuilder().build())
.setContainer(CelContainer.newBuilder().build())
- .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry());
+ .setExtensionRegistry(ExtensionRegistry.getEmptyRegistry())
+ .setAsyncEvaluationOptions(CelAsyncEvaluationOptions.defaultOptions());
}
/** Builder for {@link CelRuntimeImpl}. */
@@ -280,6 +331,13 @@ public abstract static class Builder implements CelRuntimeBuilder {
@Override
public abstract Builder setContainer(CelContainer container);
+ @Override
+ public abstract Builder setAsyncEvaluationOptions(
+ CelAsyncEvaluationOptions asyncEvaluationOptions);
+
+ @Override
+ public abstract Builder setAsyncExecutor(ListeningExecutorService asyncExecutor);
+
abstract CelOptions options();
abstract CelContainer container();
diff --git a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java
index 428c6dba5..144de7e9d 100644
--- a/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java
+++ b/runtime/src/main/java/dev/cel/runtime/CelRuntimeLegacyImpl.java
@@ -20,6 +20,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import javax.annotation.concurrent.ThreadSafe;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
@@ -84,6 +85,8 @@ public final class CelRuntimeLegacyImpl implements CelRuntime {
private final ImmutableSet celRuntimeLibraries;
private final ImmutableList celFunctionBindings;
+ private final CelAsyncEvaluationOptions asyncEvaluationOptions;
+ private final @Nullable ListeningExecutorService asyncExecutor;
@Override
public CelRuntime.Program createProgram(CelAbstractSyntaxTree ast) {
@@ -101,7 +104,8 @@ public CelRuntimeBuilder toRuntimeBuilder() {
.setExtensionRegistry(extensionRegistry)
.addFileTypes(fileDescriptors)
.addLibraries(celRuntimeLibraries)
- .addFunctionBindings(celFunctionBindings);
+ .addFunctionBindings(celFunctionBindings)
+ .setAsyncEvaluationOptions(asyncEvaluationOptions);
if (customTypeFactory != null) {
builder.setTypeFactory(customTypeFactory);
@@ -111,6 +115,9 @@ public CelRuntimeBuilder toRuntimeBuilder() {
builder.setStandardFunctions(overriddenStandardFunctions);
}
+ if (asyncExecutor != null) {
+ builder.setAsyncExecutor(asyncExecutor);
+ }
return builder;
}
@@ -132,6 +139,8 @@ public static final class Builder implements CelRuntimeBuilder {
@VisibleForTesting Function customTypeFactory;
@VisibleForTesting CelStandardFunctions overriddenStandardFunctions;
+ @VisibleForTesting CelAsyncEvaluationOptions asyncEvaluationOptions;
+ @VisibleForTesting @Nullable ListeningExecutorService asyncExecutor;
private CelOptions options;
@@ -257,6 +266,19 @@ public CelRuntimeBuilder setContainer(CelContainer container) {
"This method is not supported for the legacy runtime");
}
+ @Override
+ public CelRuntimeBuilder setAsyncEvaluationOptions(
+ CelAsyncEvaluationOptions asyncEvaluationOptions) {
+ this.asyncEvaluationOptions = checkNotNull(asyncEvaluationOptions);
+ return this;
+ }
+
+ @Override
+ public CelRuntimeBuilder setAsyncExecutor(ListeningExecutorService asyncExecutor) {
+ this.asyncExecutor = checkNotNull(asyncExecutor);
+ return this;
+ }
+
/** Build a new {@code CelRuntimeLegacyImpl} instance from the builder config. */
@Override
public CelRuntimeLegacyImpl build() {
@@ -357,7 +379,9 @@ public CelRuntimeLegacyImpl build() {
overriddenStandardFunctions,
fileDescriptors,
runtimeLibraries,
- ImmutableList.copyOf(customFunctionBindings.values()));
+ ImmutableList.copyOf(customFunctionBindings.values()),
+ asyncEvaluationOptions,
+ asyncExecutor);
}
private ImmutableSet newStandardFunctionBindings(
@@ -432,6 +456,8 @@ private Builder() {
this.celRuntimeLibraries = ImmutableSet.builder();
this.extensionRegistry = ExtensionRegistry.getEmptyRegistry();
this.customTypeFactory = null;
+ this.asyncEvaluationOptions = CelAsyncEvaluationOptions.defaultOptions();
+ this.asyncExecutor = null;
}
}
@@ -444,7 +470,9 @@ private CelRuntimeLegacyImpl(
@Nullable CelStandardFunctions overriddenStandardFunctions,
ImmutableSet fileDescriptors,
ImmutableSet celRuntimeLibraries,
- ImmutableList celFunctionBindings) {
+ ImmutableList celFunctionBindings,
+ CelAsyncEvaluationOptions asyncEvaluationOptions,
+ @Nullable ListeningExecutorService asyncExecutor) {
this.interpreter = interpreter;
this.options = options;
this.standardEnvironmentEnabled = standardEnvironmentEnabled;
@@ -454,5 +482,7 @@ private CelRuntimeLegacyImpl(
this.fileDescriptors = fileDescriptors;
this.celRuntimeLibraries = celRuntimeLibraries;
this.celFunctionBindings = celFunctionBindings;
+ this.asyncEvaluationOptions = asyncEvaluationOptions;
+ this.asyncExecutor = asyncExecutor;
}
}
diff --git a/runtime/src/main/java/dev/cel/runtime/Program.java b/runtime/src/main/java/dev/cel/runtime/Program.java
index e808a373c..c9df239eb 100644
--- a/runtime/src/main/java/dev/cel/runtime/Program.java
+++ b/runtime/src/main/java/dev/cel/runtime/Program.java
@@ -14,6 +14,7 @@
package dev.cel.runtime;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.Immutable;
import java.util.Map;
@@ -46,4 +47,33 @@ Object eval(CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionR
/** Evaluate a compiled program with unknown attribute patterns {@code partialVars}. */
Object eval(PartialVars partialVars) throws CelEvaluationException;
+
+ /** Evaluate the expression asynchronously without any variables. */
+ ListenableFuture evalAsync();
+
+ /**
+ * Evaluate the expression asynchronously using a {@code mapValue} as the source of input
+ * variables.
+ */
+ ListenableFuture evalAsync(Map mapValue);
+
+ /**
+ * Evaluate the expression asynchronously using a {@code mapValue} as the source of input
+ * variables and late-bound functions {@code lateBoundFunctionResolver}.
+ */
+ ListenableFuture evalAsync(
+ Map mapValue, CelFunctionResolver lateBoundFunctionResolver);
+
+ /** Evaluate the expression asynchronously with a custom variable {@code resolver}. */
+ ListenableFuture evalAsync(CelVariableResolver resolver);
+
+ /**
+ * Evaluate the expression asynchronously with a custom variable {@code resolver} and late-bound
+ * functions {@code lateBoundFunctionResolver}.
+ */
+ ListenableFuture evalAsync(
+ CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver);
+
+ /** Evaluate the expression asynchronously with unknown attribute patterns {@code partialVars}. */
+ ListenableFuture evalAsync(PartialVars partialVars);
}
diff --git a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java
index 2543a9525..cc6795561 100644
--- a/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java
+++ b/runtime/src/main/java/dev/cel/runtime/ProgramImpl.java
@@ -16,6 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.Message;
import dev.cel.common.CelOptions;
@@ -68,6 +69,50 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException {
/* listener= */ Optional.empty());
}
+ @Override
+ public ListenableFuture evalAsync() {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(Map mapValue) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ Map mapValue, CelFunctionResolver lateBoundFunctionResolver) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(Message message) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(CelVariableResolver resolver) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(PartialVars partialVars) {
+ throw new UnsupportedOperationException(
+ "evalAsync is not supported by the legacy interpreter.");
+ }
+
@Override
public Object trace(CelEvaluationListener listener) throws CelEvaluationException {
return evalInternal(Activation.EMPTY, listener);
diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
index ca7665953..d4dbb1659 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
+++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
@@ -92,6 +92,7 @@ java_library(
"//runtime:resolved_overload",
"//runtime:variable_resolver",
"@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
],
)
@@ -622,6 +623,7 @@ cel_android_library(
"//runtime/src/main/java/dev/cel/runtime:program_android",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:org_jspecify_jspecify",
+ "@maven_android//:com_google_guava_guava",
],
)
diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java
index 1470e4909..f7f3d7f01 100644
--- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java
+++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java
@@ -15,6 +15,7 @@
package dev.cel.runtime.planner;
import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.Immutable;
import dev.cel.common.CelOptions;
import dev.cel.common.annotations.Internal;
@@ -129,6 +130,38 @@ public Object eval(PartialVars partialVars) throws CelEvaluationException {
/* listener= */ null);
}
+ @Override
+ public ListenableFuture evalAsync() {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(Map mapValue) {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ Map mapValue, CelFunctionResolver lateBoundFunctionResolver) {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(CelVariableResolver resolver) {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(
+ CelVariableResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
+ @Override
+ public ListenableFuture evalAsync(PartialVars partialVars) {
+ throw new UnsupportedOperationException("evalAsync is not supported by PlannedProgram.");
+ }
+
public Object evalOrThrow(
PlannedInterpretable interpretable,
GlobalResolver resolver,
diff --git a/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java b/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java
new file mode 100644
index 000000000..fc26513e7
--- /dev/null
+++ b/runtime/src/test/java/dev/cel/runtime/CelAsyncEvaluationOptionsTest.java
@@ -0,0 +1,120 @@
+// Copyright 2026 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import java.time.Duration;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import org.jspecify.annotations.Nullable;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class CelAsyncEvaluationOptionsTest {
+
+ private ScheduledExecutorService customScheduler;
+
+ @After
+ public void tearDown() {
+ if (customScheduler != null) {
+ customScheduler.shutdown();
+ }
+ }
+
+ @Test
+ public void defaultOptions_returnsDefaultValues() {
+ CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.defaultOptions();
+
+ assertThat(options.maxConcurrency()).isEqualTo(100);
+ assertThat(options.maxIterations()).isEqualTo(1_000);
+ assertThat(options.drainStrategy()).isNotNull();
+ assertThat(options.observer()).isEmpty();
+ assertThat(options.scheduledExecutorService()).isEmpty();
+ assertThat(options.resolveScheduledExecutorService()).isNotNull();
+ }
+
+ @Test
+ public void resolveScheduledExecutorService_defaultScheduler_runsAsDaemonThread()
+ throws Exception {
+ CelAsyncEvaluationOptions options = CelAsyncEvaluationOptions.defaultOptions();
+
+ Future isDaemonFuture =
+ options.resolveScheduledExecutorService().submit(() -> Thread.currentThread().isDaemon());
+
+ assertThat(isDaemonFuture.get(5, SECONDS)).isTrue();
+ }
+
+ @Test
+ public void builder_validations() {
+ CelAsyncEvaluationOptions.Builder builder = CelAsyncEvaluationOptions.builder();
+
+ assertThrows(NullPointerException.class, () -> builder.setDrainStrategy(null));
+ assertThrows(NullPointerException.class, () -> builder.setObserver(null));
+ assertThrows(NullPointerException.class, () -> builder.setScheduledExecutorService(null));
+ }
+
+ @Test
+ public void builder_nonPositiveMaxConcurrency_roundTripsCleanly() {
+ CelAsyncEvaluationOptions unboundedZero =
+ CelAsyncEvaluationOptions.builder().setMaxConcurrency(0).build();
+ CelAsyncEvaluationOptions unboundedNegative =
+ CelAsyncEvaluationOptions.builder().setMaxConcurrency(-1).build();
+
+ assertThat(unboundedZero.maxConcurrency()).isEqualTo(0);
+ assertThat(unboundedNegative.maxConcurrency()).isEqualTo(-1);
+ }
+
+ @Test
+ public void builder_customValuesAndRoundTrip() {
+ CelAsyncDrainStrategy drainStrategy = CelAsyncDrainStrategy.drainReady(Duration.ofMillis(25));
+ CelAsyncObserver observer =
+ new CelAsyncObserver() {
+ @Override
+ public void onCallStarted(CelAsyncCall call, ImmutableList args) {}
+
+ @Override
+ public void onCallFinished(
+ CelAsyncCall call, @Nullable Object result, @Nullable Throwable error) {}
+ };
+ customScheduler = Executors.newSingleThreadScheduledExecutor();
+
+ CelAsyncEvaluationOptions options =
+ CelAsyncEvaluationOptions.builder()
+ .setMaxConcurrency(8)
+ .setMaxIterations(50)
+ .setDrainStrategy(drainStrategy)
+ .setObserver(observer)
+ .setScheduledExecutorService(customScheduler)
+ .build();
+
+ assertThat(options.maxConcurrency()).isEqualTo(8);
+ assertThat(options.maxIterations()).isEqualTo(50);
+ assertThat(options.drainStrategy()).isSameInstanceAs(drainStrategy);
+ assertThat(options.observer()).hasValue(observer);
+ assertThat(options.scheduledExecutorService()).hasValue(customScheduler);
+ assertThat(options.resolveScheduledExecutorService()).isSameInstanceAs(customScheduler);
+
+ CelAsyncEvaluationOptions copy = options.toBuilder().build();
+ assertThat(copy).isEqualTo(options);
+ }
+}
diff --git a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java
index fec5fab41..5bef0c61e 100644
--- a/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java
+++ b/runtime/src/test/java/dev/cel/runtime/CelRuntimeLegacyImplTest.java
@@ -15,7 +15,11 @@
package dev.cel.runtime;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+import static org.junit.Assert.assertThrows;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.protobuf.Message;
import dev.cel.common.CelException;
import dev.cel.common.exceptions.CelDivideByZeroException;
@@ -23,8 +27,8 @@
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.expr.conformance.proto3.TestAllTypes;
import dev.cel.runtime.CelStandardFunctions.StandardFunction;
+import java.util.Optional;
import java.util.function.Function;
-import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -37,7 +41,7 @@ public void evalException() throws CelException {
CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build();
CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
CelRuntime.Program program = runtime.createProgram(compiler.compile("1/0").getAst());
- CelEvaluationException e = Assert.assertThrows(CelEvaluationException.class, program::eval);
+ CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval);
assertThat(e).hasCauseThat().isInstanceOf(CelDivideByZeroException.class);
}
@@ -120,4 +124,44 @@ public void toRuntimeBuilder_optionalProperties() {
assertThat(newRuntimeBuilder.overriddenStandardFunctions)
.isEqualTo(overriddenStandardFunctions);
}
+
+ @Test
+ public void toRuntimeBuilder_asyncProperties_copied() {
+ ListeningExecutorService executor = newDirectExecutorService();
+ CelAsyncEvaluationOptions options =
+ CelAsyncEvaluationOptions.newBuilder().setMaxConcurrency(5).build();
+ CelRuntimeBuilder celRuntimeBuilder =
+ CelRuntimeFactory.standardCelRuntimeBuilder()
+ .setAsyncEvaluationOptions(options)
+ .setAsyncExecutor(executor);
+ CelRuntime celRuntime = celRuntimeBuilder.build();
+
+ CelRuntimeLegacyImpl.Builder newRuntimeBuilder =
+ (CelRuntimeLegacyImpl.Builder) celRuntime.toRuntimeBuilder();
+
+ assertThat(newRuntimeBuilder.asyncEvaluationOptions).isEqualTo(options);
+ assertThat(newRuntimeBuilder.asyncExecutor).isEqualTo(executor);
+ }
+
+ @Test
+ public void evalAsync_legacyInterpreter_throwsUnsupportedOperationException() throws Exception {
+ CelCompiler compiler = CelCompilerFactory.standardCelCompilerBuilder().build();
+ CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build();
+ CelRuntime.Program program = runtime.createProgram(compiler.compile("1 + 1").getAst());
+ CelVariableResolver resolver = name -> Optional.of(1L);
+
+ assertThrows(UnsupportedOperationException.class, program::evalAsync);
+ assertThrows(UnsupportedOperationException.class, () -> program.evalAsync(ImmutableMap.of()));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> program.evalAsync(ImmutableMap.of(), CelFunctionResolver.EMPTY));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> program.evalAsync(TestAllTypes.getDefaultInstance()));
+ assertThrows(UnsupportedOperationException.class, () -> program.evalAsync(resolver));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> program.evalAsync(resolver, CelFunctionResolver.EMPTY));
+ assertThrows(UnsupportedOperationException.class, () -> program.evalAsync((PartialVars) null));
+ }
}