Skip to content

Commit 8486286

Browse files
authored
context: pluggable Storage mechanism. (grpc#2461)
Currently Context propagate in-thread by its own ThreadLocal, and cross-thread propagation must be done with the mechanisms provied by gRPC Context. However, there are frameworks (e.g. what we are using inside Google) which have already established context-propagation mechanisms. If gRPC Context may ride on top of them, it would be propagated automatically without additional effort from the application. The Storage API allows gRPC Context to be attached to anything. The default implementation still uses its own ThreadLocal. If an override implementation is present, gRPC Context will use it.
1 parent f15ed05 commit 8486286

File tree

3 files changed

+176
-57
lines changed

3 files changed

+176
-57
lines changed

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

Lines changed: 97 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,32 @@
4949
* <li>Local and distributed tracing information.</li>
5050
* </ul>
5151
*
52-
* <p>Context objects make their state available by being attached to the executing thread using
53-
* a {@link ThreadLocal}. The context object bound to a thread is considered {@link #current()}.
54-
* Context objects are immutable and inherit state from their parent. To add or overwrite the
55-
* current state a new context object must be created and then attached to the thread replacing the
56-
* previously bound context. For example:
52+
* <p>A Context object can be {@link #attach attached} to the {@link Storage}, which effectively
53+
* forms a <b>scope</b> for the context. The scope is bound to the current thread. Within a scope,
54+
* its Context is accessible even across API boundaries, through {@link #current}. The scope is
55+
* later exited by {@link #detach detaching} the Context.
56+
*
57+
* <p>Context objects are immutable and inherit state from their parent. To add or overwrite the
58+
* current state a new context object must be created and then attached, replacing the previously
59+
* bound context. For example:
60+
*
5761
* <pre>
5862
* Context withCredential = Context.current().withValue(CRED_KEY, cred);
5963
* executorService.execute(withCredential.wrap(new Runnable() {
6064
* public void run() {
6165
* readUserRecords(userId, CRED_KEY.get());
6266
* }
6367
* }));
64-
6568
* </pre>
6669
*
67-
*
68-
* <p>Contexts are also used to represent a scoped unit of work. When the unit of work is
69-
* done the context can be cancelled. This cancellation will also cascade to all descendant
70-
* contexts. You can add a {@link CancellationListener} to a context to be notified when it or
71-
* one of its ancestors has been cancelled. Cancellation does not release the state stored by
72-
* a context and it's perfectly valid to {@link #attach()} an already cancelled context to a
73-
* thread to make it current. To cancel a context (and its descendants) you first create a
74-
* {@link CancellableContext} and when you need to signal cancellation call
75-
* {@link CancellableContext#cancel} or {@link CancellableContext#detachAndCancel}. For example:
70+
* <p>Contexts are also used to represent a scoped unit of work. When the unit of work is done the
71+
* context can be cancelled. This cancellation will also cascade to all descendant contexts. You can
72+
* add a {@link CancellationListener} to a context to be notified when it or one of its ancestors
73+
* has been cancelled. Cancellation does not release the state stored by a context and it's
74+
* perfectly valid to {@link #attach()} an already cancelled context to make it current. To cancel a
75+
* context (and its descendants) you first create a {@link CancellableContext} and when you need to
76+
* signal cancellation call {@link CancellableContext#cancel} or {@link
77+
* CancellableContext#detachAndCancel}. For example:
7678
* <pre>
7779
* CancellableContext withCancellation = Context.current().withCancellation();
7880
* try {
@@ -110,20 +112,42 @@ public class Context {
110112
private static final Key<Deadline> DEADLINE_KEY = new Key<Deadline>("deadline");
111113

112114
/**
113-
* The logical root context which is {@link #current()} if no other context is bound. This context
115+
* The logical root context which is the ultimate ancestor of all contexts. This context
114116
* is not cancellable and so will not cascade cancellation or retain listeners.
117+
*
118+
* <p>Never assume this is the default context for new threads, because {@link Storage} may define
119+
* a default context that is different from ROOT.
115120
*/
116121
public static final Context ROOT = new Context(null);
117122

118-
/**
119-
* Currently bound context.
120-
*/
121-
private static final ThreadLocal<Context> localContext = new ThreadLocal<Context>() {
122-
@Override
123-
protected Context initialValue() {
124-
return ROOT;
123+
private static Storage storage;
124+
125+
private static synchronized Storage initializeStorage() {
126+
if (storage != null) {
127+
return storage;
128+
}
129+
try {
130+
Class<?> clazz = Class.forName("io.grpc.ContextStorageOverride");
131+
storage = (Storage) clazz.newInstance();
132+
return storage;
133+
} catch (ClassNotFoundException e) {
134+
log.log(Level.FINE, "Storage override doesn't exist. Using default.", e);
135+
} catch (InstantiationException e) {
136+
throw new RuntimeException("Failed to initialize Storage implementation", e);
137+
} catch (IllegalAccessException e) {
138+
throw new RuntimeException("Failed to initialize Storage implementation", e);
139+
}
140+
storage = new ThreadLocalContextStorage();
141+
return storage;
142+
}
143+
144+
// For testing
145+
static Storage storage() {
146+
if (storage == null) {
147+
return initializeStorage();
125148
}
126-
};
149+
return storage;
150+
}
127151

128152
/**
129153
* Create a {@link Key} with the given debug name. Multiple different keys may have the same name;
@@ -142,15 +166,14 @@ public static <T> Key<T> keyWithDefault(String name, T defaultValue) {
142166
}
143167

144168
/**
145-
* Return the context associated with the current thread, will never return {@code null} as
146-
* the {@link #ROOT} context is implicitly associated with all threads.
169+
* Return the context associated with the current scope, will never return {@code null}.
147170
*
148171
* <p>Will never return {@link CancellableContext} even if one is attached, instead a
149172
* {@link Context} is returned with the same properties and lifetime. This is to avoid
150173
* code stealing the ability to cancel arbitrarily.
151174
*/
152175
public static Context current() {
153-
Context current = localContext.get();
176+
Context current = storage().current();
154177
if (current == null) {
155178
return ROOT;
156179
}
@@ -324,34 +347,29 @@ boolean canBeCancelled() {
324347
}
325348

326349
/**
327-
* Attach this context to the thread and make it {@link #current}, the previously current context
328-
* is returned. It is allowed to attach contexts where {@link #isCancelled()} is {@code true}.
350+
* Attach this context, thus enter a new scope within which this context is {@link #current}. The
351+
* previously current context is returned. It is allowed to attach contexts where {@link
352+
* #isCancelled()} is {@code true}.
329353
*
330354
* <p>Instead of using {@link #attach()} & {@link #detach(Context)} most use-cases are better
331-
* served by using the {@link #run(Runnable)} or {@link #call(java.util.concurrent.Callable)}
332-
* to execute work immediately within a context. If work needs to be done in other threads
333-
* it is recommended to use the 'wrap' methods or to use a propagating executor.
355+
* served by using the {@link #run(Runnable)} or {@link #call(java.util.concurrent.Callable)} to
356+
* execute work immediately within a context's scope. If work needs to be done in other threads it
357+
* is recommended to use the 'wrap' methods or to use a propagating executor.
334358
*/
335359
public Context attach() {
336360
Context previous = current();
337-
localContext.set(this);
361+
storage().attach(this);
338362
return previous;
339363
}
340364

341365
/**
342-
* Detach the current context from the thread and attach the provided replacement. If this
343-
* context is not {@link #current()} a SEVERE message will be logged but the context to attach
344-
* will still be bound.
366+
* Detach the current context and attach the provided replacement which should be the context of
367+
* the outer scope, thus exit the current scope. If this context is not {@link #current()} a
368+
* SEVERE message will be logged but the context to attach will still be bound.
345369
*/
346370
public void detach(Context toAttach) {
347371
checkNotNull(toAttach, "toAttach");
348-
if (toAttach.attach() != this) {
349-
// Log a severe message instead of throwing an exception as the context to attach is assumed
350-
// to be the correct one and the unbalanced state represents a coding mistake in a lower
351-
// layer in the stack that cannot be recovered from here.
352-
log.log(Level.SEVERE, "Context was not attached when detaching",
353-
new Throwable().fillInStackTrace());
354-
}
372+
storage().detach(this, toAttach);
355373
}
356374

357375
// Visible for testing
@@ -709,7 +727,7 @@ public boolean cancel(Throwable cause) {
709727
}
710728

711729
/**
712-
* Cancel this context and detach it as the current context from the thread.
730+
* Cancel this context and detach it as the current context.
713731
*
714732
* @param toAttach context to make current.
715733
* @param cause of cancellation, can be {@code null}.
@@ -798,7 +816,41 @@ public String toString() {
798816
}
799817

800818
/**
801-
* Stores listener & executor pair.
819+
* Defines the mechanisms for attaching and detaching the "current" context.
820+
*
821+
* <p>The default implementation will put the current context in a {@link ThreadLocal}. If an
822+
* alternative implementation named {@code io.grpc.ContextStorageOverride} exists in the
823+
* classpath, it will be used instead of the default implementation.
824+
*
825+
* <p>This API is <a href="https://github.com/grpc/grpc-java/issues/2462">experimental</a> and
826+
* subject to change.
827+
*/
828+
public abstract static class Storage {
829+
/**
830+
* Implements {@link io.grpc.Context#attach}.
831+
*
832+
* @param toAttach the context to be attached
833+
*/
834+
public abstract void attach(Context toAttach);
835+
836+
/**
837+
* Implements {@link io.grpc.Context#detach}
838+
*
839+
* @param toDetach the context to be detached. Should be, or be equivalent to, the current
840+
* context of the current scope
841+
* @param toRestore the context to be the current. Should be, or be equivalent to, the context
842+
* of the outer scope
843+
*/
844+
public abstract void detach(Context toDetach, Context toRestore);
845+
846+
/**
847+
* Implements {@link io.grpc.Context#current}. Returns the context of the current scope.
848+
*/
849+
public abstract Context current();
850+
}
851+
852+
/**
853+
* Stores listener and executor pair.
802854
*/
803855
private class ExecutableListener implements Runnable {
804856
private final Executor executor;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2016, Google Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google Inc. nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package io.grpc;
33+
34+
import java.util.logging.Level;
35+
import java.util.logging.Logger;
36+
37+
/**
38+
* A {@link ThreadLocal}-based context storage implementation. Used by default.
39+
*/
40+
final class ThreadLocalContextStorage extends Context.Storage {
41+
private static final Logger log = Logger.getLogger(ThreadLocalContextStorage.class.getName());
42+
43+
/**
44+
* Currently bound context.
45+
*/
46+
private static final ThreadLocal<Context> localContext = new ThreadLocal<Context>();
47+
48+
@Override
49+
public void attach(Context toAttach) {
50+
localContext.set(toAttach);
51+
}
52+
53+
@Override
54+
public void detach(Context toDetach, Context toRestore) {
55+
if (current() != toDetach) {
56+
// Log a severe message instead of throwing an exception as the context to attach is assumed
57+
// to be the correct one and the unbalanced state represents a coding mistake in a lower
58+
// layer in the stack that cannot be recovered from here.
59+
log.log(Level.SEVERE, "Context was not attached when detaching",
60+
new Throwable().fillInStackTrace());
61+
}
62+
attach(toRestore);
63+
}
64+
65+
@Override
66+
public Context current() {
67+
return localContext.get();
68+
}
69+
}

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,19 @@ public void tearDown() throws Exception {
109109
}
110110

111111
@Test
112-
public void rootIsInitialContext() {
113-
assertNotNull(Context.ROOT);
114-
assertTrue(Context.ROOT.isCurrent());
115-
}
116-
117-
@Test
118-
public void rootIsAlwaysBound() throws Exception {
119-
final SettableFuture<Boolean> rootIsBound = SettableFuture.create();
112+
public void defaultContext() throws Exception {
113+
final SettableFuture<Context> contextOfNewThread = SettableFuture.create();
114+
Context contextOfThisThread = Context.ROOT.withValue(PET, "dog");
115+
contextOfThisThread.attach();
120116
new Thread(new Runnable() {
121117
@Override
122118
public void run() {
123-
rootIsBound.set(Context.current() == Context.ROOT);
119+
contextOfNewThread.set(Context.current());
124120
}
125-
}).start();
126-
assertTrue(rootIsBound.get(5, TimeUnit.SECONDS));
121+
}).start();
122+
assertNotNull(contextOfNewThread.get(5, TimeUnit.SECONDS));
123+
assertNotSame(contextOfThisThread, contextOfNewThread.get());
124+
assertSame(contextOfThisThread, Context.current());
127125
}
128126

129127
@Test
@@ -186,7 +184,7 @@ public void flush() {
186184
public void close() throws SecurityException {
187185
}
188186
};
189-
Logger logger = Logger.getLogger(Context.class.getName());
187+
Logger logger = Logger.getLogger(Context.storage().getClass().getName());
190188
try {
191189
logger.addHandler(handler);
192190
Context initial = Context.current();

0 commit comments

Comments
 (0)