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
30 changes: 28 additions & 2 deletions context/src/main/java/io/grpc/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public class Context {
private static final PersistentHashArrayMappedTrie<Key<?>, Object> EMPTY_ENTRIES =
new PersistentHashArrayMappedTrie<Key<?>, Object>();

// Long chains of contexts are suspicious and usually indicate a misuse of Context.
// The threshold is arbitrarily chosen.
// VisibleForTesting
static final int CONTEXT_DEPTH_WARN_THRESH = 1000;

/**
* The logical root context which is the ultimate ancestor of all contexts. This context
* is not cancellable and so will not cascade cancellation or retain listeners.
Expand Down Expand Up @@ -181,13 +186,17 @@ public static Context current() {
private CancellationListener parentListener = new ParentListener();
final CancellableContext cancellableAncestor;
final PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries;
// The number parents between this context and the root context.
final int generation;

/**
* Construct a context that cannot be cancelled and will not cascade cancellation from its parent.
*/
private Context(PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) {
private Context(PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries, int generation) {
cancellableAncestor = null;
this.keyValueEntries = keyValueEntries;
this.generation = generation;
validateGeneration(generation);
}

/**
Expand All @@ -197,6 +206,8 @@ private Context(PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) {
private Context(Context parent, PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) {
cancellableAncestor = cancellableAncestor(parent);
this.keyValueEntries = keyValueEntries;
this.generation = parent == null ? 0 : parent.generation + 1;
validateGeneration(generation);
}

/**
Expand Down Expand Up @@ -337,7 +348,7 @@ public <V1, V2, V3, V4> Context withValues(Key<V1> k1, V1 v1, Key<V2> k2, V2 v2,
* cancellation.
*/
public Context fork() {
return new Context(keyValueEntries);
return new Context(keyValueEntries, generation + 1);
}

boolean canBeCancelled() {
Expand Down Expand Up @@ -993,4 +1004,19 @@ static CancellableContext cancellableAncestor(Context parent) {
// Bypass the parent and reference the ancestor directly (may be null).
return parent.cancellableAncestor;
}

/**
* If the ancestry chain length is unreasonably long, then print an error to the log and record
* the stack trace.
*/
private static void validateGeneration(int generation) {
if (generation == CONTEXT_DEPTH_WARN_THRESH) {
log.log(
Level.SEVERE,
"Context ancestry chain length is abnormally long. "
+ "This suggests an error in application code. "
+ "Length exceeded: " + CONTEXT_DEPTH_WARN_THRESH,
new Exception());
}
}
}
34 changes: 34 additions & 0 deletions context/src/test/java/io/grpc/ContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,40 @@ public void cancellableAncestorFork() {
assertNull(fork.cancellableAncestor);
}

@Test
public void errorWhenAncestryLengthLong() {
final AtomicReference<LogRecord> logRef = new AtomicReference<LogRecord>();
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 = 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
Expand Down