Skip to content

Commit 6640976

Browse files
author
Louis Ryan
committed
Add deadline support and tests
Improve Javadoc
1 parent be0c033 commit 6640976

File tree

2 files changed

+151
-8
lines changed

2 files changed

+151
-8
lines changed

core/src/main/java/io/grpc/Context.java

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,68 @@
22

33
import com.google.common.base.Preconditions;
44
import com.google.common.util.concurrent.MoreExecutors;
5+
import com.google.common.util.concurrent.ThreadFactoryBuilder;
56

67
import java.io.Closeable;
78
import java.io.IOException;
89
import java.util.ArrayDeque;
910
import java.util.ArrayList;
1011
import java.util.concurrent.Callable;
1112
import java.util.concurrent.Executor;
12-
import java.util.concurrent.ExecutorService;
1313
import java.util.concurrent.Executors;
1414
import java.util.concurrent.ScheduledExecutorService;
15+
import java.util.concurrent.ScheduledFuture;
16+
import java.util.concurrent.TimeUnit;
17+
import java.util.concurrent.TimeoutException;
1518
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
1619

1720
import javax.annotation.Nullable;
1821

1922

2023
/**
21-
* --Add warning about using this too liberally--
22-
* --Add warning about using this with containers that unload Classloaders--
23-
* -- Add note that in the case of cascading closure that listeners to the parent are always
24-
* notified before listeners on the children.
24+
* A context propagation mechanism which carries deadlines, cancellation signals,
25+
* and other scoped values across API boundaries and between threads. Examples of functionality
26+
* propagated via context include:
27+
* <ul>
28+
* <li>Deadlines for a local operation or remote call.</li>
29+
* <li>Security principals & credentials.</li>
30+
* <li>Local and distributed tracing context.</li>
31+
* </ul>
32+
*
33+
* <p>Context is not intended for passing optional parameters to an API and developers should
34+
* take care to avoid excessive dependence on context when designing an API.
35+
*
36+
* <p>If Context is being used in an environment that needs to support class unloading it is the
37+
* responsibility of the application to ensure that all contexts are properly cancelled.
2538
*
2639
*/
2740
public class Context {
2841

42+
/**
43+
* Use a shared resource to retain the {@link ScheduledExecutorService} used to
44+
* implement deadline based context cancellation. This allows the executor to be
45+
* shutdown if its not in use thereby allowing Context to be unloaded.
46+
*/
47+
static final SharedResourceHolder.Resource<ScheduledExecutorService> SCHEDULER =
48+
new SharedResourceHolder.Resource<ScheduledExecutorService>() {
49+
private static final String name = "context-scheduler";
50+
@Override
51+
public ScheduledExecutorService create() {
52+
return Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder()
53+
.setNameFormat(name + "-%d").build());
54+
}
55+
56+
@Override
57+
public void close(ScheduledExecutorService instance) {
58+
instance.shutdown();
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return name;
64+
}
65+
};
66+
2967
/**
3068
* Stack of context objects which is used to record attach & detach history on a thread.
3169
*/
@@ -123,6 +161,28 @@ public CancellableContext withCancellation() {
123161
return new CancellableContext(this);
124162
}
125163

164+
/**
165+
* Create a new context which will cancel itself after an absolute deadline expressed as
166+
* nanoseconds in the {@link System#nanoTime()} clock.
167+
*
168+
* <p>It is recommended that callers only use this method when propagating a derivative of
169+
* a received existing deadline. When establishing a new deadline {@link #withDeadlineAfter}
170+
* is the better mechanism.
171+
*/
172+
public CancellableContext withDeadlineNanoTime(long deadlineNanoTime) {
173+
return withDeadlineAfter(Math.max(0, deadlineNanoTime - System.nanoTime()),
174+
TimeUnit.NANOSECONDS);
175+
}
176+
177+
/**
178+
* Create a new context which will cancel itself after the given {@code duration} from now.
179+
*/
180+
public CancellableContext withDeadlineAfter(long duration, TimeUnit unit) {
181+
Preconditions.checkArgument(duration >= 0, "duration must be greater than or equal to 0");
182+
Preconditions.checkNotNull(unit, "unit");
183+
return new CancellableContext(this, unit.toNanos(duration));
184+
}
185+
126186
/**
127187
* Create a new context which cannot be cancelled and will not propagate the cancellation of its
128188
* parent.
@@ -326,6 +386,7 @@ public static final class CancellableContext extends Context {
326386
volatile int cancelled;
327387
private volatile Throwable cause;
328388
private final Context dummy;
389+
private ScheduledFuture<?> scheduledFuture;
329390

330391

331392
private CancellableContext(Context parent) {
@@ -335,6 +396,20 @@ private CancellableContext(Context parent) {
335396
dummy = new Context(this, EMPTY_ENTRIES);
336397
}
337398

399+
private CancellableContext(Context parent, long delayNanos) {
400+
super(parent, EMPTY_ENTRIES);
401+
// Create a dummy that inherits from this to attach and detach so that you cannot retrieve a
402+
// cancellable context from Context.current()
403+
dummy = new Context(this, EMPTY_ENTRIES);
404+
scheduledFuture = SCHEDULER.create().schedule(new Runnable() {
405+
@Override
406+
public void run() {
407+
cancel(new TimeoutException("context timed out"));
408+
}
409+
}, delayNanos, TimeUnit.NANOSECONDS);
410+
}
411+
412+
338413
@Override
339414
public void attach() {
340415
dummy.attach();
@@ -371,6 +446,11 @@ public void close() throws IOException {
371446
*/
372447
public boolean cancel(@Nullable Throwable cause) {
373448
if (cancelledUpdater.compareAndSet(this, 0, 1)) {
449+
if (scheduledFuture != null) {
450+
// If we have a scheduled cancellation pending attempt to cancel it.
451+
scheduledFuture.cancel(false);
452+
scheduledFuture = null;
453+
}
374454
this.cause = cause;
375455
notifyAndClearListeners();
376456
return true;

core/src/test/java/io/grpc/ContextTest.java

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package io.grpc;
22

3-
import static org.junit.Assert.*;
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.assertNotSame;
6+
import static org.junit.Assert.assertNull;
7+
import static org.junit.Assert.assertNotNull;
8+
import static org.junit.Assert.assertSame;
9+
import static org.junit.Assert.assertTrue;
10+
import static org.junit.Assert.fail;
411

512
import com.google.common.util.concurrent.MoreExecutors;
613

@@ -10,13 +17,14 @@
1017
import org.junit.runner.RunWith;
1118
import org.junit.runners.JUnit4;
1219

13-
import java.io.Closeable;
1420
import java.util.concurrent.CountDownLatch;
1521
import java.util.concurrent.ExecutorService;
1622
import java.util.concurrent.Executors;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.TimeoutException;
1725

1826
/**
19-
* TODO: http://go/java-style#javadoc
27+
* Tests for {@link Context}.
2028
*/
2129
@RunWith(JUnit4.class)
2230
public class ContextTest {
@@ -27,10 +35,12 @@ public class ContextTest {
2735
private static final Context.Key<Object> OBSERVED = Context.key("observed");
2836

2937
private boolean listenerNotified;
38+
private CountDownLatch deadlineLatch = new CountDownLatch(1);
3039
private Context.CancellationListener cancellationListener = new Context.CancellationListener() {
3140
@Override
3241
public void cancelled(Context context) {
3342
listenerNotified = true;
43+
deadlineLatch.countDown();
3444
}
3545
};
3646

@@ -253,6 +263,7 @@ public void typicalTryCatchFinallyHandling() throws Exception {
253263
assertNotNull(base.cause());
254264
}
255265

266+
256267
/*
257268
public void testTryWithResource() throws Exception {
258269
Context.CancellableContext base = Context.current().withCancellation();
@@ -280,4 +291,56 @@ public void testTryWithResource() throws Exception {
280291
assertNotNull(base.cause());
281292
}
282293
*/
294+
295+
@Test
296+
public void absoluteDeadlineTriggersAndPropagates() throws Exception {
297+
Context base = Context.current().withDeadlineNanoTime(System.nanoTime() +
298+
TimeUnit.SECONDS.toNanos(1));
299+
Context child = base.withValue(FOOD, "lasagna");
300+
child.addListener(cancellationListener, MoreExecutors.directExecutor());
301+
assertFalse(base.isCancelled());
302+
assertFalse(child.isCancelled());
303+
deadlineLatch.await(2, TimeUnit.SECONDS);
304+
assertTrue(base.isCancelled());
305+
assertTrue(base.cause() instanceof TimeoutException);
306+
assertTrue(listenerNotified);
307+
assertTrue(child.isCancelled());
308+
assertSame(base.cause(), child.cause());
309+
}
310+
311+
@Test
312+
public void relativeDeadlineTriggersAndPropagates() throws Exception {
313+
Context base = Context.current().withDeadlineAfter(1, TimeUnit.SECONDS);
314+
Context child = base.withValue(FOOD, "lasagna");
315+
child.addListener(cancellationListener, MoreExecutors.directExecutor());
316+
assertFalse(base.isCancelled());
317+
assertFalse(child.isCancelled());
318+
deadlineLatch.await(2, TimeUnit.SECONDS);
319+
assertTrue(base.isCancelled());
320+
assertTrue(base.cause() instanceof TimeoutException);
321+
assertTrue(listenerNotified);
322+
assertTrue(child.isCancelled());
323+
assertSame(base.cause(), child.cause());
324+
}
325+
326+
@Test
327+
public void innerDeadlineCompletesBeforeOuter() throws Exception {
328+
Context base = Context.current().withDeadlineAfter(2, TimeUnit.SECONDS);
329+
Context child = base.withDeadlineAfter(1, TimeUnit.SECONDS);
330+
child.addListener(cancellationListener, MoreExecutors.directExecutor());
331+
assertFalse(base.isCancelled());
332+
assertFalse(child.isCancelled());
333+
deadlineLatch.await(2, TimeUnit.SECONDS);
334+
assertFalse(base.isCancelled());
335+
assertTrue(listenerNotified);
336+
assertTrue(child.isCancelled());
337+
assertTrue(child.cause() instanceof TimeoutException);
338+
339+
deadlineLatch = new CountDownLatch(1);
340+
base.addListener(cancellationListener, MoreExecutors.directExecutor());
341+
deadlineLatch.await(2, TimeUnit.SECONDS);
342+
assertTrue(base.isCancelled());
343+
assertTrue(base.cause() instanceof TimeoutException);
344+
assertNotSame(base.cause(), child.cause());
345+
}
283346
}

0 commit comments

Comments
 (0)