| /* |
| * Copyright 2015 The gRPC 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 io.grpc; |
| |
| import static com.google.common.truth.TruthJUnit.assume; |
| import static io.grpc.Context.cancellableAncestor; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| 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.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.common.util.concurrent.SettableFuture; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| 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.Future; |
| 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 java.util.regex.Pattern; |
| 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) |
| @SuppressWarnings("CheckReturnValue") // false-positive in test for current ver errorprone plugin |
| 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 final Context.CancellationListener cancellationListener = |
| new Context.CancellationListener() { |
| @Override |
| public void cancelled(Context context) { |
| listenerNotifedContext = context; |
| deadlineLatch.countDown(); |
| } |
| }; |
| |
| private Context observed; |
| private final Runnable runner = new Runnable() { |
| @Override |
| public void run() { |
| observed = Context.current(); |
| } |
| }; |
| private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); |
| |
| @Before |
| public void setUp() { |
| Context.ROOT.attach(); |
| } |
| |
| @After |
| public void tearDown() { |
| scheduler.shutdown(); |
| assertEquals(Context.ROOT, Context.current()); |
| } |
| |
| @Test |
| public void defaultContext() throws Exception { |
| final SettableFuture<Context> contextOfNewThread = SettableFuture.create(); |
| Context contextOfThisThread = Context.ROOT.withValue(PET, "dog"); |
| Context toRestore = 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()); |
| contextOfThisThread.detach(toRestore); |
| } |
| |
| @Test |
| public void rootCanBeAttached() { |
| Context fork = Context.ROOT.fork(); |
| Context toRestore1 = fork.attach(); |
| Context toRestore2 = Context.ROOT.attach(); |
| assertTrue(Context.ROOT.isCurrent()); |
| |
| Context toRestore3 = fork.attach(); |
| assertTrue(fork.isCurrent()); |
| |
| fork.detach(toRestore3); |
| Context.ROOT.detach(toRestore2); |
| fork.detach(toRestore1); |
| } |
| |
| @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<>(); |
| 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); |
| |
| Context toRestore = child.attach(); |
| |
| assertEquals("cat", PET.get()); |
| assertEquals("cheese", FOOD.get()); |
| assertEquals("blue", COLOR.get()); |
| assertEquals(fav, FAVORITE.get()); |
| |
| child.detach(toRestore); |
| } |
| |
| @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); |
| |
| Context toRestore = child.attach(); |
| |
| assertEquals("cat", PET.get()); |
| assertEquals("cheese", FOOD.get()); |
| assertEquals("blue", COLOR.get()); |
| assertEquals(fav, FAVORITE.get()); |
| assertEquals(7, (int) LUCKY.get()); |
| |
| child.detach(toRestore); |
| } |
| |
| @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<>(); |
| base.addListener(new SetContextCancellationListener(observed1), MoreExecutors.directExecutor()); |
| final AtomicReference<Context> observed2 = new AtomicReference<>(); |
| 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<>(); |
| base.addListener(new SetContextCancellationListener(observed3), MoreExecutors.directExecutor()); |
| assertSame(base, observed3.get()); |
| } |
| |
| @Test |
| public void removeListenersFromContextAndChildContext() { |
| class SetContextCancellationListener implements Context.CancellationListener { |
| private final List<Context> observedContexts; |
| |
| SetContextCancellationListener() { |
| this.observedContexts = Collections.synchronizedList(new ArrayList<Context>()); |
| } |
| |
| @Override |
| public void cancelled(Context context) { |
| observedContexts.add(context); |
| } |
| } |
| |
| Context.CancellableContext base = Context.current().withCancellation(); |
| Context child = base.withValue(PET, "tiger"); |
| Context childOfChild = base.withValue(PET, "lion"); |
| final SetContextCancellationListener listener = new SetContextCancellationListener(); |
| base.addListener(listener, MoreExecutors.directExecutor()); |
| child.addListener(listener, MoreExecutors.directExecutor()); |
| childOfChild.addListener(listener, MoreExecutors.directExecutor()); |
| base.removeListener(listener); |
| childOfChild.removeListener(listener); |
| base.cancel(null); |
| assertEquals(1, listener.observedContexts.size()); |
| assertSame(child, listener.observedContexts.get(0)); |
| } |
| |
| @Test |
| public void exceptionOfExecutorDoesntThrow() { |
| final AtomicReference<Throwable> loggedThrowable = new AtomicReference<>(); |
| 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<>(); |
| 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<>(); |
| 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()); |
| } |
| |
| // Context#isCurrent() and Context.CancellableContext#isCurrent() are intended |
| // to be visible only for testing. The deprecation is meant for users. |
| @SuppressWarnings("deprecation") |
| @Test |
| public void cancellableContextIsAttached() { |
| Context.CancellableContext base = Context.current().withValue(FOOD, "fish").withCancellation(); |
| assertFalse(base.isCurrent()); |
| Context toRestore = base.attach(); |
| |
| Context attached = Context.current(); |
| assertSame("fish", FOOD.get()); |
| assertFalse(attached.isCancelled()); |
| assertNull(attached.cancellationCause()); |
| 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); |
| |
| base.detach(toRestore); |
| } |
| |
| @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() { |
| 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 TestError err = new TestError(); |
| try { |
| base.wrap(new Runnable() { |
| @Override |
| public void run() { |
| throw err; |
| } |
| }).run(); |
| fail("Expected exception"); |
| } catch (TestError 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 TestError err = new TestError(); |
| try { |
| base.wrap(new Callable<Object>() { |
| @Override |
| public Object call() { |
| throw err; |
| } |
| }).call(); |
| fail("Excepted exception"); |
| } catch (TestError ex) { |
| assertSame(err, ex); |
| } |
| assertSame(current, Context.current()); |
| |
| current.detach(Context.ROOT); |
| } |
| |
| @Test |
| public void currentContextExecutor() { |
| 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() { |
| 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() { |
| 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() { |
| 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<>(); |
| 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()); |
| assertTrue("cancellation failed", 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<>(); |
| |
| @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)); |
| } |
| |
| /** |
| * Tests initializing the {@link Context} class with a custom logger which uses Context's storage |
| * when logging. |
| */ |
| @Test |
| public void initContextWithCustomClassLoaderWithCustomLogger() throws Exception { |
| StaticTestingClassLoader classLoader = |
| new StaticTestingClassLoader( |
| getClass().getClassLoader(), Pattern.compile("io\\.grpc\\.[^.]+")); |
| Class<?> runnable = |
| classLoader.loadClass(LoadMeWithStaticTestingClassLoader.class.getName()); |
| |
| ((Runnable) runnable.getDeclaredConstructor().newInstance()).run(); |
| } |
| |
| /** |
| * Ensure that newly created threads can attach/detach a context. |
| * The current test thread already has a context manually attached in {@link #setUp()}. |
| */ |
| @Test |
| public void newThreadAttachContext() throws Exception { |
| Context parent = Context.current().withValue(COLOR, "blue"); |
| parent.call(new Callable<Object>() { |
| @Override |
| public Object call() throws Exception { |
| assertEquals("blue", COLOR.get()); |
| |
| final Context child = Context.current().withValue(COLOR, "red"); |
| Future<String> workerThreadVal = scheduler |
| .submit(new Callable<String>() { |
| @Override |
| public String call() { |
| Context initial = Context.current(); |
| assertNotNull(initial); |
| Context toRestore = child.attach(); |
| try { |
| assertNotNull(toRestore); |
| return COLOR.get(); |
| } finally { |
| child.detach(toRestore); |
| assertEquals(initial, Context.current()); |
| } |
| } |
| }); |
| assertEquals("red", workerThreadVal.get()); |
| |
| assertEquals("blue", COLOR.get()); |
| return null; |
| } |
| }); |
| } |
| |
| /** |
| * Similar to {@link #newThreadAttachContext()} but without giving the new thread a specific ctx. |
| */ |
| @Test |
| public void newThreadWithoutContext() throws Exception { |
| Context parent = Context.current().withValue(COLOR, "blue"); |
| parent.call(new Callable<Object>() { |
| @Override |
| public Object call() throws Exception { |
| assertEquals("blue", COLOR.get()); |
| |
| Future<String> workerThreadVal = scheduler |
| .submit(new Callable<String>() { |
| @Override |
| public String call() { |
| assertNotNull(Context.current()); |
| return COLOR.get(); |
| } |
| }); |
| assertNull(workerThreadVal.get()); |
| |
| assertEquals("blue", COLOR.get()); |
| return null; |
| } |
| }); |
| } |
| |
| @Test |
| public void storageReturnsNullTest() throws Exception { |
| // TODO(sergiitk): JDK-8210522 changes the behaviour of Java reflection to filter out |
| // security-sensitive fields in the java.lang.reflect.Field. This prohibits |
| // Field.class.getDeclaredFields("modifiers") call we rely on in this test. |
| // Until we have a good solution for setting a custom storage for testing purposes, |
| // we'll have to skip this test for JDK >= 11. Ref https://bugs.openjdk.org/browse/JDK-8210522 |
| double javaVersion; |
| // Graceful version check. Run the test if the version undetermined. |
| try { |
| javaVersion = Double.parseDouble(System.getProperty("java.specification.version", "0")); |
| } catch (NumberFormatException e) { |
| javaVersion = 0; |
| } |
| assume().that(javaVersion).isLessThan(11); |
| |
| Class<?> lazyStorageClass = Class.forName("io.grpc.Context$LazyStorage"); |
| Field storage = lazyStorageClass.getDeclaredField("storage"); |
| assertTrue(Modifier.isFinal(storage.getModifiers())); |
| // use reflection to forcibly change the storage object to a test object |
| storage.setAccessible(true); |
| Field modifiersField = Field.class.getDeclaredField("modifiers"); |
| modifiersField.setAccessible(true); |
| int storageModifiers = modifiersField.getInt(storage); |
| modifiersField.set(storage, storageModifiers & ~Modifier.FINAL); |
| Object o = storage.get(null); |
| Context.Storage originalStorage = (Context.Storage) o; |
| try { |
| storage.set(null, new Context.Storage() { |
| @Override |
| public Context doAttach(Context toAttach) { |
| return null; |
| } |
| |
| @Override |
| public void detach(Context toDetach, Context toRestore) { |
| // noop |
| } |
| |
| @Override |
| public Context current() { |
| return null; |
| } |
| }); |
| // current() returning null gets transformed into ROOT |
| assertEquals(Context.ROOT, Context.current()); |
| |
| // doAttach() returning null gets transformed into ROOT |
| Context blueContext = Context.current().withValue(COLOR, "blue"); |
| Context toRestore = blueContext.attach(); |
| assertEquals(Context.ROOT, toRestore); |
| |
| // final sanity check |
| blueContext.detach(toRestore); |
| assertEquals(Context.ROOT, Context.current()); |
| } finally { |
| // undo the changes |
| storage.set(null, originalStorage); |
| storage.setAccessible(false); |
| modifiersField.set(storage, storageModifiers | Modifier.FINAL); |
| modifiersField.setAccessible(false); |
| } |
| } |
| |
| @Test |
| public void cancellableAncestorTest() { |
| Context c = Context.current(); |
| assertNull(cancellableAncestor(c)); |
| |
| Context.CancellableContext withCancellation = c.withCancellation(); |
| assertEquals(withCancellation, cancellableAncestor(withCancellation)); |
| |
| Context child = withCancellation.withValue(COLOR, "blue"); |
| assertFalse(child instanceof Context.CancellableContext); |
| assertEquals(withCancellation, cancellableAncestor(child)); |
| |
| Context grandChild = child.withValue(COLOR, "red"); |
| assertFalse(grandChild instanceof Context.CancellableContext); |
| assertEquals(withCancellation, cancellableAncestor(grandChild)); |
| } |
| |
| @Test |
| public void cancellableAncestorIntegrationTest() { |
| Context base = Context.current(); |
| |
| Context blue = base.withValue(COLOR, "blue"); |
| assertNull(blue.cancellableAncestor); |
| Context.CancellableContext cancellable = blue.withCancellation(); |
| assertNull(cancellable.cancellableAncestor); |
| Context childOfCancel = cancellable.withValue(PET, "cat"); |
| assertSame(cancellable, childOfCancel.cancellableAncestor); |
| Context grandChildOfCancel = childOfCancel.withValue(FOOD, "lasagna"); |
| assertSame(cancellable, grandChildOfCancel.cancellableAncestor); |
| |
| Context.CancellableContext cancellable2 = childOfCancel.withCancellation(); |
| assertSame(cancellable, cancellable2.cancellableAncestor); |
| Context childOfCancellable2 = cancellable2.withValue(PET, "dog"); |
| assertSame(cancellable2, childOfCancellable2.cancellableAncestor); |
| } |
| |
| @Test |
| public void cancellableAncestorFork() { |
| Context.CancellableContext cancellable = Context.current().withCancellation(); |
| Context fork = cancellable.fork(); |
| assertNull(fork.cancellableAncestor); |
| } |
| |
| @Test |
| public void cancellableContext_closeCancelsWithNullCause() { |
| Context.CancellableContext cancellable = Context.current().withCancellation(); |
| cancellable.close(); |
| assertTrue(cancellable.isCancelled()); |
| assertNull(cancellable.cancellationCause()); |
| } |
| |
| @Test |
| public void errorWhenAncestryLengthLong() { |
| final AtomicReference<LogRecord> logRef = new AtomicReference<>(); |
| 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.class.getName()); |
| try { |
| logger.addHandler(handler); |
| Context ctx = Context.current(); |
| for (int i = 0; i < Context.CONTEXT_DEPTH_WARN_THRESH ; i++) { |
| assertNull(logRef.get()); |
| ctx = ctx.fork(); |
| } |
| ctx.fork(); |
| assertNotNull(logRef.get()); |
| assertNotNull(logRef.get().getThrown()); |
| assertEquals(Level.SEVERE, logRef.get().getLevel()); |
| } finally { |
| logger.removeHandler(handler); |
| } |
| } |
| |
| // UsedReflectively |
| public static final class LoadMeWithStaticTestingClassLoader implements Runnable { |
| @Override |
| public void run() { |
| Logger logger = Logger.getLogger(Context.class.getName()); |
| logger.setLevel(Level.ALL); |
| Handler handler = new Handler() { |
| @Override |
| public void publish(LogRecord record) { |
| Context ctx = Context.current(); |
| Context previous = ctx.attach(); |
| ctx.detach(previous); |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public void close() throws SecurityException { |
| } |
| }; |
| logger.addHandler(handler); |
| |
| try { |
| assertNotNull(Context.ROOT); |
| } finally { |
| logger.removeHandler(handler); |
| } |
| } |
| } |
| |
| /** Allows more precise catch blocks than plain Error to avoid catching AssertionError. */ |
| private static final class TestError extends Error {} |
| } |