Skip to content

Commit

Permalink
Address TSAN errors:
Browse files Browse the repository at this point in the history
1. Suppress safe racy init in AbstractMultiset and HashBiMap. All the classes that are stored in the newly annotated fields have only final fields (except HashBiMap.Inverse, which inherits AbstractBiMap's keySet and values fields, which are themselves @lazyinit), so they are safe to read racily.

2. Mark some graph "cache" fields as volatile. These fields are an optimization: When the user accesses an entry through iteration (and sometimes through get(...)), the graph stores the entry in a field. That way, if the user then queries it again (such as to look up the value associated with a key during iteration), the graph doesn't have to perform a potentially mildly expensive lookup. But this caching isn't implemented in a thread-safe way. It *ought* to be safe to initialize a graph in one thread, safely publish it, and then read it concurrently from other threads. But because of the racy reads of the cached entry fields, I don't think this is guaranteed to be safe.

RELNOTES=`graph`: Fixed data race.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=273585560
  • Loading branch information
cpovirk authored and nick-someone committed Oct 9, 2019
1 parent 491fe70 commit 0e94fb5
Show file tree
Hide file tree
Showing 14 changed files with 382 additions and 30 deletions.
Expand Up @@ -22,14 +22,20 @@
import static com.google.common.graph.TestUtil.assertStronglyEquivalent;
import static com.google.common.graph.TestUtil.sanityCheckSet;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -678,4 +684,64 @@ public void removeEdge_queryAfterRemoval() {
assertEdgeNotInGraphErrorMessage(e);
}
}

@Test
public void concurrentIteration() throws Exception {
addEdge(1, 2, "foo");
addEdge(3, 4, "bar");
addEdge(5, 6, "baz");

int threadCount = 20;
ExecutorService executor = newFixedThreadPool(threadCount);
final CyclicBarrier barrier = new CyclicBarrier(threadCount);
ImmutableList.Builder<Future<?>> futures = ImmutableList.builder();
for (int i = 0; i < threadCount; i++) {
futures.add(
executor.submit(
new Callable<Object>() {
@Override
public Object call() throws Exception {
barrier.await();
Integer first = network.nodes().iterator().next();
for (Integer node : network.nodes()) {
Set<Integer> unused = network.successors(node);
}
/*
* Also look up an earlier node so that, if the graph is using MapRetrievalCache,
* we read one of the fields declared in that class.
*/
Set<Integer> unused = network.successors(first);
return null;
}
}));
}

/*
* It's unlikely that any operations would fail by throwing an exception, but let's check them
* just to be safe.
*
* The real purpose of this test is to produce a TSAN failure if MapIteratorCache is unsafe for
* reads from multiple threads -- unsafe, in fact, even in the absence of a concurrent write.
* The specific problem we had was unsafe reads of lastEntryReturnedBySomeIterator. (To fix the
* problem, we've since marked that field as volatile.)
*
* When MapIteratorCache is used from Immutable* classes, the TSAN failure doesn't indicate a
* real problem: The Entry objects are ImmutableMap entries, whose fields are all final and thus
* safe to read even when the Entry object is unsafely published. But with a mutable graph, the
* Entry object is likely to have a non-final value field, which is not safe to read when
* unsafely published. (The Entry object might even be newly created by each iterator.next()
* call, so we can't assume that writes to the Entry have been safely published by some other
* synchronization actions.)
*
* All that said: I haven't actually managed to make this particular test produce a TSAN error
* for the field accesses in MapIteratorCache. This teset *has* found other TSAN errors,
* including in MapRetrievalCache, so I'm not sure why this one is different. I did at least
* confirm that my change to MapIteratorCache fixes the TSAN error in the (larger) test it was
* originally reported in.
*/
for (Future<?> future : futures.build()) {
future.get();
}
executor.shutdown();
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2014 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.common.graph;

import static com.google.common.graph.ElementOrder.sorted;

import com.google.common.collect.Ordering;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
* Tests for a directed {@link ConfigurableMutableNetwork}, creating a simple directed sorted graph
* (parallel and self-loop edges are not allowed).
*
* <p>The main purpose of this class is to run the inherited {@link #concurrentIteration} test
* against a sorted graph so as to cover {@link MapRetrievalCache}.
*/
@RunWith(JUnit4.class)
public class ConfigurableSimpleDirectedSortedNetworkTest
extends ConfigurableSimpleDirectedNetworkTest {

@Override
public MutableNetwork<Integer, String> createGraph() {
return NetworkBuilder.directed()
.allowsParallelEdges(false)
.allowsSelfLoops(false)
.edgeOrder(sorted(Ordering.natural()))
.nodeOrder(sorted(Ordering.natural()))
.build();
}

@Override
public void addEdge_nodesNotInGraph() {
/*
* Skip this test because the expected ordering is different here than in the superclass because
* of sorting.
*
* TODO(cpovirk): Implement this to check for the proper order.
*/
}
}
Expand Up @@ -19,8 +19,15 @@
import static com.google.common.graph.GraphConstants.ENDPOINTS_MISMATCH;
import static com.google.common.graph.TestUtil.assertStronglyEquivalent;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -330,4 +337,43 @@ public void equivalence_considersEdgeValue() {
otherGraph.putEdgeValue(1, 2, "valueB");
assertThat(graph).isNotEqualTo(otherGraph); // values differ
}

@Test
public void concurrentIteration() throws Exception {
graph = ValueGraphBuilder.directed().build();
graph.putEdgeValue(1, 2, "A");
graph.putEdgeValue(3, 4, "B");
graph.putEdgeValue(5, 6, "C");

int threadCount = 20;
ExecutorService executor = newFixedThreadPool(threadCount);
final CyclicBarrier barrier = new CyclicBarrier(threadCount);
ImmutableList.Builder<Future<?>> futures = ImmutableList.builder();
for (int i = 0; i < threadCount; i++) {
futures.add(
executor.submit(
new Callable<Object>() {
@Override
public Object call() throws Exception {
barrier.await();
Integer first = graph.nodes().iterator().next();
for (Integer node : graph.nodes()) {
Set<Integer> unused = graph.successors(node);
}
/*
* Also look up an earlier node so that, if the graph is using MapRetrievalCache,
* we read one of the fields declared in that class.
*/
Set<Integer> unused = graph.successors(first);
return null;
}
}));
}

// For more about this test, see the equivalent in AbstractNetworkTest.
for (Future<?> future : futures.build()) {
future.get();
}
executor.shutdown();
}
}
Expand Up @@ -20,6 +20,7 @@

import com.google.common.annotations.GwtCompatible;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import com.google.j2objc.annotations.WeakOuter;
import java.util.AbstractCollection;
import java.util.Collection;
Expand Down Expand Up @@ -124,7 +125,7 @@ public final boolean retainAll(Collection<?> elementsToRetain) {

// Views

@MonotonicNonNullDecl private transient Set<E> elementSet;
@LazyInit @MonotonicNonNullDecl private transient Set<E> elementSet;

@Override
public Set<E> elementSet() {
Expand Down Expand Up @@ -158,7 +159,7 @@ public Iterator<E> iterator() {

abstract Iterator<E> elementIterator();

@MonotonicNonNullDecl private transient Set<Entry<E>> entrySet;
@LazyInit @MonotonicNonNullDecl private transient Set<Entry<E>> entrySet;

@Override
public Set<Entry<E>> entrySet() {
Expand Down
3 changes: 2 additions & 1 deletion android/guava/src/com/google/common/collect/HashBiMap.java
Expand Up @@ -20,6 +20,7 @@
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import com.google.j2objc.annotations.RetainedWith;
import java.io.IOException;
import java.io.ObjectInputStream;
Expand Down Expand Up @@ -899,7 +900,7 @@ public V setValue(V value) {
}
}

@MonotonicNonNullDecl @RetainedWith private transient BiMap<V, K> inverse;
@LazyInit @MonotonicNonNullDecl @RetainedWith private transient BiMap<V, K> inverse;

@Override
public BiMap<V, K> inverse() {
Expand Down
26 changes: 16 additions & 10 deletions android/guava/src/com/google/common/graph/MapIteratorCache.java
Expand Up @@ -44,28 +44,34 @@
class MapIteratorCache<K, V> {
private final Map<K, V> backingMap;

// Per JDK: "the behavior of a map entry is undefined if the backing map has been modified after
// the entry was returned by the iterator, except through the setValue operation on the map entry"
// As such, this field must be cleared before every map mutation.
@NullableDecl private transient Entry<K, V> entrySetCache;
/*
* Per JDK: "the behavior of a map entry is undefined if the backing map has been modified after
* the entry was returned by the iterator, except through the setValue operation on the map entry"
* As such, this field must be cleared before every map mutation.
*
* Note about volatile: volatile doesn't make it safe to read from a mutable graph in one thread
* while writing to it in another. All it does is help with _reading_ from multiple threads
* concurrently. For more information, see AbstractNetworkTest.concurrentIteration.
*/
@NullableDecl private transient volatile Entry<K, V> cacheEntry;

MapIteratorCache(Map<K, V> backingMap) {
this.backingMap = checkNotNull(backingMap);
}

@CanIgnoreReturnValue
public V put(@NullableDecl K key, @NullableDecl V value) {
public final V put(@NullableDecl K key, @NullableDecl V value) {
clearCache();
return backingMap.put(key, value);
}

@CanIgnoreReturnValue
public V remove(@NullableDecl Object key) {
public final V remove(@NullableDecl Object key) {
clearCache();
return backingMap.remove(key);
}

public void clear() {
public final void clear() {
clearCache();
backingMap.clear();
}
Expand Down Expand Up @@ -98,7 +104,7 @@ public boolean hasNext() {
@Override
public K next() {
Entry<K, V> entry = entryIterator.next(); // store local reference for thread-safety
entrySetCache = entry;
cacheEntry = entry;
return entry.getKey();
}
};
Expand All @@ -119,7 +125,7 @@ public boolean contains(@NullableDecl Object key) {
// Internal methods ('protected' is still package-visible, but treat as only subclass-visible)

protected V getIfCached(@NullableDecl Object key) {
Entry<K, V> entry = entrySetCache; // store local reference for thread-safety
Entry<K, V> entry = cacheEntry; // store local reference for thread-safety

// Check cache. We use == on purpose because it's cheaper and a cache miss is ok.
if (entry != null && entry.getKey() == key) {
Expand All @@ -129,6 +135,6 @@ protected V getIfCached(@NullableDecl Object key) {
}

protected void clearCache() {
entrySetCache = null;
cacheEntry = null;
}
}
Expand Up @@ -26,8 +26,9 @@
* @author James Sexton
*/
class MapRetrievalCache<K, V> extends MapIteratorCache<K, V> {
@NullableDecl private transient CacheEntry<K, V> cacheEntry1;
@NullableDecl private transient CacheEntry<K, V> cacheEntry2;
// See the note about volatile in the superclass.
@NullableDecl private transient volatile CacheEntry<K, V> cacheEntry1;
@NullableDecl private transient volatile CacheEntry<K, V> cacheEntry2;

MapRetrievalCache(Map<K, V> backingMap) {
super(backingMap);
Expand Down

0 comments on commit 0e94fb5

Please sign in to comment.