Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions context/src/main/java/io/grpc/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -657,8 +658,29 @@ private Object lookup(Key<?> key) {
* cancelled and which will propagate cancellation to its descendants. To avoid leaking memory,
* every CancellableContext must have a defined lifetime, after which it is guaranteed to be
* cancelled.
*
* <p>This class must be cancelled by either calling {@link #close} or {@link #cancel}.
* {@link #close} is equivalent to calling {@code cancel(null)}. It is safe to call the methods
* more than once, but only the first call will have any effect. Because it's safe to call the
* methods multiple times, users are encouraged to always call {@link #close} at the end of
* the operation, and disregard whether {@link #cancel} was already called somewhere else.
*
* <p>Blocking code can use the try-with-resources idiom:
* <pre>
* try (CancellableContext c = Context.current()
* .withDeadlineAfter(100, TimeUnit.MILLISECONDS, executor)) {
* Context toRestore = c.attach();
* try {
* // do some blocking work
* } finally {
* c.detach(toRestore);
* }
* }</pre>
*
* <p>Asynchronous code will have to manually track the end of the CancellableContext's lifetime,
* and cancel the context at the appropriate time.
*/
public static final class CancellableContext extends Context {
public static final class CancellableContext extends Context implements Closeable {

private final Deadline deadline;
private final Context uncancellableSurrogate;
Expand Down Expand Up @@ -739,7 +761,10 @@ public boolean isCurrent() {

/**
* Cancel this context and optionally provide a cause (can be {@code null}) for the
* cancellation. This will trigger notification of listeners.
* cancellation. This will trigger notification of listeners. It is safe to call this method
* multiple times. Only the first call will have any effect.
*
* <p>Calling {@code cancel(null)} is the same as calling {@link #close}.
*
* @return {@code true} if this context cancelled the context and notified listeners,
* {@code false} if the context was already cancelled.
Expand Down Expand Up @@ -811,6 +836,14 @@ public Deadline getDeadline() {
boolean canBeCancelled() {
return true;
}

/**
* Cleans up this object by calling {@code cancel(null)}.
*/
@Override
public void close() {
cancel(null);
}
}

/**
Expand Down
35 changes: 25 additions & 10 deletions context/src/test/java/io/grpc/ContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,14 @@ public void setUp() throws Exception {
@After
public void tearDown() throws Exception {
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");
contextOfThisThread.attach();
Context toRestore = contextOfThisThread.attach();
new Thread(new Runnable() {
@Override
public void run() {
Expand All @@ -111,16 +112,22 @@ public void run() {
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();
fork.attach();
Context.ROOT.attach();
Context toRestore1 = fork.attach();
Context toRestore2 = Context.ROOT.attach();
assertTrue(Context.ROOT.isCurrent());
fork.attach();

Context toRestore3 = fork.attach();
assertTrue(fork.isCurrent());

fork.detach(toRestore3);
Context.ROOT.detach(toRestore2);
fork.detach(toRestore1);
}

@Test
Expand Down Expand Up @@ -225,14 +232,14 @@ public void withValuesThree() {
Context base = Context.current().withValues(PET, "dog", COLOR, "blue");
Context child = base.withValues(PET, "cat", FOOD, "cheese", FAVORITE, fav);

child.attach();
Context toRestore = child.attach();

assertEquals("cat", PET.get());
assertEquals("cheese", FOOD.get());
assertEquals("blue", COLOR.get());
assertEquals(fav, FAVORITE.get());

base.attach();
child.detach(toRestore);
}

@Test
Expand All @@ -241,15 +248,15 @@ public void withValuesFour() {
Context base = Context.current().withValues(PET, "dog", COLOR, "blue");
Context child = base.withValues(PET, "cat", FOOD, "cheese", FAVORITE, fav, LUCKY, 7);

child.attach();
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());

base.attach();
child.detach(toRestore);
}

@Test
Expand Down Expand Up @@ -389,7 +396,7 @@ public void cascadingCancellationWithoutListener() {
public void cancellableContextIsAttached() {
Context.CancellableContext base = Context.current().withValue(FOOD, "fish").withCancellation();
assertFalse(base.isCurrent());
base.attach();
Context toRestore = base.attach();

Context attached = Context.current();
assertSame("fish", FOOD.get());
Expand All @@ -406,7 +413,7 @@ public void cancellableContextIsAttached() {
assertSame(t, attached.cancellationCause());
assertSame(attached, listenerNotifedContext);

Context.ROOT.attach();
base.detach(toRestore);
}

@Test
Expand Down Expand Up @@ -921,6 +928,14 @@ public void cancellableAncestorFork() {
assertNull(fork.cancellableAncestor);
}

@Test
public void cancellableContext_closeCancelsWithNullCause() throws Exception {
Context.CancellableContext cancellable = Context.current().withCancellation();
cancellable.close();
assertTrue(cancellable.isCancelled());
assertNull(cancellable.cancellationCause());
}

@Test
public void errorWhenAncestryLengthLong() {
final AtomicReference<LogRecord> logRef = new AtomicReference<LogRecord>();
Expand Down