Skip to content

Commit 73248e3

Browse files
hatyonormen662
andauthored
Fix instability in the query planner (#3185)
This fixes a number of instabilities found in Cascades when planning certain type of queries that end up generating a relatively large number of variations that might end up storing actual duplicates in the memoization structure (which is permissible). Essentially, the following issues were detected and resolved in this PR: 1. **Fix instability in hash calculation**: Certain expressions had their hash unstable, because it includes hashing an `enum` member, which is [known to be unstable](https://bugs.openjdk.org/browse/JDK-8050217?page=com.atlassian.jira.plugin.system.issuetabpanels%3Aall-tabpanel). This issue made it very difficult to compare planner traces from different runs, since a planner trace might include the hash codes of some expressions, and the hash code could change across different JVMs, causing the comparator to report false negatives. 2. **Set instantiation with deterministic traversal order**: Certain instantiations of `Maps.newIdentityHashMap` and `Sets.newHashSet` are replaced with `LinkedIdentityMap`, resp. `LinkedIdentitySet`. 3. **Fix instability detected in Guava `StandardMutableNetwork`**: We use Guava [`Network` ](https://guava.dev/releases/20.0/api/docs/com/google/common/graph/Network.html) for the planner memoization structure, internally, the network uses a hash map to store the parallel edges between network vertices, this is exactly why we were getting the alternatives in different order as explained above. Due to the way the `Network` code is written, it is not possible to pass the (type of the) map to the builder, therefore, I went ahead and copied enough of the code to be able to modify the map construction and make it stable, which effectively fixed the issue, but ideally, Guava should make it possible to control how these maps are created, making it easier to use and reason about the code. This fixes #3191. --------- Co-authored-by: Normen Seemann <[email protected]>
1 parent cc186ba commit 73248e3

37 files changed

+1225
-692
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/expressions/Comparisons.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,7 @@ public int hashCode() {
16791679
}
16801680

16811681
public int computeHashCode() {
1682-
return Objects.hash(type, relatedByEquality());
1682+
return Objects.hash(type.name(), relatedByEquality());
16831683
}
16841684

16851685
private Set<String> relatedByEquality() {
@@ -2091,7 +2091,7 @@ public boolean equals(Object o) {
20912091

20922092
@Override
20932093
public int hashCode() {
2094-
return Objects.hash(type);
2094+
return Objects.hash(type.name());
20952095
}
20962096

20972097
@Override

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CascadesRuleCall.java

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
import com.google.common.base.Preconditions;
3333
import com.google.common.base.Verify;
3434
import com.google.common.collect.ImmutableList;
35+
import com.google.common.collect.Iterables;
3536
import com.google.common.collect.Sets;
3637

3738
import javax.annotation.Nonnull;
3839
import java.util.Arrays;
3940
import java.util.Collection;
4041
import java.util.Collections;
4142
import java.util.Deque;
43+
import java.util.Objects;
4244
import java.util.Optional;
4345
import java.util.Set;
4446
import java.util.function.Function;
@@ -241,8 +243,7 @@ public Reference memoizeExpression(@Nonnull final RelationalExpression expressio
241243
if (expression.getQuantifiers().isEmpty()) {
242244
return memoizeLeafExpression(expression);
243245
}
244-
245-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.BEGIN)));
246+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.begin()));
246247
try {
247248
Preconditions.checkArgument(!(expression instanceof RecordQueryPlan));
248249

@@ -278,28 +279,33 @@ public Reference memoizeExpression(@Nonnull final RelationalExpression expressio
278279
commonReferencingExpressions.retainAll(referencingExpressionsIterator.next());
279280
}
280281

281-
for (final var commonReferencingExpression : commonReferencingExpressions) {
282-
if (Reference.isMemoizedExpression(commonReferencingExpression, expression)) {
283-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.REUSED)));
284-
final var reference = expressionToReferenceMap.get(commonReferencingExpression);
285-
Verify.verifyNotNull(reference);
286-
Verify.verify(reference != this.root);
287-
return reference;
288-
}
282+
commonReferencingExpressions.removeIf(commonReferencingExpression -> !Reference.isMemoizedExpression(expression, commonReferencingExpression));
283+
284+
if (!commonReferencingExpressions.isEmpty()) {
285+
Debugger.withDebugger(debugger ->
286+
debugger.onEvent(Debugger.InsertIntoMemoEvent.reusedExpWithReferences(expression,
287+
commonReferencingExpressions.stream()
288+
.map(expressionToReferenceMap::get)
289+
.collect(ImmutableList.toImmutableList()))));
290+
final var reference = expressionToReferenceMap.get(Objects.requireNonNull(Iterables.getFirst(commonReferencingExpressions, null)));
291+
Verify.verifyNotNull(reference);
292+
Verify.verify(reference != this.root);
293+
return reference;
289294
}
290-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.NEW)));
295+
296+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.newExp(expression)));
291297
final var newRef = Reference.of(expression);
292298
traversal.addExpression(newRef, expression);
293299
return newRef;
294300
} finally {
295-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.END)));
301+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.end()));
296302
}
297303
}
298304

299305
@Nonnull
300306
@Override
301307
public Reference memoizeLeafExpression(@Nonnull final RelationalExpression expression) {
302-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.BEGIN)));
308+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.begin()));
303309
try {
304310
Preconditions.checkArgument(!(expression instanceof RecordQueryPlan));
305311
Preconditions.checkArgument(expression.getQuantifiers().isEmpty());
@@ -309,17 +315,17 @@ public Reference memoizeLeafExpression(@Nonnull final RelationalExpression expre
309315
for (final var leafRef : leafRefs) {
310316
for (final var member : leafRef.getMembers()) {
311317
if (Reference.isMemoizedExpression(expression, member)) {
312-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.REUSED)));
318+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.reusedExp(expression)));
313319
return leafRef;
314320
}
315321
}
316322
}
317-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.NEW)));
323+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.newExp(expression)));
318324
final var newRef = Reference.of(expression);
319325
traversal.addExpression(newRef, expression);
320326
return newRef;
321327
} finally {
322-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.END)));
328+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.end()));
323329
}
324330
}
325331

@@ -351,7 +357,8 @@ public Reference memoizeReference(@Nonnull final Reference reference) {
351357
@Nonnull
352358
private Reference memoizeExpressionsExactly(@Nonnull final Collection<? extends RelationalExpression> expressions,
353359
@Nonnull Function<Set<? extends RelationalExpression>, Reference> referenceCreator) {
354-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.BEGIN)));
360+
Debugger.withDebugger(debugger -> expressions.forEach(
361+
expression -> debugger.onEvent(Debugger.InsertIntoMemoEvent.begin())));
355362
try {
356363
final var expressionSet = new LinkedIdentitySet<>(expressions);
357364

@@ -367,19 +374,21 @@ private Reference memoizeExpressionsExactly(@Nonnull final Collection<? extends
367374
final Optional<Reference> memoizedRefMaybe = findExpressionsInMemo(expressionSet);
368375
if (memoizedRefMaybe.isPresent()) {
369376
Debugger.withDebugger(debugger ->
370-
expressionSet.forEach(plan -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.REUSED))));
377+
expressionSet.forEach(
378+
plan -> debugger.onEvent(Debugger.InsertIntoMemoEvent.reusedExpWithReferences(plan, ImmutableList.of(memoizedRefMaybe.get())))));
371379
return memoizedRefMaybe.get();
372380
}
373381
}
374382

375383
final var newRef = referenceCreator.apply(expressionSet);
376384
for (final var plan : expressionSet) {
377-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.NEW)));
385+
Debugger.withDebugger(debugger -> expressions.forEach(
386+
expression -> debugger.onEvent(Debugger.InsertIntoMemoEvent.newExp(expression))));
378387
traversal.addExpression(newRef, plan);
379388
}
380389
return newRef;
381390
} finally {
382-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.END)));
391+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.end()));
383392
}
384393
}
385394

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/LinkedIdentityMap.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> funct
128128
map.replaceAll((wrappedK, v) -> function.apply(wrappedK.get(), v));
129129
}
130130

131+
@Override
132+
public V remove(final Object key) {
133+
return map.remove(identity.wrap(key));
134+
}
135+
131136
@Override
132137
public boolean equals(final Object o) {
133138
if (!(o instanceof LinkedIdentityMap)) {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/Reference.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,23 @@ public boolean insert(@Nonnull final RelationalExpression newValue) {
185185
* otherwise.
186186
*/
187187
private boolean insert(@Nonnull final RelationalExpression newValue, @Nullable final Map<PlanProperty<?>, ?> precomputedPropertiesMap) {
188-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.BEGIN)));
188+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.begin()));
189189
try {
190190
final boolean containsInMemo = containsInMemo(newValue);
191-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(containsInMemo ? Debugger.Location.REUSED : Debugger.Location.NEW)));
192-
191+
Debugger.withDebugger(debugger -> {
192+
if (containsInMemo) {
193+
debugger.onEvent(Debugger.InsertIntoMemoEvent.reusedExpWithReferences(newValue, ImmutableList.of(this)));
194+
} else {
195+
debugger.onEvent(Debugger.InsertIntoMemoEvent.newExp(newValue));
196+
}
197+
});
193198
if (!containsInMemo) {
194199
insertUnchecked(newValue, precomputedPropertiesMap);
195200
return true;
196201
}
197202
return false;
198203
} finally {
199-
Debugger.withDebugger(debugger -> debugger.onEvent(new Debugger.InsertIntoMemoEvent(Debugger.Location.END)));
204+
Debugger.withDebugger(debugger -> debugger.onEvent(Debugger.InsertIntoMemoEvent.end()));
200205
}
201206
}
202207

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/Traversal.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
2626
import com.google.common.base.Verify;
2727
import com.google.common.collect.ImmutableSet;
28-
import com.google.common.collect.Maps;
2928
import com.google.common.collect.Multimaps;
3029
import com.google.common.collect.SetMultimap;
3130
import com.google.common.collect.Sets;
31+
import com.google.common.graph.ElementOrder;
3232
import com.google.common.graph.EndpointPair;
3333
import com.google.common.graph.MutableNetwork;
3434
import com.google.common.graph.NetworkBuilder;
35+
import com.google.common.graph.StableStandardMutableNetwork;
3536

3637
import javax.annotation.Nonnull;
3738
import java.util.Set;
@@ -172,15 +173,19 @@ public void removeExpression(@Nonnull final Reference reference, @Nonnull final
172173
* @return a new traversal object
173174
*/
174175
public static Traversal withRoot(final Reference rootRef) {
176+
// This diverges from the standard construction of Guava network; it delegates the construction to a slightly
177+
// modified version that guarantees a stable iteration order over the edges, which is required to guarantee a
178+
// deterministic planning behavior.
175179
final MutableNetwork<Reference, ReferencePath> network =
176-
NetworkBuilder.directed()
180+
new StableStandardMutableNetwork<>(NetworkBuilder.directed()
177181
.allowsParallelEdges(true)
178182
.allowsSelfLoops(true)
179-
.build();
183+
.edgeOrder(ElementOrder.insertion())
184+
.nodeOrder(ElementOrder.insertion()));
180185

181186
final SetMultimap<RelationalExpression, Reference> containedInMap =
182-
Multimaps.newSetMultimap(Maps.newIdentityHashMap(), Sets::newHashSet);
183-
final Set<Reference> leafRefs = Sets.newHashSet();
187+
Multimaps.newSetMultimap(new LinkedIdentityMap<>(), LinkedIdentitySet::new);
188+
final Set<Reference> leafRefs = new LinkedIdentitySet<>();
184189
collectNetwork(network, containedInMap, leafRefs, rootRef);
185190

186191
return new Traversal(rootRef, network, containedInMap, leafRefs);

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/debug/Debugger.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@
4646
import com.apple.foundationdb.record.query.plan.cascades.debug.eventprotos.PTransformRuleCallEvent;
4747
import com.apple.foundationdb.record.query.plan.cascades.debug.eventprotos.PTranslateCorrelationsEvent;
4848
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
49+
import com.google.common.collect.ImmutableList;
4950
import com.google.errorprone.annotations.CanIgnoreReturnValue;
5051
import com.google.protobuf.Message;
5152

5253
import javax.annotation.Nonnull;
5354
import javax.annotation.Nullable;
55+
import java.util.Collection;
5456
import java.util.Deque;
57+
import java.util.List;
5558
import java.util.Optional;
5659
import java.util.function.Consumer;
5760
import java.util.function.Function;
@@ -919,11 +922,26 @@ public PEvent.Builder toEventBuilder() {
919922
* structures of the planner.
920923
*/
921924
class InsertIntoMemoEvent implements Event {
925+
@Nullable
926+
private final RelationalExpression expression;
927+
922928
@Nonnull
923929
private final Location location;
924930

925-
public InsertIntoMemoEvent(@Nonnull final Location location) {
931+
@Nonnull
932+
private final List<Reference> reusedExpressionReferences;
933+
934+
private InsertIntoMemoEvent(@Nonnull final Location location,
935+
@Nullable final RelationalExpression expression,
936+
@Nonnull final Collection<Reference> reusedExpressionReferences) {
937+
if (expression != null) {
938+
Debugger.registerExpression(expression);
939+
}
940+
this.expression = expression;
926941
this.location = location;
942+
this.reusedExpressionReferences = ImmutableList.copyOf(reusedExpressionReferences);
943+
// Call debugger hook to potentially register this new reference.
944+
this.reusedExpressionReferences.forEach(Debugger::registerReference);
927945
}
928946

929947
@Override
@@ -938,6 +956,16 @@ public Shorthand getShorthand() {
938956
return Shorthand.INSERT_INTO_MEMO;
939957
}
940958

959+
@Nullable
960+
public RelationalExpression getExpression() {
961+
return expression;
962+
}
963+
964+
@Nonnull
965+
public Collection<Reference> getReusedExpressionReferences() {
966+
return reusedExpressionReferences;
967+
}
968+
941969
@Nonnull
942970
@Override
943971
public Location getLocation() {
@@ -947,9 +975,15 @@ public Location getLocation() {
947975
@Nonnull
948976
@Override
949977
public PInsertIntoMemoEvent toProto() {
950-
return PInsertIntoMemoEvent.newBuilder()
978+
final var builder = PInsertIntoMemoEvent.newBuilder()
951979
.setLocation(getLocation().name())
952-
.build();
980+
.addAllReusedExpressionReferences(getReusedExpressionReferences().stream()
981+
.map(Event::toReferenceProto)
982+
.collect(ImmutableList.toImmutableList()));
983+
if (expression != null) {
984+
builder.setExpression(Event.toExpressionProto(expression));
985+
}
986+
return builder.build();
953987
}
954988

955989
@Nonnull
@@ -958,6 +992,32 @@ public PEvent.Builder toEventBuilder() {
958992
return PEvent.newBuilder()
959993
.setInsertIntoMemoEvent(toProto());
960994
}
995+
996+
@Nonnull
997+
public static InsertIntoMemoEvent begin() {
998+
return new InsertIntoMemoEvent(Location.BEGIN, null, ImmutableList.of());
999+
}
1000+
1001+
@Nonnull
1002+
public static InsertIntoMemoEvent end() {
1003+
return new InsertIntoMemoEvent(Location.END, null, ImmutableList.of());
1004+
}
1005+
1006+
@Nonnull
1007+
public static InsertIntoMemoEvent newExp(@Nonnull final RelationalExpression expression) {
1008+
return new InsertIntoMemoEvent(Location.NEW, expression, ImmutableList.of());
1009+
}
1010+
1011+
@Nonnull
1012+
public static InsertIntoMemoEvent reusedExp(@Nonnull final RelationalExpression expression) {
1013+
return new InsertIntoMemoEvent(Location.REUSED, expression, ImmutableList.of());
1014+
}
1015+
1016+
@Nonnull
1017+
public static InsertIntoMemoEvent reusedExpWithReferences(@Nonnull final RelationalExpression expression,
1018+
@Nonnull final List<Reference> references) {
1019+
return new InsertIntoMemoEvent(Location.REUSED, expression, references);
1020+
}
9611021
}
9621022

9631023
/**

0 commit comments

Comments
 (0)