Skip to content

Commit d204db7

Browse files
justinhorvitzcopybara-github
authored andcommitted
Improve checksum computation for starlark options.
This is important now that there is such a thing as `config.string_set`. * A `set` should result in a different checksum than a `list` because the difference is observable to starlark code. Uses `Starlark#type` in the checksum computation. * `set([])` and `set([""])` should result in different checksums. This fixes a nondeterminism bug whereby `BuildOptions` with the same checksum but different collection types can be incorrectly cached across invocations. PiperOrigin-RevId: 863645964 Change-Id: I257f7a723ce33751141708417c888ef6c08752d0
1 parent e2ac765 commit d204db7

3 files changed

Lines changed: 80 additions & 13 deletions

File tree

src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ public String checksum() {
188188
for (FragmentOptions options : fragmentOptionsMap.values()) {
189189
fingerprint.addString(options.cacheKey());
190190
}
191-
fingerprint.addString(OptionsBase.mapToCacheKey(starlarkOptionsMap));
191+
fingerprint.addString(OptionsBase.starlarkMapToCacheKey(starlarkOptionsMap));
192192
fingerprint.addString(OptionsBase.mapToCacheKey(scopes));
193-
fingerprint.addString(OptionsBase.mapToCacheKey(onLeaveScopeValuesMap));
193+
fingerprint.addString(OptionsBase.starlarkMapToCacheKey(onLeaveScopeValuesMap));
194194
checksum = fingerprint.hexDigestAndReset();
195195
}
196196
}

src/main/java/com/google/devtools/common/options/OptionsBase.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import com.google.common.collect.Maps;
1818
import com.google.common.escape.CharEscaperBuilder;
1919
import com.google.common.escape.Escaper;
20+
import java.util.Collection;
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Objects;
24+
import net.starlark.java.eval.Starlark;
2325

2426
/**
2527
* Base class for all options classes. Extend this class, adding public instance fields annotated
@@ -84,32 +86,49 @@ public final String toString() {
8486
}
8587

8688
/**
87-
* Returns a string that uniquely identifies the options. This value is
88-
* intended for analysis caching.
89+
* Returns a string that uniquely identifies the options. This value is intended for analysis
90+
* caching.
8991
*/
9092
public final String cacheKey() {
9193
StringBuilder result = new StringBuilder(getClass().getName()).append("{");
9294
result.append(mapToCacheKey(asMap()));
9395
return result.append("}").toString();
9496
}
9597

98+
/**
99+
* Like {@link #mapToCacheKey} but the returned key is sensitive to the {@link Starlark#type} of
100+
* values in the map.
101+
*
102+
* <p>This is important because types are observable to starlark code. See b/478938163.
103+
*/
104+
public static String starlarkMapToCacheKey(Map<?, ?> starlarkOptionsMap) {
105+
return mapToCacheKey(starlarkOptionsMap, /* distinguishStarlarkTypes= */ true);
106+
}
107+
96108
public static String mapToCacheKey(Map<?, ?> optionsMap) {
109+
return mapToCacheKey(optionsMap, /* distinguishStarlarkTypes= */ false);
110+
}
111+
112+
private static String mapToCacheKey(Map<?, ?> optionsMap, boolean distinguishStarlarkTypes) {
97113
StringBuilder result = new StringBuilder();
98114
for (Map.Entry<?, ?> entry : optionsMap.entrySet()) {
99115
result.append(entry.getKey()).append("=");
100116

101117
Object value = entry.getValue();
102-
// This special case is needed because List.toString() prints the same
103-
// ("[]") for an empty list and for a list with a single empty string.
104-
if (value instanceof List<?> && ((List<?>) value).isEmpty()) {
105-
result.append("EMPTY");
106-
} else if (value == null) {
118+
119+
if (value == null) {
107120
result.append("NULL");
108121
} else {
109-
result
110-
.append('"')
111-
.append(ESCAPER.escape(value.toString()))
112-
.append('"');
122+
if (distinguishStarlarkTypes) {
123+
result.append(Starlark.type(value));
124+
}
125+
// This special case is needed because Collection.toString() prints the same ("[]") for an
126+
// empty collection and for a collection with a single empty string.
127+
if (value instanceof Collection<?> c && c.isEmpty()) {
128+
result.append("EMPTY");
129+
} else {
130+
result.append('"').append(ESCAPER.escape(value.toString())).append('"');
131+
}
113132
}
114133
result.append(", ");
115134
}

src/test/java/com/google/devtools/build/lib/analysis/config/BuildOptionsTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.common.collect.ImmutableClassToInstanceMap;
2020
import com.google.common.collect.ImmutableList;
2121
import com.google.common.collect.ImmutableMap;
22+
import com.google.common.collect.Lists;
2223
import com.google.devtools.build.lib.analysis.config.BuildOptions.MapBackedChecksumCache;
2324
import com.google.devtools.build.lib.analysis.config.BuildOptions.OptionsChecksumCache;
2425
import com.google.devtools.build.lib.cmdline.Label;
@@ -34,6 +35,8 @@
3435
import com.google.protobuf.ByteString;
3536
import com.google.testing.junit.testparameterinjector.TestParameter;
3637
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
38+
import java.util.ArrayList;
39+
import java.util.LinkedHashSet;
3740
import java.util.List;
3841
import org.junit.Test;
3942
import org.junit.runner.RunWith;
@@ -407,5 +410,50 @@ public void starlarkOptionsOrderedByLabel() {
407410
assertThat(backward.getStarlarkOptions())
408411
.containsExactly(label1, true, label2, false)
409412
.inOrder();
413+
assertThat(backward).isEqualTo(forward);
414+
assertThat(backward.checksum()).isEqualTo(forward.checksum());
415+
}
416+
417+
@Test
418+
public void listAndSetAreDifferent() {
419+
Label label = Label.parseCanonicalUnchecked("//pkg:option");
420+
421+
BuildOptions optionsWithList =
422+
BuildOptions.builder().addStarlarkOption(label, Lists.newArrayList("a")).build();
423+
BuildOptions optionsWithSet =
424+
BuildOptions.builder()
425+
.addStarlarkOption(label, new LinkedHashSet<>(ImmutableList.of("a")))
426+
.build();
427+
428+
assertThat(optionsWithList).isNotEqualTo(optionsWithSet);
429+
assertThat(optionsWithList.checksum()).isNotEqualTo(optionsWithSet.checksum());
430+
}
431+
432+
@Test
433+
public void emptyListDifferentFromListWithEmptyString() {
434+
Label label = Label.parseCanonicalUnchecked("//pkg:option");
435+
436+
BuildOptions emptyListOptions =
437+
BuildOptions.builder().addStarlarkOption(label, new ArrayList<>()).build();
438+
BuildOptions emptyStringListOptions =
439+
BuildOptions.builder().addStarlarkOption(label, Lists.newArrayList("")).build();
440+
441+
assertThat(emptyListOptions).isNotEqualTo(emptyStringListOptions);
442+
assertThat(emptyListOptions.checksum()).isNotEqualTo(emptyStringListOptions.checksum());
443+
}
444+
445+
@Test
446+
public void emptySetDifferentFromSetWithEmptyString() {
447+
Label label = Label.parseCanonicalUnchecked("//pkg:option");
448+
449+
BuildOptions emptySetOptions =
450+
BuildOptions.builder().addStarlarkOption(label, new LinkedHashSet<>()).build();
451+
BuildOptions emptyStringSetOptions =
452+
BuildOptions.builder()
453+
.addStarlarkOption(label, new LinkedHashSet<>(ImmutableList.of("")))
454+
.build();
455+
456+
assertThat(emptySetOptions).isNotEqualTo(emptyStringSetOptions);
457+
assertThat(emptySetOptions.checksum()).isNotEqualTo(emptyStringSetOptions.checksum());
410458
}
411459
}

0 commit comments

Comments
 (0)