Skip to content

Commit 870588e

Browse files
l46kokcopybara-github
authored andcommitted
Add async state and in-flight tracking primitives.
PiperOrigin-RevId: 975452308
1 parent 248b623 commit 870588e

14 files changed

Lines changed: 2202 additions & 123 deletions

runtime/planner/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ java_library(
3535
visibility = ["//:internal"],
3636
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_completion_coordinator"],
3737
)
38+
39+
java_library(
40+
name = "async_call_state_tracker",
41+
visibility = ["//:internal"],
42+
exports = ["//runtime/src/main/java/dev/cel/runtime/planner:async_call_state_tracker"],
43+
)

runtime/src/main/java/dev/cel/runtime/AccumulatedUnknowns.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Arrays;
2121
import java.util.Collection;
22+
import java.util.Collections;
2223
import java.util.HashSet;
2324
import java.util.Set;
2425
import org.jspecify.annotations.Nullable;
@@ -35,6 +36,7 @@ public final class AccumulatedUnknowns {
3536
private static final int MAX_UNKNOWN_ATTRIBUTE_SIZE = 500_000;
3637
private final Set<Long> exprIds;
3738
private final Set<CelAttribute> attributes;
39+
private final Set<Long> callIds;
3840

3941
Set<Long> exprIds() {
4042
return exprIds;
@@ -44,6 +46,14 @@ Set<CelAttribute> attributes() {
4446
return attributes;
4547
}
4648

49+
public Set<Long> callIds() {
50+
return Collections.unmodifiableSet(callIds);
51+
}
52+
53+
public boolean hasCallIds() {
54+
return !callIds.isEmpty();
55+
}
56+
4757
/**
4858
* Evaluates if the right hand side is an accumulated unknown, and if so, merges it into the
4959
* accumulator.
@@ -62,6 +72,7 @@ public AccumulatedUnknowns merge(AccumulatedUnknowns arg) {
6272
enforceMaxAttributeSize(this.attributes, arg.attributes);
6373
this.exprIds.addAll(arg.exprIds);
6474
this.attributes.addAll(arg.attributes);
75+
this.callIds.addAll(arg.callIds);
6576
return this;
6677
}
6778

@@ -75,7 +86,20 @@ static AccumulatedUnknowns create(Collection<Long> ids) {
7586

7687
public static AccumulatedUnknowns create(
7788
Collection<Long> exprIds, Collection<CelAttribute> attributes) {
78-
return new AccumulatedUnknowns(new HashSet<>(exprIds), new HashSet<>(attributes));
89+
return new AccumulatedUnknowns(
90+
new HashSet<>(exprIds), new HashSet<>(attributes), new HashSet<>());
91+
}
92+
93+
/**
94+
* Creates an accumulated unknown for a pending asynchronous call, recording {@code exprId} so the
95+
* unknown retains its origin when adapted into a {@link CelUnknownSet}.
96+
*/
97+
public static AccumulatedUnknowns createForAsyncCall(long exprId, long callId) {
98+
HashSet<Long> exprIds = new HashSet<>();
99+
exprIds.add(exprId);
100+
HashSet<Long> callIds = new HashSet<>();
101+
callIds.add(callId);
102+
return new AccumulatedUnknowns(exprIds, new HashSet<>(), callIds);
79103
}
80104

81105
private static void enforceMaxAttributeSize(
@@ -88,8 +112,9 @@ private static void enforceMaxAttributeSize(
88112
}
89113
}
90114

91-
private AccumulatedUnknowns(Set<Long> exprIds, Set<CelAttribute> attributes) {
115+
private AccumulatedUnknowns(Set<Long> exprIds, Set<CelAttribute> attributes, Set<Long> callIds) {
92116
this.exprIds = exprIds;
93117
this.attributes = attributes;
118+
this.callIds = callIds;
94119
}
95120
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.runtime.planner;
16+
17+
import static java.util.Objects.requireNonNull;
18+
19+
import dev.cel.runtime.RuntimeEquality;
20+
import java.util.Arrays;
21+
import java.util.Iterator;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
26+
/**
27+
* Unique cache key for an asynchronous function invocation at a given AST expression node.
28+
*
29+
* <p>Equality delegates to {@link RuntimeEquality}, except that {@link Double#NaN} and {@link
30+
* Float#NaN} argument values compare equal so that re-evaluating a node with a NaN argument hits
31+
* its existing call record. Map keys do not get this override because lookup goes through {@link
32+
* RuntimeEquality#findInMap}.
33+
*/
34+
final class AsyncCallKey {
35+
private final long exprId;
36+
private final String functionName;
37+
private final String overloadId;
38+
private final Object[] args;
39+
private final RuntimeEquality runtimeEquality;
40+
private final int hashCode;
41+
42+
static AsyncCallKey create(
43+
long exprId,
44+
String functionName,
45+
String overloadId,
46+
Object[] args,
47+
RuntimeEquality runtimeEquality) {
48+
return new AsyncCallKey(exprId, functionName, overloadId, args, runtimeEquality);
49+
}
50+
51+
@Override
52+
public boolean equals(Object o) {
53+
if (this == o) {
54+
return true;
55+
}
56+
if (!(o instanceof AsyncCallKey)) {
57+
return false;
58+
}
59+
AsyncCallKey other = (AsyncCallKey) o;
60+
if (exprId != other.exprId
61+
|| !functionName.equals(other.functionName)
62+
|| !overloadId.equals(other.overloadId)
63+
|| args.length != other.args.length) {
64+
return false;
65+
}
66+
for (int i = 0; i < args.length; i++) {
67+
if (!argEquals(args[i], other.args[i], runtimeEquality)) {
68+
return false;
69+
}
70+
}
71+
return true;
72+
}
73+
74+
@Override
75+
public int hashCode() {
76+
return hashCode;
77+
}
78+
79+
private static boolean argEquals(Object a, Object b, RuntimeEquality runtimeEquality) {
80+
if (a == b) {
81+
return true;
82+
}
83+
if (a == null || b == null) {
84+
return false;
85+
}
86+
if (a instanceof Number && b instanceof Number) {
87+
double da = ((Number) a).doubleValue();
88+
double db = ((Number) b).doubleValue();
89+
// CEL defines NaN != NaN; override so a node re-evaluated with NaN hits its existing record.
90+
if (Double.isNaN(da) && Double.isNaN(db)) {
91+
return true;
92+
}
93+
if (da == 0.0d && db == 0.0d) {
94+
return celEquals(normalizeSignedZero(a, da), normalizeSignedZero(b, db), runtimeEquality);
95+
}
96+
return celEquals(a, b, runtimeEquality);
97+
}
98+
if (a instanceof List && b instanceof List) {
99+
List<?> listA = (List<?>) a;
100+
List<?> listB = (List<?>) b;
101+
if (listA.size() != listB.size()) {
102+
return false;
103+
}
104+
Iterator<?> iterA = listA.iterator();
105+
Iterator<?> iterB = listB.iterator();
106+
while (iterA.hasNext() && iterB.hasNext()) {
107+
if (!argEquals(iterA.next(), iterB.next(), runtimeEquality)) {
108+
return false;
109+
}
110+
}
111+
return true;
112+
}
113+
if (a instanceof Map && b instanceof Map) {
114+
Map<?, ?> mapA = (Map<?, ?>) a;
115+
Map<?, ?> mapB = (Map<?, ?>) b;
116+
if (mapA.size() != mapB.size()) {
117+
return false;
118+
}
119+
for (Map.Entry<?, ?> entry : mapA.entrySet()) {
120+
Optional<Object> valB = findInMap(mapB, entry.getKey(), runtimeEquality);
121+
if (valB.isPresent()) {
122+
if (!argEquals(entry.getValue(), valB.get(), runtimeEquality)) {
123+
return false;
124+
}
125+
} else {
126+
if (!mapB.containsKey(entry.getKey())
127+
|| entry.getValue() != null
128+
|| mapB.get(entry.getKey()) != null) {
129+
return false;
130+
}
131+
}
132+
}
133+
return true;
134+
}
135+
if (a instanceof byte[] && b instanceof byte[]) {
136+
return Arrays.equals((byte[]) a, (byte[]) b);
137+
}
138+
if (a instanceof Object[] && b instanceof Object[]) {
139+
return Arrays.deepEquals((Object[]) a, (Object[]) b);
140+
}
141+
return celEquals(a, b, runtimeEquality);
142+
}
143+
144+
/**
145+
* Applies CEL heterogeneous equality, treating incomparable argument pairs (which throw unchecked
146+
* exceptions from {@link RuntimeEquality#objectEquals}) as unequal.
147+
*/
148+
private static boolean celEquals(Object a, Object b, RuntimeEquality runtimeEquality) {
149+
try {
150+
return runtimeEquality.objectEquals(a, b);
151+
} catch (RuntimeException e) {
152+
return false;
153+
}
154+
}
155+
156+
/**
157+
* Normalizes {@code -0.0} to {@code 0.0} before comparison, because {@link
158+
* RuntimeEquality#objectEquals} returns {@code false} for cross-type pairs such as {@code (0L,
159+
* -0.0d)}.
160+
*/
161+
private static Object normalizeSignedZero(Object value, double asDouble) {
162+
if (asDouble == 0.0d && (value instanceof Double || value instanceof Float)) {
163+
return 0.0d;
164+
}
165+
return value;
166+
}
167+
168+
private static Optional<Object> findInMap(
169+
Map<?, ?> map, Object key, RuntimeEquality runtimeEquality) {
170+
try {
171+
return runtimeEquality.findInMap(map, key);
172+
} catch (RuntimeException e) {
173+
return Optional.empty();
174+
}
175+
}
176+
177+
private static int computeHashCode(
178+
long exprId,
179+
String functionName,
180+
String overloadId,
181+
Object[] args,
182+
RuntimeEquality runtimeEquality) {
183+
int result = (int) (exprId ^ (exprId >>> 32));
184+
result = 31 * result + functionName.hashCode();
185+
result = 31 * result + overloadId.hashCode();
186+
for (Object arg : args) {
187+
result = 31 * result + hashArg(arg, runtimeEquality);
188+
}
189+
return result;
190+
}
191+
192+
/**
193+
* Hashes a single argument consistently with {@link #argEquals}.
194+
*
195+
* <p>Does not delegate to {@link RuntimeEquality#hashCode} because that method hashes {@code
196+
* -0.0} and {@code 0.0} differently, which would break the {@link Object#hashCode} contract for
197+
* keys containing signed zero.
198+
*/
199+
private static int hashArg(Object arg, RuntimeEquality runtimeEquality) {
200+
if (arg == null) {
201+
return 0;
202+
}
203+
if (arg instanceof Number) {
204+
double d = ((Number) arg).doubleValue();
205+
// Normalize -0.0d to +0.0d so that values CEL considers equal hash identically.
206+
if (d == 0.0d) {
207+
d = 0.0d;
208+
}
209+
return Double.hashCode(d);
210+
}
211+
if (arg instanceof Iterable) {
212+
int h = 1;
213+
for (Object elem : (Iterable<?>) arg) {
214+
h = h * 31 + hashArg(elem, runtimeEquality);
215+
}
216+
return h;
217+
}
218+
if (arg instanceof Map) {
219+
int h = 0;
220+
for (Map.Entry<?, ?> entry : ((Map<?, ?>) arg).entrySet()) {
221+
h += hashArg(entry.getKey(), runtimeEquality) ^ hashArg(entry.getValue(), runtimeEquality);
222+
}
223+
return h;
224+
}
225+
if (arg instanceof byte[]) {
226+
return Arrays.hashCode((byte[]) arg);
227+
}
228+
if (arg instanceof Object[]) {
229+
return Arrays.deepHashCode((Object[]) arg);
230+
}
231+
return runtimeEquality.hashCode(arg);
232+
}
233+
234+
private AsyncCallKey(
235+
long exprId,
236+
String functionName,
237+
String overloadId,
238+
Object[] args,
239+
RuntimeEquality runtimeEquality) {
240+
this.exprId = exprId;
241+
this.functionName = requireNonNull(functionName);
242+
this.overloadId = requireNonNull(overloadId);
243+
this.args = args.clone();
244+
this.runtimeEquality = requireNonNull(runtimeEquality);
245+
this.hashCode = computeHashCode(exprId, functionName, overloadId, this.args, runtimeEquality);
246+
}
247+
}

0 commit comments

Comments
 (0)