| /* | |
| * 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 {} | |
| } |