Skip to content

Commit 505a0a2

Browse files
committed
Preserve insertion order in maps that are traversed
Consumers of an `Immutable{List,Map,Set}` cannot know whether it has been created from a source with unspecified iteration order, such as `HashMap`. Since `ImmutableMap` itself prescribes that it preserves insertion order, it's reasonable for consumers to expect a specified, reasonable, deterministic order. While the current implementation of `HashMap` in the OpenJDK still results in a deterministic order, this kind of "sequenced-washing" doesn't cause non-determinism, but it still runs the risk of having the order of elements change between JDK versions or vendors. The locations changed in this PR have been identified by instrumenting the factory methods of the `Immutable{List,Map,Set}` classes to track whether they 1) have been created from an unsequenced collection and 2) are iterated over. I then manually picked out those cases in which I couldn't rule out that a change in order would be observable to users. Related to google/guava#6903 (comment)
1 parent 59f6a2c commit 505a0a2

19 files changed

+71
-55
lines changed

src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
import java.io.OutputStream;
6767
import java.lang.reflect.Constructor;
6868
import java.util.ArrayList;
69-
import java.util.HashMap;
69+
import java.util.LinkedHashMap;
7070
import java.util.List;
7171
import java.util.Map;
7272
import java.util.Optional;
@@ -152,10 +152,10 @@ public static final class Builder implements RuleDefinitionEnvironment {
152152
private final List<Class<? extends Fragment>> configurationFragmentClasses = new ArrayList<>();
153153
private final List<Class<? extends FragmentOptions>> configurationOptions = new ArrayList<>();
154154

155-
private final Map<String, RuleClass> ruleClassMap = new HashMap<>();
156-
private final Map<String, RuleDefinition> ruleDefinitionMap = new HashMap<>();
157-
private final Map<String, NativeAspectClass> nativeAspectClassMap = new HashMap<>();
158-
private final Map<Class<? extends RuleDefinition>, RuleClass> ruleMap = new HashMap<>();
155+
private final Map<String, RuleClass> ruleClassMap = new LinkedHashMap<>();
156+
private final Map<String, RuleDefinition> ruleDefinitionMap = new LinkedHashMap<>();
157+
private final Map<String, NativeAspectClass> nativeAspectClassMap = new LinkedHashMap<>();
158+
private final Map<Class<? extends RuleDefinition>, RuleClass> ruleMap = new LinkedHashMap<>();
159159
private final Digraph<Class<? extends RuleDefinition>> dependencyGraph = new Digraph<>();
160160
private final List<Class<? extends Fragment>> universalFragments = new ArrayList<>();
161161
@Nullable private TransitionFactory<RuleTransitionData> trimmingTransitionFactory = null;

src/main/java/com/google/devtools/build/lib/analysis/ExecGroupCollection.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.common.collect.ImmutableSet;
2525
import com.google.common.collect.ImmutableTable;
2626
import com.google.common.collect.Iterables;
27-
import com.google.common.collect.Maps;
2827
import com.google.common.collect.Table;
2928
import com.google.devtools.build.lib.analysis.config.ToolchainTypeRequirement;
3029
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
@@ -60,7 +59,7 @@ public static ImmutableMap<String, ExecGroup> process(
6059
ImmutableSet<ToolchainTypeRequirement> defaultToolchainTypes,
6160
boolean useAutoExecGroups) {
6261
var processedGroups =
63-
Maps.<String, ExecGroup>newHashMapWithExpectedSize(
62+
ImmutableMap.<String, ExecGroup>builderWithExpectedSize(
6463
useAutoExecGroups
6564
? (execGroups.size() + defaultToolchainTypes.size())
6665
: execGroups.size());
@@ -108,7 +107,7 @@ public static ImmutableMap<String, ExecGroup> process(
108107
.build());
109108
}
110109
}
111-
return ImmutableMap.copyOf(processedGroups);
110+
return processedGroups.buildOrThrow();
112111
}
113112

114113
/** Builder class for correctly constructing ExecGroupCollection instances. */

src/main/java/com/google/devtools/build/lib/analysis/ToolchainCollection.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
package com.google.devtools.build.lib.analysis;
1515

1616
import static com.google.common.collect.ImmutableSet.toImmutableSet;
17-
import static com.google.common.collect.Maps.newHashMapWithExpectedSize;
17+
import static com.google.common.collect.Maps.newLinkedHashMapWithExpectedSize;
1818
import static java.util.Objects.requireNonNull;
1919

2020
import com.google.common.base.Preconditions;
@@ -25,8 +25,8 @@
2525
import com.google.devtools.build.lib.packages.ExecGroup;
2626
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
2727
import com.google.errorprone.annotations.CanIgnoreReturnValue;
28-
import java.util.HashMap;
29-
import java.util.Map;
28+
import java.util.LinkedHashMap;
29+
import java.util.SequencedMap;
3030

3131
/**
3232
* A wrapper class for a map of exec_group names to their relevant ToolchainContext.
@@ -88,14 +88,14 @@ public static <T extends ToolchainContext> Builder<T> builderWithExpectedSize(in
8888
/** Builder for ToolchainCollection. */
8989
public static final class Builder<T extends ToolchainContext> {
9090
// This is not immutable so that we can check for duplicate keys easily.
91-
private final Map<String, T> toolchainContexts;
91+
private final SequencedMap<String, T> toolchainContexts;
9292

9393
private Builder() {
94-
this.toolchainContexts = new HashMap<>();
94+
this.toolchainContexts = new LinkedHashMap<>();
9595
}
9696

9797
private Builder(int expectedSize) {
98-
this.toolchainContexts = newHashMapWithExpectedSize(expectedSize);
98+
this.toolchainContexts = newLinkedHashMapWithExpectedSize(expectedSize);
9999
}
100100

101101
public ToolchainCollection<T> build() {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -600,11 +600,11 @@ public boolean shouldCollectCodeCoverageForGeneratedFiles() {
600600
* <p>Command-line definitions of make environments override variables defined by {@code
601601
* Fragment.addGlobalMakeVariables()}.
602602
*/
603-
public Map<String, String> getMakeEnvironment() {
604-
Map<String, String> makeEnvironment = new HashMap<>();
603+
public ImmutableMap<String, String> getMakeEnvironment() {
604+
ImmutableMap.Builder<String, String> makeEnvironment = ImmutableMap.builder();
605605
makeEnvironment.putAll(globalMakeEnv);
606606
makeEnvironment.putAll(commandLineBuildVariables);
607-
return ImmutableMap.copyOf(makeEnvironment);
607+
return makeEnvironment.build();
608608
}
609609

610610
/**

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
import com.google.devtools.common.options.OptionDefinition;
3030
import com.google.devtools.common.options.OptionsParser;
3131
import java.util.ArrayList;
32-
import java.util.HashMap;
3332
import java.util.HashSet;
3433
import java.util.LinkedHashMap;
3534
import java.util.List;
3635
import java.util.Map;
3736
import java.util.Objects;
37+
import java.util.SequencedMap;
3838
import java.util.Set;
3939

4040
/**
@@ -138,7 +138,7 @@ public static OptionsDiff diff(OptionsDiff diff, BuildOptions first, BuildOption
138138
private final Map<Label, Object> starlarkSecond = new LinkedHashMap<>();
139139

140140
private final List<Label> extraStarlarkOptionsFirst = new ArrayList<>();
141-
private final Map<Label, Object> extraStarlarkOptionsSecond = new HashMap<>();
141+
private final SequencedMap<Label, Object> extraStarlarkOptionsSecond = new LinkedHashMap<>();
142142

143143
private boolean hasStarlarkOptions = false;
144144

src/main/java/com/google/devtools/build/lib/analysis/platform/PlatformProperties.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
import com.google.common.base.Strings;
1818
import com.google.common.collect.ImmutableMap;
1919
import com.google.errorprone.annotations.CanIgnoreReturnValue;
20-
import java.util.HashMap;
20+
import java.util.LinkedHashMap;
2121
import java.util.Map;
22+
import java.util.SequencedMap;
2223
import javax.annotation.Nullable;
2324

2425
/** Proepeties set on a specific {@link PlatformInfo}. */
@@ -70,8 +71,8 @@ private static ImmutableMap<String, String> mergeParent(
7071
return properties;
7172
}
7273

73-
HashMap<String, String> result = new HashMap<>();
74-
if (parent != null && !parent.properties().isEmpty()) {
74+
SequencedMap<String, String> result = new LinkedHashMap<>();
75+
if (!parent.properties().isEmpty()) {
7576
result.putAll(parent.properties());
7677
}
7778

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Discovery.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static Result run(Environment env, RootModuleFileValue root)
6060
throws InterruptedException, ExternalDepsException {
6161
String rootModuleName = root.getModule().getName();
6262
ImmutableMap<String, ModuleOverride> overrides = root.getOverrides();
63-
Map<ModuleKey, InterimModule> depGraph = new HashMap<>();
63+
Map<ModuleKey, InterimModule> depGraph = new LinkedHashMap<>();
6464
depGraph.put(
6565
ModuleKey.ROOT,
6666
root.getModule()

src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileFunction.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@
7070
import com.google.errorprone.annotations.FormatMethod;
7171
import java.io.IOException;
7272
import java.util.ArrayList;
73-
import java.util.HashMap;
73+
import java.util.LinkedHashMap;
7474
import java.util.List;
7575
import java.util.Map;
7676
import java.util.Objects;
77+
import java.util.SequencedMap;
7778
import java.util.stream.Stream;
7879
import javax.annotation.Nullable;
7980
import net.starlark.java.eval.Dict;
@@ -138,7 +139,8 @@ private static class State implements Environment.SkyKeyComputeState {
138139
// the `includeLabelToCompiledModuleFile` map for use during actual Starlark execution.
139140
CompiledModuleFile compiledRootModuleFile;
140141
ImmutableList<IncludeStatement> horizon;
141-
HashMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile = new HashMap<>();
142+
SequencedMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile =
143+
new LinkedHashMap<>();
142144
}
143145

144146
@Nullable
@@ -313,7 +315,7 @@ private SkyValue computeForRootModule(
313315
*/
314316
@Nullable
315317
private static ImmutableList<IncludeStatement> advanceHorizon(
316-
HashMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile,
318+
SequencedMap<String, CompiledModuleFile> includeLabelToCompiledModuleFile,
317319
ImmutableList<IncludeStatement> horizon,
318320
Environment env,
319321
StarlarkSemantics starlarkSemantics,

src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.net.URISyntaxException;
3232
import java.net.URL;
3333
import java.net.URLConnection;
34-
import java.util.HashMap;
3534
import java.util.List;
3635
import java.util.Map;
3736
import java.util.Optional;
@@ -139,17 +138,16 @@ static Function<URL, ImmutableMap<String, List<String>>> getHeaderFunction(
139138
Preconditions.checkNotNull(credentials);
140139

141140
return url -> {
142-
Map<String, List<String>> headers = new HashMap<>(baseHeaders);
143141
try {
144-
headers.putAll(credentials.getRequestMetadata(url.toURI()));
142+
return ImmutableMap.copyOf(credentials.getRequestMetadata(url.toURI()));
145143
} catch (URISyntaxException | IOException e) {
146144
// If we can't convert the URL to a URI (because it is syntactically malformed), or fetching
147145
// credentials fails for any other reason, still try to do the connection, not adding
148146
// authentication information as we cannot look it up.
149147
eventHandler.handle(
150148
Event.warn("Error retrieving auth headers, continuing without: " + e.getMessage()));
149+
return ImmutableMap.of();
151150
}
152-
return ImmutableMap.copyOf(headers);
153151
};
154152
}
155153
}

src/main/java/com/google/devtools/build/lib/cmdline/RepositoryMapping.java

+11-6
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,28 @@ public String toString() {
8888
}
8989

9090
public static RepositoryMapping create(
91-
Map<String, RepositoryName> entries, RepositoryName ownerRepo) {
91+
ImmutableMap<String, RepositoryName> entries, RepositoryName ownerRepo) {
9292
return new RepositoryMapping(
9393
Preconditions.checkNotNull(entries), Preconditions.checkNotNull(ownerRepo));
9494
}
9595

96-
public static RepositoryMapping createAllowingFallback(Map<String, RepositoryName> entries) {
96+
public static RepositoryMapping createAllowingFallback(ImmutableMap<String, RepositoryName> entries) {
9797
return new RepositoryMapping(Preconditions.checkNotNull(entries), null);
9898
}
9999

100100
/**
101101
* Create a new {@link RepositoryMapping} instance based on existing repo mappings and given
102102
* additional mappings. If there are conflicts, existing mappings will take precedence.
103103
*/
104-
public RepositoryMapping withAdditionalMappings(Map<String, RepositoryName> additionalMappings) {
105-
HashMap<String, RepositoryName> allMappings = new HashMap<>(additionalMappings);
106-
allMappings.putAll(entries());
107-
return new RepositoryMapping(allMappings, ownerRepo());
104+
public RepositoryMapping withAdditionalMappings(
105+
ImmutableMap<String, RepositoryName> additionalMappings) {
106+
return new RepositoryMapping(
107+
ImmutableMap.<String, RepositoryName>builderWithExpectedSize(
108+
entries().size() + additionalMappings.size())
109+
.putAll(additionalMappings)
110+
.putAll(entries())
111+
.buildKeepingLast(),
112+
ownerRepo());
108113
}
109114

110115
/**

src/main/java/com/google/devtools/build/lib/exec/TestPolicy.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.devtools.build.lib.vfs.PathFragment;
2020
import java.time.Duration;
2121
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
2223
import java.util.Map;
2324

2425
/**
@@ -64,7 +65,7 @@ public Map<String, String> computeTestEnvironment(
6465
Duration timeout,
6566
PathFragment relativeRunfilesDir,
6667
PathFragment tmpDir) {
67-
Map<String, String> env = new HashMap<>();
68+
Map<String, String> env = new LinkedHashMap<>();
6869

6970
// Add all env variables, allow some string replacements and inheritance.
7071
String userProp = UserUtils.getUserName();

src/main/java/com/google/devtools/build/lib/metrics/PostGCMemoryUseRecorder.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.common.base.Supplier;
1919
import com.google.common.collect.ImmutableList;
2020
import com.google.common.collect.ImmutableMap;
21+
import com.google.common.collect.ImmutableSortedMap;
2122
import com.google.common.flogger.GoogleLogger;
2223
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
2324
import com.google.devtools.build.lib.bugreport.BugReporter;
@@ -128,7 +129,7 @@ public synchronized boolean wasMemoryUsageReportedZero() {
128129
* Returns the number of bytes garbage collected during this invocation. Broken down by GC space.
129130
*/
130131
public synchronized ImmutableMap<String, Long> getGarbageStats() {
131-
return ImmutableMap.copyOf(garbageStats);
132+
return ImmutableSortedMap.copyOf(garbageStats);
132133
}
133134

134135
public synchronized void reset() {

src/main/java/com/google/devtools/build/lib/packages/BazelStarlarkEnvironment.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import com.google.common.collect.ImmutableMap;
1919
import com.google.common.collect.Sets;
2020
import java.util.HashMap;
21+
import java.util.LinkedHashMap;
2122
import java.util.List;
2223
import java.util.Map;
24+
import java.util.SequencedMap;
2325
import java.util.Set;
2426
import javax.annotation.Nullable;
2527
import net.starlark.java.eval.GuardedValue;
@@ -458,11 +460,11 @@ private ImmutableMap<String, Object> createBzlEnvUsingInjection(
458460
throws InjectionException {
459461
Map<String, Boolean> overridesMap = parseInjectionOverridesList(overridesList);
460462

461-
Map<String, Object> env = new HashMap<>(bzlToplevelsWithoutNative);
463+
SequencedMap<String, Object> env = new LinkedHashMap<>(bzlToplevelsWithoutNative);
462464

463465
// Determine "native" bindings.
464466
// TODO(#11954): See above comment in createUninjectedBuildBzlEnv.
465-
Map<String, Object> nativeBindings = new HashMap<>(nativeBase);
467+
SequencedMap<String, Object> nativeBindings = new LinkedHashMap<>(nativeBase);
466468
for (Map.Entry<String, Object> entry : exportedRules.entrySet()) {
467469
String key = entry.getKey();
468470
String name = getKeySuffix(key);
@@ -508,7 +510,7 @@ public ImmutableMap<String, Object> createBuildEnvUsingInjection(
508510
Map<String, Object> exportedRules, List<String> overridesList) throws InjectionException {
509511
Map<String, Boolean> overridesMap = parseInjectionOverridesList(overridesList);
510512

511-
HashMap<String, Object> env = new HashMap<>(uninjectedBuildEnv);
513+
SequencedMap<String, Object> env = new LinkedHashMap<>(uninjectedBuildEnv);
512514
for (Map.Entry<String, Object> entry : exportedRules.entrySet()) {
513515
String key = entry.getKey();
514516
String name = getKeySuffix(key);

src/main/java/com/google/devtools/build/lib/packages/Package.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@
6161
import java.util.ArrayList;
6262
import java.util.HashMap;
6363
import java.util.HashSet;
64+
import java.util.LinkedHashMap;
6465
import java.util.LinkedHashSet;
6566
import java.util.List;
6667
import java.util.Map;
6768
import java.util.Optional;
6869
import java.util.OptionalInt;
6970
import java.util.OptionalLong;
71+
import java.util.SequencedMap;
7072
import java.util.Set;
7173
import java.util.concurrent.Semaphore;
7274
import javax.annotation.Nullable;
@@ -1107,7 +1109,7 @@ default boolean precomputeTransitiveLoads() {
11071109

11081110
// The map from each repository to that repository's remappings map.
11091111
// This is only used in the //external package, it is an empty map for all other packages.
1110-
private final HashMap<RepositoryName, HashMap<String, RepositoryName>>
1112+
private final HashMap<RepositoryName, LinkedHashMap<String, RepositoryName>>
11111113
externalPackageRepositoryMappings = new HashMap<>();
11121114

11131115
/** Estimates the package overhead of this package. */
@@ -1357,7 +1359,7 @@ Builder addRepositoryMappingEntry(
13571359
RepositoryName repoWithin, String localName, RepositoryName mappedName) {
13581360
HashMap<String, RepositoryName> mapping =
13591361
externalPackageRepositoryMappings.computeIfAbsent(
1360-
repoWithin, (RepositoryName k) -> new HashMap<>());
1362+
repoWithin, (RepositoryName k) -> new LinkedHashMap<>());
13611363
mapping.put(localName, mappedName);
13621364
return this;
13631365
}
@@ -1387,11 +1389,11 @@ Builder addRepositoryMappings(Package aPackage) {
13871389
* the main workspace to the canonical main name '@').
13881390
*/
13891391
RepositoryMapping getRepositoryMappingFor(RepositoryName name) {
1390-
Map<String, RepositoryName> mapping = externalPackageRepositoryMappings.get(name);
1392+
SequencedMap<String, RepositoryName> mapping = externalPackageRepositoryMappings.get(name);
13911393
if (mapping == null) {
13921394
return RepositoryMapping.ALWAYS_FALLBACK;
13931395
} else {
1394-
return RepositoryMapping.createAllowingFallback(mapping);
1396+
return RepositoryMapping.createAllowingFallback(ImmutableMap.copyOf(mapping));
13951397
}
13961398
}
13971399

src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.ArrayList;
4141
import java.util.Collection;
4242
import java.util.HashMap;
43+
import java.util.LinkedHashMap;
4344
import java.util.LinkedHashSet;
4445
import java.util.List;
4546
import java.util.Map;
@@ -110,13 +111,13 @@ public abstract static class Builder {
110111
private final Map<String, String> scopes = new TreeMap<>();
111112

112113
// Map of parsed starlark options to their loaded BuildSetting objects (used for canonicalization)
113-
private final Map<String, BuildSetting> parsedBuildSettings = new HashMap<>();
114+
private final Map<String, BuildSetting> parsedBuildSettings = new LinkedHashMap<>();
114115

115116
// Local cache of build settings so we don't repeatedly load them.
116117
private final Map<String, Target> buildSettings = new HashMap<>();
117118

118119
// The default value for each build setting.
119-
private final Map<String, Object> buildSettingDefaults = new HashMap<>();
120+
private final Map<String, Object> buildSettingDefaults = new LinkedHashMap<>();
120121

121122
// whether options explicitly set to their default values are added to {@code starlarkOptions}
122123
private final boolean includeDefaultValues;

0 commit comments

Comments
 (0)