Skip to content

Commit 19c5231

Browse files
l46kokcopybara-github
authored andcommitted
Track in-flight async call IDs in AccumulatedUnknowns
PiperOrigin-RevId: 982605765
1 parent 612f7de commit 19c5231

4 files changed

Lines changed: 169 additions & 13 deletions

File tree

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616

1717
import com.google.errorprone.annotations.CanIgnoreReturnValue;
1818
import dev.cel.common.annotations.Internal;
19-
import java.util.ArrayList;
20-
import java.util.Arrays;
2119
import java.util.Collection;
20+
import java.util.Collections;
2221
import java.util.HashSet;
2322
import java.util.Set;
2423
import org.jspecify.annotations.Nullable;
@@ -35,6 +34,7 @@ public final class AccumulatedUnknowns {
3534
private static final int MAX_UNKNOWN_ATTRIBUTE_SIZE = 500_000;
3635
private final Set<Long> exprIds;
3736
private final Set<CelAttribute> attributes;
37+
private final Set<Long> callIds;
3838

3939
Set<Long> exprIds() {
4040
return exprIds;
@@ -44,6 +44,17 @@ Set<CelAttribute> attributes() {
4444
return attributes;
4545
}
4646

47+
/**
48+
* Returns the in-flight asynchronous call IDs this unknown is waiting on.
49+
*
50+
* <p>The returned set is an unmodifiable <em>view</em> over this mutable accumulator, not a
51+
* snapshot: a subsequent {@link #merge} on this instance is visible through it. Callers that
52+
* retain the set beyond the current evaluation step must copy it.
53+
*/
54+
public Set<Long> callIds() {
55+
return Collections.unmodifiableSet(callIds);
56+
}
57+
4758
/**
4859
* Evaluates if the right hand side is an accumulated unknown, and if so, merges it into the
4960
* accumulator.
@@ -62,20 +73,29 @@ public AccumulatedUnknowns merge(AccumulatedUnknowns arg) {
6273
enforceMaxAttributeSize(this.attributes, arg.attributes);
6374
this.exprIds.addAll(arg.exprIds);
6475
this.attributes.addAll(arg.attributes);
76+
this.callIds.addAll(arg.callIds);
6577
return this;
6678
}
6779

68-
static AccumulatedUnknowns create(Long... ids) {
69-
return create(Arrays.asList(ids));
70-
}
71-
72-
static AccumulatedUnknowns create(Collection<Long> ids) {
73-
return create(ids, new ArrayList<>());
80+
static AccumulatedUnknowns create(long exprId) {
81+
return new AccumulatedUnknowns(
82+
Collections.singletonList(exprId), Collections.emptyList(), Collections.emptyList());
7483
}
7584

7685
public static AccumulatedUnknowns create(
7786
Collection<Long> exprIds, Collection<CelAttribute> attributes) {
78-
return new AccumulatedUnknowns(new HashSet<>(exprIds), new HashSet<>(attributes));
87+
return new AccumulatedUnknowns(exprIds, attributes, Collections.emptyList());
88+
}
89+
90+
/**
91+
* Creates an accumulated unknown for a pending asynchronous call, recording {@code exprId} so the
92+
* unknown retains its origin when adapted into a {@link CelUnknownSet}.
93+
*/
94+
public static AccumulatedUnknowns createForAsyncCall(long exprId, long callId) {
95+
return new AccumulatedUnknowns(
96+
Collections.singletonList(exprId),
97+
Collections.emptyList(),
98+
Collections.singletonList(callId));
7999
}
80100

81101
private static void enforceMaxAttributeSize(
@@ -88,8 +108,10 @@ private static void enforceMaxAttributeSize(
88108
}
89109
}
90110

91-
private AccumulatedUnknowns(Set<Long> exprIds, Set<CelAttribute> attributes) {
92-
this.exprIds = exprIds;
93-
this.attributes = attributes;
111+
private AccumulatedUnknowns(
112+
Collection<Long> exprIds, Collection<CelAttribute> attributes, Collection<Long> callIds) {
113+
this.exprIds = new HashSet<>(exprIds);
114+
this.attributes = new HashSet<>(attributes);
115+
this.callIds = new HashSet<>(callIds);
94116
}
95117
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import dev.cel.common.annotations.Internal;
1818
import java.util.ArrayList;
19+
import java.util.Collections;
1920
import java.util.Optional;
2021

2122
/**
@@ -100,7 +101,7 @@ Optional<Object> maybeUnknowns() {
100101
}
101102

102103
if (!exprIds.isEmpty()) {
103-
return Optional.of(AccumulatedUnknowns.create(exprIds));
104+
return Optional.of(AccumulatedUnknowns.create(exprIds, Collections.emptyList()));
104105
}
105106

106107
return Optional.empty();
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.junit.Assert.assertThrows;
19+
20+
import com.google.common.collect.ImmutableList;
21+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
22+
import java.util.Set;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
26+
@RunWith(TestParameterInjector.class)
27+
public final class AccumulatedUnknownsTest {
28+
29+
@Test
30+
public void createForAsyncCall_success() {
31+
AccumulatedUnknowns unknowns = AccumulatedUnknowns.createForAsyncCall(7L, 42L);
32+
33+
assertThat(unknowns.callIds()).containsExactly(42L);
34+
assertThat(unknowns.exprIds()).containsExactly(7L);
35+
assertThat(unknowns.attributes()).isEmpty();
36+
}
37+
38+
@Test
39+
public void createForAsyncCall_adaptedToCelUnknownSet_retainsExprId() {
40+
AccumulatedUnknowns unknowns = AccumulatedUnknowns.createForAsyncCall(7L, 42L);
41+
42+
Object adapted = InterpreterUtil.maybeAdaptToCelUnknownSet(unknowns);
43+
44+
assertThat(adapted).isEqualTo(CelUnknownSet.create(7L));
45+
}
46+
47+
@Test
48+
public void createForAsyncCall_distinctExprIds_adaptToDistinctUnknownSets() {
49+
Object first =
50+
InterpreterUtil.maybeAdaptToCelUnknownSet(AccumulatedUnknowns.createForAsyncCall(7L, 42L));
51+
Object second =
52+
InterpreterUtil.maybeAdaptToCelUnknownSet(AccumulatedUnknowns.createForAsyncCall(8L, 43L));
53+
54+
assertThat(first).isNotEqualTo(second);
55+
}
56+
57+
@Test
58+
public void callIds_returnsUnmodifiableSet() {
59+
AccumulatedUnknowns unknowns = AccumulatedUnknowns.createForAsyncCall(7L, 42L);
60+
Set<Long> callIds = unknowns.callIds();
61+
62+
assertThrows(UnsupportedOperationException.class, () -> callIds.add(99L));
63+
}
64+
65+
@Test
66+
public void merge_mergesCallIdsAndExprIdsAndAttributes() {
67+
AccumulatedUnknowns u1 =
68+
AccumulatedUnknowns.create(ImmutableList.of(1L), ImmutableList.of(CelAttribute.EMPTY));
69+
u1.merge(AccumulatedUnknowns.createForAsyncCall(1L, 100L));
70+
AccumulatedUnknowns u2 = AccumulatedUnknowns.create(ImmutableList.of(2L), ImmutableList.of());
71+
u2.merge(AccumulatedUnknowns.createForAsyncCall(2L, 200L));
72+
73+
AccumulatedUnknowns merged = u1.merge(u2);
74+
75+
assertThat(merged).isSameInstanceAs(u1);
76+
assertThat(merged.exprIds()).containsExactly(1L, 2L);
77+
assertThat(merged.attributes()).containsExactly(CelAttribute.EMPTY);
78+
assertThat(merged.callIds()).containsExactly(100L, 200L);
79+
}
80+
81+
@Test
82+
public void maybeMerge_withNullAccumulator_returnsNewUnknowns() {
83+
AccumulatedUnknowns u = AccumulatedUnknowns.createForAsyncCall(1L, 1L);
84+
85+
AccumulatedUnknowns result = AccumulatedUnknowns.maybeMerge(null, u);
86+
87+
assertThat(result).isSameInstanceAs(u);
88+
}
89+
90+
@Test
91+
public void maybeMerge_withExistingAccumulator_mergesBoth() {
92+
AccumulatedUnknowns u1 = AccumulatedUnknowns.createForAsyncCall(1L, 1L);
93+
AccumulatedUnknowns u2 = AccumulatedUnknowns.createForAsyncCall(2L, 2L);
94+
95+
AccumulatedUnknowns result = AccumulatedUnknowns.maybeMerge(u1, u2);
96+
97+
assertThat(result).isSameInstanceAs(u1);
98+
assertThat(result.callIds()).containsExactly(1L, 2L);
99+
assertThat(result.exprIds()).containsExactly(1L, 2L);
100+
}
101+
102+
@Test
103+
public void maybeMerge_withNonUnknownObject_returnsOriginalAccumulator() {
104+
AccumulatedUnknowns u = AccumulatedUnknowns.createForAsyncCall(1L, 1L);
105+
106+
AccumulatedUnknowns result = AccumulatedUnknowns.maybeMerge(u, "not an unknown");
107+
108+
assertThat(result).isSameInstanceAs(u);
109+
assertThat(result.callIds()).containsExactly(1L);
110+
}
111+
112+
@Test
113+
public void create_singleExprId_populatesExprIdsOnly() {
114+
AccumulatedUnknowns u = AccumulatedUnknowns.create(10L);
115+
116+
assertThat(u.exprIds()).containsExactly(10L);
117+
assertThat(u.attributes()).isEmpty();
118+
assertThat(u.callIds()).isEmpty();
119+
}
120+
121+
@Test
122+
public void create_exprIdsAndAttributes_leavesCallIdsEmpty() {
123+
AccumulatedUnknowns u =
124+
AccumulatedUnknowns.create(
125+
ImmutableList.of(10L, 20L), ImmutableList.of(CelAttribute.EMPTY));
126+
127+
assertThat(u.exprIds()).containsExactly(10L, 20L);
128+
assertThat(u.attributes()).containsExactly(CelAttribute.EMPTY);
129+
assertThat(u.callIds()).isEmpty();
130+
}
131+
}

runtime/src/test/java/dev/cel/runtime/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ java_library(
6565
"//parser:macro",
6666
"//parser:unparser",
6767
"//runtime",
68+
"//runtime:accumulated_unknowns",
6869
"//runtime:activation",
6970
"//runtime:dispatcher",
7071
"//runtime:evaluation_exception_builder",
@@ -73,6 +74,7 @@ java_library(
7374
"//runtime:function_resolver",
7475
"//runtime:interpretable",
7576
"//runtime:interpreter",
77+
"//runtime:interpreter_util",
7678
"//runtime:late_function_binding",
7779
"//runtime:lite_runtime",
7880
"//runtime:lite_runtime_factory",

0 commit comments

Comments
 (0)