Remove notion of stack from thread-local binding, use simple swapping instead.

This allows for binding state to be reset to known-good states precisely which in turn
facilitates making it safe to have 'detach' not throw exceptions and instead log
a severe error that attach/detach calls were not correctly balanced.
This commit is contained in:
Louis Ryan 2015-11-06 17:01:26 -08:00
parent d2046b637d
commit 49b513504c
2 changed files with 125 additions and 139 deletions

View File

@ -39,7 +39,6 @@ import io.grpc.internal.SharedResourceHolder;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -88,7 +87,7 @@ import javax.annotation.Nullable;
* <pre> * <pre>
* CancellableContext withCancellation = Context.current().withCancellation(); * CancellableContext withCancellation = Context.current().withCancellation();
* try { * try {
* executorService.execute(withCancellation.wrap(new Runnable() { * executorService.execute(withCancellation.wrap(new Runnable() {
* public void run() { * public void run() {
* while (waitingForData() &amp;&amp; !Context.current().isCancelled()) {} * while (waitingForData() &amp;&amp; !Context.current().isCancelled()) {}
* } * }
@ -142,17 +141,6 @@ public class Context {
} }
}; };
/**
* Stack of context objects which is used to record attach & detach history on a thread.
*/
private static final ThreadLocal<ArrayDeque<Context>> contextStack =
new ThreadLocal<ArrayDeque<Context>>() {
@Override
protected ArrayDeque<Context> initialValue() {
return new ArrayDeque<Context>();
}
};
private static final Object[][] EMPTY_ENTRIES = new Object[0][2]; private static final Object[][] EMPTY_ENTRIES = new Object[0][2];
/** /**
@ -161,6 +149,16 @@ public class Context {
*/ */
public static final Context ROOT = new Context(null); public static final Context ROOT = new Context(null);
/**
* Currently bound context.
*/
private static final ThreadLocal<Context> localContext = new ThreadLocal<Context>() {
@Override
protected Context initialValue() {
return ROOT;
}
};
/** /**
* Create a {@link Key} with the given name. * Create a {@link Key} with the given name.
*/ */
@ -184,11 +182,11 @@ public class Context {
* code stealing the ability to cancel arbitrarily. * code stealing the ability to cancel arbitrarily.
*/ */
public static Context current() { public static Context current() {
ArrayDeque<Context> stack = contextStack.get(); Context current = localContext.get();
if (stack.isEmpty()) { if (current == null) {
return ROOT; return ROOT;
} }
return stack.peekLast(); return current;
} }
private final Context parent; private final Context parent;
@ -229,8 +227,8 @@ public class Context {
/** /**
* Create a new context which is independently cancellable and also cascades cancellation from * Create a new context which is independently cancellable and also cascades cancellation from
* its parent. Callers should ensure that either {@link CancellableContext#cancel(Throwable)} * its parent. Callers should ensure that either {@link CancellableContext#cancel(Throwable)}
* or {@link CancellableContext#detachAndCancel(Throwable)} are called to notify listeners and * or {@link CancellableContext#detachAndCancel(Context, Throwable)} are called to notify
* release the resources associated with them. * listeners and release the resources associated with them.
* *
* <p>Sample usage: * <p>Sample usage:
* <pre> * <pre>
@ -343,11 +341,30 @@ public class Context {
/** /**
* Attach this context to the thread and make it {@link #current}, the previously current context * Attach this context to the thread and make it {@link #current}, the previously current context
* will be restored when detach is called. It is allowed to attach contexts where * is returned. It is allowed to attach contexts where {@link #isCancelled()} is {@code true}.
* {@link #isCancelled()} is {@code true}.
*/ */
public void attach() { public Context attach() {
contextStack.get().addLast(this); Context previous = current();
localContext.set(this);
return previous;
}
/**
* Detach the current context from the thread and attach the provided replacement. If this
* context is not {@link #current()} a SEVERE message will be logged but the context to attach
* will still be bound.
*/
public void detach(Context toAttach) {
Preconditions.checkNotNull(toAttach);
Context previous = current();
if (previous != this) {
// Log a severe message instead of throwing an exception as the context to attach is assumed
// to be the correct one and the unbalanced state represents a coding mistake in a lower
// layer in the stack that cannot be recovered from here.
LOG.log(Level.SEVERE, "Context was not attached when detaching",
new Throwable().fillInStackTrace());
}
localContext.set(toAttach);
} }
// Visible for testing // Visible for testing
@ -355,27 +372,6 @@ public class Context {
return current() == this; return current() == this;
} }
/**
* Detach the current context from the thread and restore the context that was previously
* attached to the thread as the 'current' context.
*
* @throws java.lang.IllegalStateException if this context is not {@link #current()}.
*/
public void detach() {
ArrayDeque<Context> stack = contextStack.get();
if (stack.isEmpty()) {
if (this == ROOT) {
throw new IllegalStateException("Cannot detach root");
} else {
throw new IllegalStateException("Cannot detach non-root context when root is current");
}
}
if (stack.peekLast() != this) {
throw new IllegalStateException("Cannot detach a context that is not current");
}
stack.removeLast();
}
/** /**
* Is this context cancelled. * Is this context cancelled.
*/ */
@ -492,11 +488,11 @@ public class Context {
return new Runnable() { return new Runnable() {
@Override @Override
public void run() { public void run() {
attach(); Context previous = attach();
try { try {
r.run(); r.run();
} finally { } finally {
detach(); detach(previous);
} }
} }
}; };
@ -509,11 +505,11 @@ public class Context {
return new Callable<C>() { return new Callable<C>() {
@Override @Override
public C call() throws Exception { public C call() throws Exception {
attach(); Context previous = attach();
try { try {
return c.call(); return c.call();
} finally { } finally {
detach(); detach(previous);
} }
} }
}; };
@ -586,7 +582,7 @@ public class Context {
*/ */
private CancellableContext(Context parent) { private CancellableContext(Context parent) {
super(parent, EMPTY_ENTRIES); super(parent, EMPTY_ENTRIES);
// Create a dummy that inherits from this to attach and detach so that you cannot retrieve a // Create a dummy that inherits from this to attach so that you cannot retrieve a
// cancellable context from Context.current() // cancellable context from Context.current()
dummy = new Context(this, EMPTY_ENTRIES); dummy = new Context(this, EMPTY_ENTRIES);
} }
@ -611,13 +607,13 @@ public class Context {
@Override @Override
public void attach() { public Context attach() {
dummy.attach(); return dummy.attach();
} }
@Override @Override
public void detach() { public void detach(Context toAttach) {
dummy.detach(); dummy.detach(toAttach);
} }
@Override @Override
@ -627,17 +623,17 @@ public class Context {
/** /**
* Attach this context to the thread and return a {@link AutoCloseable} that can be * Attach this context to the thread and return a {@link AutoCloseable} that can be
* used with try-with-resource statements to properly {@link #detach} and {@link #cancel} * used with try-with-resource statements to properly attach the previously bound context
* the context on completion. * when {@link AutoCloseable#close()} is called.
* *
* @return a {@link java.io.Closeable} which can be used with try-with-resource blocks. * @return a {@link java.io.Closeable} which can be used with try-with-resource blocks.
*/ */
public Closeable attachAsCloseable() { public Closeable attachAsCloseable() {
attach(); final Context previous = attach();
return new Closeable() { return new Closeable() {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
detachAndCancel(null); detachAndCancel(previous, null);
} }
}; };
} }
@ -670,14 +666,14 @@ public class Context {
} }
/** /**
* Cancel this context and detach it from the current context from the thread and restore the * Cancel this context and detach it as the current context from the thread.
* context that was previously attached to the thread as the 'current' context.
* *
* @throws java.lang.IllegalStateException if this context is not {@link #current()}. * @param toAttach context to make current.
* @param cause of cancellation, can be {@code null}.
*/ */
public void detachAndCancel(@Nullable Throwable cause) { public void detachAndCancel(Context toAttach, @Nullable Throwable cause) {
try { try {
detach(); detach(toAttach);
} finally { } finally {
cancel(cause); cancel(cause);
} }

View File

@ -41,6 +41,7 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import io.grpc.internal.SharedResourceHolder; import io.grpc.internal.SharedResourceHolder;
@ -63,6 +64,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Handler; import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord; import java.util.logging.LogRecord;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -97,10 +99,7 @@ public class ContextTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
// Detach all contexts back to the root. Context.ROOT.attach();
while (Context.current() != Context.ROOT) {
Context.current().detach();
}
} }
@After @After
@ -114,29 +113,25 @@ public class ContextTest {
} }
@Test @Test
public void rootIsAlwaysBound() { public void rootIsAlwaysBound() throws Exception {
Context root = Context.current(); final SettableFuture<Boolean> rootIsBound = SettableFuture.create();
try { new Thread(new Runnable() {
root.detach(); @Override
} catch (IllegalStateException ise) { public void run() {
// Expected rootIsBound.set(Context.current() == Context.ROOT);
assertTrue(Context.ROOT.isCurrent()); }
return; }).start();
} assertTrue(rootIsBound.get(5, TimeUnit.SECONDS));
fail("Attempting to detach root should fail");
} }
@Test @Test
public void rootCanBeAttached() { public void rootCanBeAttached() {
Context root = Context.current(); Context.CancellableContext fork = Context.ROOT.fork();
Context.CancellableContext fork = root.fork(); fork.attach();
Context.ROOT.attach();
assertTrue(Context.ROOT.isCurrent());
fork.attach(); fork.attach();
root.attach();
assertTrue(root.isCurrent());
root.detach();
assertTrue(fork.isCurrent()); assertTrue(fork.isCurrent());
fork.detach();
assertTrue(root.isCurrent());
} }
@Test @Test
@ -155,58 +150,53 @@ public class ContextTest {
@Test @Test
public void attachedCancellableContextCannotBeCastFromCurrent() { public void attachedCancellableContextCannotBeCastFromCurrent() {
Context initial = Context.current(); Context initial = Context.current();
Context.CancellableContext base = Context.current().withCancellation(); Context.CancellableContext base = initial.withCancellation();
base.attach(); base.attach();
assertFalse(Context.current() instanceof Context.CancellableContext); assertFalse(Context.current() instanceof Context.CancellableContext);
assertNotSame(base, Context.current());
assertNotSame(initial, Context.current()); assertNotSame(initial, Context.current());
base.detachAndCancel(null); base.detachAndCancel(initial, null);
assertTrue(initial.isCurrent()); assertSame(initial, Context.current());
} }
@Test @Test
public void detachingNonCurrentThrowsIllegalStateException() { public void attachingNonCurrentReturnsCurrent() {
Context current = Context.current(); Context initial = Context.current();
Context base = Context.current().withValue(PET, "dog"); Context base = initial.withValue(PET, "dog");
try { assertSame(initial, base.attach());
base.detach(); assertSame(base, initial.attach());
fail("Expected exception");
} catch (IllegalStateException ise) {
// expected
}
assertSame(current, Context.current());
base.attach();
try {
current.withValue(PET, "cat").detach();
fail("Expected exception");
} catch (IllegalStateException ise) {
// expected
}
assertSame(base, Context.current());
base.detach();
} }
@Test @Test
public void detachUnwindsAttach() { public void detachingNonCurrentLogsSevereMessage() {
Context base = Context.current().fork(); final AtomicReference<LogRecord> logRef = new AtomicReference<LogRecord>();
Context child = base.withValue(COLOR, "red"); Handler handler = new Handler() {
Context grandchild = child.withValue(COLOR, "blue"); @Override
base.attach(); public void publish(LogRecord record) {
base.attach(); logRef.set(record);
child.attach(); }
base.attach();
grandchild.attach(); @Override
assertTrue(grandchild.isCurrent()); public void flush() {
grandchild.detach(); }
assertTrue(base.isCurrent());
base.detach(); @Override
assertTrue(child.isCurrent()); public void close() throws SecurityException {
child.detach(); }
assertTrue(base.isCurrent()); };
base.detach(); Logger logger = Logger.getLogger(Context.class.getName());
assertTrue(base.isCurrent()); try {
base.detach(); logger.addHandler(handler);
assertTrue(Context.ROOT.isCurrent()); Context initial = Context.current();
Context base = initial.withValue(PET, "dog");
// Base is not attached
base.detach(initial);
assertSame(initial, Context.current());
assertNotNull(logRef.get());
assertEquals(Level.SEVERE, logRef.get().getLevel());
} finally {
logger.removeHandler(handler);
}
} }
@Test @Test
@ -226,14 +216,14 @@ public class ContextTest {
assertEquals("cheese", FOOD.get()); assertEquals("cheese", FOOD.get());
assertNull(COLOR.get()); assertNull(COLOR.get());
child.detach(); child.detach(base);
// Should have values from base // Should have values from base
assertEquals("dog", PET.get()); assertEquals("dog", PET.get());
assertEquals("lasagna", FOOD.get()); assertEquals("lasagna", FOOD.get());
assertNull(COLOR.get()); assertNull(COLOR.get());
base.detach(); base.detach(Context.ROOT);
assertNull(PET.get()); assertNull(PET.get());
assertEquals("lasagna", FOOD.get()); assertEquals("lasagna", FOOD.get());
@ -253,7 +243,7 @@ public class ContextTest {
assertEquals("blue", COLOR.get()); assertEquals("blue", COLOR.get());
assertEquals(fav, FAVORITE.get()); assertEquals(fav, FAVORITE.get());
child.detach(); base.attach();
} }
@Test @Test
@ -407,7 +397,7 @@ public class ContextTest {
assertSame(t, attached.cause()); assertSame(t, attached.cause());
assertSame(attached, listenerNotifedContext); assertSame(attached, listenerNotifedContext);
base.detach(); Context.ROOT.attach();
} }
@Test @Test
@ -469,7 +459,7 @@ public class ContextTest {
} }
assertSame(current, Context.current()); assertSame(current, Context.current());
current.detach(); current.detach(Context.ROOT);
} }
@Test @Test
@ -509,7 +499,7 @@ public class ContextTest {
} }
assertSame(current, Context.current()); assertSame(current, Context.current());
current.detach(); current.detach(Context.ROOT);
} }
@Test @Test
@ -517,11 +507,11 @@ public class ContextTest {
QueuedExecutor queuedExecutor = new QueuedExecutor(); QueuedExecutor queuedExecutor = new QueuedExecutor();
Executor executor = Context.propagate(queuedExecutor); Executor executor = Context.propagate(queuedExecutor);
Context base = Context.current().withValue(PET, "cat"); Context base = Context.current().withValue(PET, "cat");
base.attach(); Context previous = base.attach();
try { try {
executor.execute(runner); executor.execute(runner);
} finally { } finally {
base.detach(); base.detach(previous);
} }
assertEquals(1, queuedExecutor.runnables.size()); assertEquals(1, queuedExecutor.runnables.size());
queuedExecutor.runnables.remove().run(); queuedExecutor.runnables.remove().run();
@ -541,12 +531,12 @@ public class ContextTest {
@Test @Test
public void typicalTryFinallyHandling() throws Exception { public void typicalTryFinallyHandling() throws Exception {
Context base = Context.current().withValue(COLOR, "blue"); Context base = Context.current().withValue(COLOR, "blue");
base.attach(); Context previous = base.attach();
try { try {
assertTrue(base.isCurrent()); assertTrue(base.isCurrent());
// Do something // Do something
} finally { } finally {
base.detach(); base.detach(previous);
} }
assertFalse(base.isCurrent()); assertFalse(base.isCurrent());
} }
@ -554,14 +544,14 @@ public class ContextTest {
@Test @Test
public void typicalCancellableTryCatchFinallyHandling() throws Exception { public void typicalCancellableTryCatchFinallyHandling() throws Exception {
Context.CancellableContext base = Context.current().withCancellation(); Context.CancellableContext base = Context.current().withCancellation();
base.attach(); Context previous = base.attach();
try { try {
// Do something // Do something
throw new IllegalStateException("Argh"); throw new IllegalStateException("Argh");
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
base.cancel(ise); base.cancel(ise);
} finally { } finally {
base.detachAndCancel(null); base.detachAndCancel(previous, null);
} }
assertTrue(base.isCancelled()); assertTrue(base.isCancelled());
assertNotNull(base.cause()); assertNotNull(base.cause());
@ -572,11 +562,11 @@ public class ContextTest {
Context current = Context.current(); Context current = Context.current();
Context.CancellableContext base = Context.current().withValue(FOOD, "fish").withCancellation(); Context.CancellableContext base = Context.current().withValue(FOOD, "fish").withCancellation();
base.attach(); Context previous = base.attach();
Context baseAttached = Context.current(); Context baseAttached = Context.current();
// Should not be able to get back the CancellableContext // Should not be able to get back the CancellableContext
assertNotSame(baseAttached, base); assertNotSame(baseAttached, base);
base.detach(); base.detach(previous);
Closeable c = base.attachAsCloseable(); Closeable c = base.attachAsCloseable();
assertEquals("fish", FOOD.get()); assertEquals("fish", FOOD.get());