Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-23 - [Parallel Stream Safety]
**Learning:** Switching to `parallelStream` requires careful audit of all shared mutable state, even in test helpers. I learned that `GraphMemoizator` was using a non-thread-safe `HashMap`, and `SourcesLocatorTest`'s `FakeOpener` also used `HashMap`.
**Action:** Always check thread-safety of underlying data structures when introducing parallel streams, including abstract classes and test doubles.
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package net.evendanan.bazel.mvn.merger;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;

public abstract class GraphMemoizator<T> {
private final Map<String, T> cache = new HashMap<>();
private final Map<String, T> cache = new ConcurrentHashMap<>();

@Nonnull
protected abstract T calculate(@Nonnull T original);

@Nonnull
public T map(@Nonnull T original) {
final String key = getKeyForObject(original);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
final T revised = calculate(original);
cache.put(key, revised);
return revised;
}
return cache.computeIfAbsent(key, k -> calculate(original));
}

protected abstract String getKeyForObject(final T object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import net.evendanan.bazel.mvn.api.DependencyTools;
Expand All @@ -18,7 +18,7 @@ public class SourcesJarLocator {
private static final String SOURCES_CLASSIFIER = "sources";

private final ConnectionFactory mConnectionFactory;
private final Map<String, String> mURLCache = new HashMap<>();
private final Map<String, String> mURLCache = new ConcurrentHashMap<>();

public SourcesJarLocator() {
this(url -> (HttpURLConnection) url.openConnection());
Expand All @@ -31,7 +31,7 @@ public SourcesJarLocator() {

private static Collection<Dependency> fillSourcesAttribute(
Collection<Dependency> dependencies, DependencyMemoizator memoizator) {
return dependencies.stream().map(memoizator::map).collect(Collectors.toList());
return dependencies.parallelStream().map(memoizator::map).collect(Collectors.toList());
}

public Collection<Dependency> fillSourcesAttribute(Collection<Dependency> dependencies) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ public void testOnlyQueriesURIOnce() throws Exception {
private static class FakeOpener
implements net.evendanan.bazel.mvn.merger.SourcesJarLocator.ConnectionFactory {

private final Map<URL, Integer> buildsCounter = new HashMap<>();
private final Map<URL, HttpURLConnection> returnedConnections = new HashMap<>();
private final Map<URL, Integer> buildsCounter = new java.util.concurrent.ConcurrentHashMap<>();
private final Map<URL, HttpURLConnection> returnedConnections =
new java.util.concurrent.ConcurrentHashMap<>();

private boolean openFailure = false;

Expand Down