blob: cd1c405936e9e94d26ede302279b72cf97b162b7 [file] [log] [blame]
/*
* Copyright 2015, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link Context}.
*/
@RunWith(JUnit4.class)
public class ContextTest {
private static final Context.Key<String> PET = Context.key("pet");
private static final Context.Key<String> FOOD = Context.keyWithDefault("food", "lasagna");
private static final Context.Key<String> COLOR = Context.key("color");
private static final Context.Key<Object> FAVORITE = Context.key("favorite");
private static final Context.Key<Integer> LUCKY = Context.key("lucky");
private Context listenerNotifedContext;
private CountDownLatch deadlineLatch = new CountDownLatch(1);
private Context.CancellationListener cancellationListener = new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
listenerNotifedContext = context;
deadlineLatch.countDown();
}
};
private Context observed;
private Runnable runner = new Runnable() {
@Override
public void run() {
observed = Context.current();
}
};
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
@Before
public void setUp() throws Exception {
Context.ROOT.attach();
}
@After
public void tearDown() throws Exception {
scheduler.shutdown();
}
@Test
public void defaultContext() throws Exception {
final SettableFuture<Context> contextOfNewThread = SettableFuture.create();
Context contextOfThisThread = Context.ROOT.withValue(PET, "dog");
contextOfThisThread.attach();
new Thread(new Runnable() {
@Override
public void run() {
contextOfNewThread.set(Context.current());
}
}).start();
assertNotNull(contextOfNewThread.get(5, TimeUnit.SECONDS));
assertNotSame(contextOfThisThread, contextOfNewThread.get());
assertSame(contextOfThisThread, Context.current());
}
@Test
public void rootCanBeAttached() {
Context fork = Context.ROOT.fork();
fork.attach();
Context.ROOT.attach();
assertTrue(Context.ROOT.isCurrent());
fork.attach();
assertTrue(fork.isCurrent());
}
@Test
public void rootCanNeverHaveAListener() {
Context root = Context.current();
root.addListener(cancellationListener, MoreExecutors.directExecutor());
assertEquals(0, root.listenerCount());
}
@Test
public void rootIsNotCancelled() {
assertFalse(Context.ROOT.isCancelled());
assertNull(Context.ROOT.cancellationCause());
}
@Test
public void attachedCancellableContextCannotBeCastFromCurrent() {
Context initial = Context.current();
Context.CancellableContext base = initial.withCancellation();
base.attach();
assertFalse(Context.current() instanceof Context.CancellableContext);
assertNotSame(base, Context.current());
assertNotSame(initial, Context.current());
base.detachAndCancel(initial, null);
assertSame(initial, Context.current());
}
@Test
public void attachingNonCurrentReturnsCurrent() {
Context initial = Context.current();
Context base = initial.withValue(PET, "dog");
assertSame(initial, base.attach());
assertSame(base, initial.attach());
}
@Test
public void detachingNonCurrentLogsSevereMessage() {
final AtomicReference<LogRecord> logRef = new AtomicReference<LogRecord>();
Handler handler = new Handler() {
@Override
public void publish(LogRecord record) {
logRef.set(record);
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
};
Logger logger = Logger.getLogger(Context.storage().getClass().getName());
try {
logger.addHandler(handler);
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
public void valuesAndOverrides() {
Context base = Context.current().withValue(PET, "dog");
Context child = base.withValues(PET, null, FOOD, "cheese");
base.attach();
assertEquals("dog", PET.get());
assertEquals("lasagna", FOOD.get());
assertNull(COLOR.get());
child.attach();
assertNull(PET.get());
assertEquals("cheese", FOOD.get());
assertNull(COLOR.get());
child.detach(base);
// Should have values from base
assertEquals("dog", PET.get());
assertEquals("lasagna", FOOD.get());
assertNull(COLOR.get());
base.detach(Context.ROOT);
assertNull(PET.get());
assertEquals("lasagna", FOOD.get());
assertNull(COLOR.get());
}
@Test
public void withValuesThree() {
Object fav = new Object();
Context base = Context.current().withValues(PET, "dog", COLOR, "blue");
Context child = base.withValues(PET, "cat", FOOD, "cheese", FAVORITE, fav);
child.attach();
assertEquals("cat", PET.get());
assertEquals("cheese", FOOD.get());
assertEquals("blue", COLOR.get());
assertEquals(fav, FAVORITE.get());
base.attach();
}
@Test
public void withValuesFour() {
Object fav = new Object();
Context base = Context.current().withValues(PET, "dog", COLOR, "blue");
Context child = base.withValues(PET, "cat", FOOD, "cheese", FAVORITE, fav, LUCKY, 7);
child.attach();
assertEquals("cat", PET.get());
assertEquals("cheese", FOOD.get());
assertEquals("blue", COLOR.get());
assertEquals(fav, FAVORITE.get());
assertEquals(7, (int) LUCKY.get());
base.attach();
}
@Test
public void cancelReturnsFalseIfAlreadyCancelled() {
Context.CancellableContext base = Context.current().withCancellation();
assertTrue(base.cancel(null));
assertTrue(base.isCancelled());
assertFalse(base.cancel(null));
}
@Test
public void notifyListenersOnCancel() {
class SetContextCancellationListener implements Context.CancellationListener {
private final AtomicReference<Context> observed;
public SetContextCancellationListener(AtomicReference<Context> observed) {
this.observed = observed;
}
@Override
public void cancelled(Context context) {
observed.set(context);
}
}
Context.CancellableContext base = Context.current().withCancellation();
final AtomicReference<Context> observed1 = new AtomicReference<Context>();
base.addListener(new SetContextCancellationListener(observed1), MoreExecutors.directExecutor());
final AtomicReference<Context> observed2 = new AtomicReference<Context>();
base.addListener(new SetContextCancellationListener(observed2), MoreExecutors.directExecutor());
assertNull(observed1.get());
assertNull(observed2.get());
base.cancel(null);
assertSame(base, observed1.get());
assertSame(base, observed2.get());
final AtomicReference<Context> observed3 = new AtomicReference<Context>();
base.addListener(new SetContextCancellationListener(observed3), MoreExecutors.directExecutor());
assertSame(base, observed3.get());
}
@Test
public void exceptionOfExecutorDoesntThrow() {
final AtomicReference<Throwable> loggedThrowable = new AtomicReference<Throwable>();
Handler logHandler = new Handler() {
@Override
public void publish(LogRecord record) {
Throwable thrown = record.getThrown();
if (thrown != null) {
if (loggedThrowable.get() == null) {
loggedThrowable.set(thrown);
} else {
loggedThrowable.set(new RuntimeException("Too many exceptions", thrown));
}
}
}
@Override
public void close() {}
@Override
public void flush() {}
};
Logger logger = Logger.getLogger(Context.class.getName());
logger.addHandler(logHandler);
try {
Context.CancellableContext base = Context.current().withCancellation();
final AtomicReference<Runnable> observed1 = new AtomicReference<Runnable>();
final Error err = new Error();
base.addListener(cancellationListener, new Executor() {
@Override
public void execute(Runnable runnable) {
observed1.set(runnable);
throw err;
}
});
assertNull(observed1.get());
assertNull(loggedThrowable.get());
base.cancel(null);
assertNotNull(observed1.get());
assertSame(err, loggedThrowable.get());
final Error err2 = new Error();
loggedThrowable.set(null);
final AtomicReference<Runnable> observed2 = new AtomicReference<Runnable>();
base.addListener(cancellationListener, new Executor() {
@Override
public void execute(Runnable runnable) {
observed2.set(runnable);
throw err2;
}
});
assertNotNull(observed2.get());
assertSame(err2, loggedThrowable.get());
} finally {
logger.removeHandler(logHandler);
}
}
@Test
public void cascadingCancellationNotifiesChild() {
// Root is not cancellable so we can't cascade from it
Context.CancellableContext base = Context.current().withCancellation();
assertEquals(0, base.listenerCount());
Context child = base.withValue(FOOD, "lasagna");
assertEquals(0, child.listenerCount());
child.addListener(cancellationListener, MoreExecutors.directExecutor());
assertEquals(1, child.listenerCount());
assertEquals(1, base.listenerCount()); // child is now listening to base
assertFalse(base.isCancelled());
assertFalse(child.isCancelled());
IllegalStateException cause = new IllegalStateException();
base.cancel(cause);
assertTrue(base.isCancelled());
assertSame(cause, base.cancellationCause());
assertSame(child, listenerNotifedContext);
assertTrue(child.isCancelled());
assertSame(cause, child.cancellationCause());
assertEquals(0, base.listenerCount());
assertEquals(0, child.listenerCount());
}
@Test
public void cascadingCancellationWithoutListener() {
Context.CancellableContext base = Context.current().withCancellation();
Context child = base.withCancellation();
Throwable t = new Throwable();
base.cancel(t);
assertTrue(child.isCancelled());
assertSame(t, child.cancellationCause());
}
@Test
public void cancellableContextIsAttached() {
Context.CancellableContext base = Context.current().withValue(FOOD, "fish").withCancellation();
assertFalse(base.isCurrent());
base.attach();
Context attached = Context.current();
assertSame("fish", FOOD.get());
assertFalse(attached.isCancelled());
assertNull(attached.cancellationCause());
assertTrue(attached.canBeCancelled());
assertTrue(attached.isCurrent());
assertTrue(base.isCurrent());
attached.addListener(cancellationListener, MoreExecutors.directExecutor());
Throwable t = new Throwable();
base.cancel(t);
assertTrue(attached.isCancelled());
assertSame(t, attached.cancellationCause());
assertSame(attached, listenerNotifedContext);
Context.ROOT.attach();
}
@Test
public void cancellableContextCascadesFromCancellableParent() {
// Root is not cancellable so we can't cascade from it
Context.CancellableContext base = Context.current().withCancellation();
Context child = base.withCancellation();
child.addListener(cancellationListener, MoreExecutors.directExecutor());
assertFalse(base.isCancelled());
assertFalse(child.isCancelled());
IllegalStateException cause = new IllegalStateException();
base.cancel(cause);
assertTrue(base.isCancelled());
assertSame(cause, base.cancellationCause());
assertSame(child, listenerNotifedContext);
assertTrue(child.isCancelled());
assertSame(cause, child.cancellationCause());
assertEquals(0, base.listenerCount());
assertEquals(0, child.listenerCount());
}
@Test
public void nonCascadingCancellationDoesNotNotifyForked() {
Context.CancellableContext base = Context.current().withCancellation();
Context fork = base.fork();
fork.addListener(cancellationListener, MoreExecutors.directExecutor());
assertEquals(0, base.listenerCount());
assertEquals(0, fork.listenerCount());
assertTrue(base.cancel(new Throwable()));
assertNull(listenerNotifedContext);
assertFalse(fork.isCancelled());
assertNull(fork.cancellationCause());
}
@Test
public void testWrapRunnable() throws Exception {
Context base = Context.current().withValue(PET, "cat");
Context current = Context.current().withValue(PET, "fish");
current.attach();
base.wrap(runner).run();
assertSame(base, observed);
assertSame(current, Context.current());
current.wrap(runner).run();
assertSame(current, observed);
assertSame(current, Context.current());
final Error err = new Error();
try {
base.wrap(new Runnable() {
@Override
public void run() {
throw err;
}
}).run();
fail("Expected exception");
} catch (Error ex) {
assertSame(err, ex);
}
assertSame(current, Context.current());
current.detach(Context.ROOT);
}
@Test
public void testWrapCallable() throws Exception {
Context base = Context.current().withValue(PET, "cat");
Context current = Context.current().withValue(PET, "fish");
current.attach();
final Object ret = new Object();
Callable<Object> callable = new Callable<Object>() {
@Override
public Object call() {
runner.run();
return ret;
}
};
assertSame(ret, base.wrap(callable).call());
assertSame(base, observed);
assertSame(current, Context.current());
assertSame(ret, current.wrap(callable).call());
assertSame(current, observed);
assertSame(current, Context.current());
final Error err = new Error();
try {
base.wrap(new Callable<Object>() {
@Override
public Object call() {
throw err;
}
}).call();
fail("Excepted exception");
} catch (Error ex) {
assertSame(err, ex);
}
assertSame(current, Context.current());
current.detach(Context.ROOT);
}
@Test
public void currentContextExecutor() throws Exception {
QueuedExecutor queuedExecutor = new QueuedExecutor();
Executor executor = Context.currentContextExecutor(queuedExecutor);
Context base = Context.current().withValue(PET, "cat");
Context previous = base.attach();
try {
executor.execute(runner);
} finally {
base.detach(previous);
}
assertEquals(1, queuedExecutor.runnables.size());
queuedExecutor.runnables.remove().run();
assertSame(base, observed);
}
@Test
public void fixedContextExecutor() throws Exception {
Context base = Context.current().withValue(PET, "cat");
QueuedExecutor queuedExecutor = new QueuedExecutor();
base.fixedContextExecutor(queuedExecutor).execute(runner);
assertEquals(1, queuedExecutor.runnables.size());
queuedExecutor.runnables.remove().run();
assertSame(base, observed);
}
@Test
public void typicalTryFinallyHandling() throws Exception {
Context base = Context.current().withValue(COLOR, "blue");
Context previous = base.attach();
try {
assertTrue(base.isCurrent());
// Do something
} finally {
base.detach(previous);
}
assertFalse(base.isCurrent());
}
@Test
public void typicalCancellableTryCatchFinallyHandling() throws Exception {
Context.CancellableContext base = Context.current().withCancellation();
Context previous = base.attach();
try {
// Do something
throw new IllegalStateException("Argh");
} catch (IllegalStateException ise) {
base.cancel(ise);
} finally {
base.detachAndCancel(previous, null);
}
assertTrue(base.isCancelled());
assertNotNull(base.cancellationCause());
}
@Test
public void rootHasNoDeadline() {
assertNull(Context.ROOT.getDeadline());
}
@Test
public void contextWithDeadlineHasDeadline() {
Context.CancellableContext cancellableContext =
Context.ROOT.withDeadlineAfter(1, TimeUnit.SECONDS, scheduler);
assertNotNull(cancellableContext.getDeadline());
}
@Test
public void earlierParentDeadlineTakesPrecedenceOverLaterChildDeadline() throws Exception {
final Deadline sooner = Deadline.after(100, TimeUnit.MILLISECONDS);
final Deadline later = Deadline.after(1, TimeUnit.MINUTES);
Context.CancellableContext parent = Context.ROOT.withDeadline(sooner, scheduler);
Context.CancellableContext child = parent.withDeadline(later, scheduler);
assertSame(parent.getDeadline(), sooner);
assertSame(child.getDeadline(), sooner);
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Exception> error = new AtomicReference<Exception>();
child.addListener(new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
try {
assertTrue(sooner.isExpired());
assertFalse(later.isExpired());
} catch (Exception e) {
error.set(e);
}
latch.countDown();
}
}, MoreExecutors.directExecutor());
latch.await(3, TimeUnit.SECONDS);
if (error.get() != null) {
throw error.get();
}
}
@Test
public void earlierChldDeadlineTakesPrecedenceOverLaterParentDeadline() {
Deadline sooner = Deadline.after(1, TimeUnit.HOURS);
Deadline later = Deadline.after(1, TimeUnit.DAYS);
Context.CancellableContext parent = Context.ROOT.withDeadline(later, scheduler);
Context.CancellableContext child = parent.withDeadline(sooner, scheduler);
assertSame(parent.getDeadline(), later);
assertSame(child.getDeadline(), sooner);
}
@Test
public void forkingContextDoesNotCarryDeadline() {
Deadline deadline = Deadline.after(1, TimeUnit.HOURS);
Context.CancellableContext parent = Context.ROOT.withDeadline(deadline, scheduler);
Context fork = parent.fork();
assertNull(fork.getDeadline());
}
@Test
public void cancellationDoesNotExpireDeadline() {
Deadline deadline = Deadline.after(1, TimeUnit.HOURS);
Context.CancellableContext parent = Context.ROOT.withDeadline(deadline, scheduler);
parent.cancel(null);
assertFalse(deadline.isExpired());
}
@Test
public void absoluteDeadlineTriggersAndPropagates() throws Exception {
Context base = Context.current().withDeadline(Deadline.after(1, TimeUnit.SECONDS), scheduler);
Context child = base.withValue(FOOD, "lasagna");
child.addListener(cancellationListener, MoreExecutors.directExecutor());
assertFalse(base.isCancelled());
assertFalse(child.isCancelled());
assertTrue(deadlineLatch.await(2, TimeUnit.SECONDS));
assertTrue(base.isCancelled());
assertTrue(base.cancellationCause() instanceof TimeoutException);
assertSame(child, listenerNotifedContext);
assertTrue(child.isCancelled());
assertSame(base.cancellationCause(), child.cancellationCause());
}
@Test
public void relativeDeadlineTriggersAndPropagates() throws Exception {
Context base = Context.current().withDeadline(Deadline.after(1, TimeUnit.SECONDS), scheduler);
Context child = base.withValue(FOOD, "lasagna");
child.addListener(cancellationListener, MoreExecutors.directExecutor());
assertFalse(base.isCancelled());
assertFalse(child.isCancelled());
assertTrue(deadlineLatch.await(2, TimeUnit.SECONDS));
assertTrue(base.isCancelled());
assertTrue(base.cancellationCause() instanceof TimeoutException);
assertSame(child, listenerNotifedContext);
assertTrue(child.isCancelled());
assertSame(base.cancellationCause(), child.cancellationCause());
}
@Test
public void innerDeadlineCompletesBeforeOuter() throws Exception {
Context base = Context.current().withDeadline(Deadline.after(2, TimeUnit.SECONDS), scheduler);
Context child = base.withDeadline(Deadline.after(1, TimeUnit.SECONDS), scheduler);
child.addListener(cancellationListener, MoreExecutors.directExecutor());
assertFalse(base.isCancelled());
assertFalse(child.isCancelled());
assertTrue(deadlineLatch.await(2, TimeUnit.SECONDS));
assertFalse(base.isCancelled());
assertSame(child, listenerNotifedContext);
assertTrue(child.isCancelled());
assertTrue(child.cancellationCause() instanceof TimeoutException);
deadlineLatch = new CountDownLatch(1);
base.addListener(cancellationListener, MoreExecutors.directExecutor());
assertTrue(deadlineLatch.await(2, TimeUnit.SECONDS));
assertTrue(base.isCancelled());
assertTrue(base.cancellationCause() instanceof TimeoutException);
assertNotSame(base.cancellationCause(), child.cancellationCause());
}
@Test
public void cancellationCancelsScheduledTask() {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
try {
assertEquals(0, executor.getQueue().size());
Context.CancellableContext base
= Context.current().withDeadline(Deadline.after(1, TimeUnit.DAYS), executor);
assertEquals(1, executor.getQueue().size());
base.cancel(null);
executor.purge();
assertEquals(0, executor.getQueue().size());
} finally {
executor.shutdown();
}
}
private static class QueuedExecutor implements Executor {
private final Queue<Runnable> runnables = new ArrayDeque<Runnable>();
@Override
public void execute(Runnable r) {
runnables.add(r);
}
}
@Test
public void childContextListenerNotifiedAfterParentListener() {
Context.CancellableContext parent = Context.current().withCancellation();
Context child = parent.withValue(COLOR, "red");
final AtomicBoolean childAfterParent = new AtomicBoolean();
final AtomicBoolean parentCalled = new AtomicBoolean();
child.addListener(new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
if (parentCalled.get()) {
childAfterParent.set(true);
}
}
}, MoreExecutors.directExecutor());
parent.addListener(new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
parentCalled.set(true);
}
}, MoreExecutors.directExecutor());
parent.cancel(null);
assertTrue(parentCalled.get());
assertTrue(childAfterParent.get());
}
@Test
public void expiredDeadlineShouldCancelContextImmediately() {
Context parent = Context.current();
assertFalse(parent.isCancelled());
Context.CancellableContext context = parent.withDeadlineAfter(0, TimeUnit.SECONDS, scheduler);
assertTrue(context.isCancelled());
assertThat(context.cancellationCause(), instanceOf(TimeoutException.class));
assertFalse(parent.isCancelled());
Deadline deadline = Deadline.after(-10, TimeUnit.SECONDS);
assertTrue(deadline.isExpired());
context = parent.withDeadline(deadline, scheduler);
assertTrue(context.isCancelled());
assertThat(context.cancellationCause(), instanceOf(TimeoutException.class));
}
}