context: compress cancellation ancestor chain (#3372)

Now that we have the copy of write keyvalue store (#3368), there
is no need to keep the full parent chain. We only need a
reference to the nearest cancellable ancestor. This optimization
should in theory make cancellations more efficient and also make
our data structs more GC friendly.
This commit is contained in:
zpencer 2017-08-22 09:20:31 -07:00 committed by GitHub
parent 608b95547b
commit e707d95d77
2 changed files with 85 additions and 16 deletions

View File

@ -179,20 +179,20 @@ public class Context {
return current; return current;
} }
private final Context parent;
private final boolean cascadesCancellation; private final boolean cascadesCancellation;
private ArrayList<ExecutableListener> listeners; private ArrayList<ExecutableListener> listeners;
private CancellationListener parentListener = new ParentListener(); private CancellationListener parentListener = new ParentListener();
private final boolean canBeCancelled; private final boolean canBeCancelled;
final CancellableContext cancellableAncestor;
final PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries; final PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries;
/** /**
* Construct a context that cannot be cancelled and will not cascade cancellation from its parent. * Construct a context that cannot be cancelled and will not cascade cancellation from its parent.
*/ */
private Context(Context parent) { private Context(PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) {
this.parent = parent; cancellableAncestor = null;
// Not inheriting cancellation implies not inheriting a deadline too. // Not inheriting cancellation implies not inheriting a deadline too.
keyValueEntries = parent.keyValueEntries.put(DEADLINE_KEY, null); this.keyValueEntries = keyValueEntries.put(DEADLINE_KEY, null);
cascadesCancellation = false; cascadesCancellation = false;
canBeCancelled = false; canBeCancelled = false;
} }
@ -202,10 +202,10 @@ public class Context {
* it is cancellable. * it is cancellable.
*/ */
private Context(Context parent, PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) { private Context(Context parent, PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries) {
this.parent = parent; cancellableAncestor = cancellableAncestor(parent);
this.keyValueEntries = keyValueEntries; this.keyValueEntries = keyValueEntries;
cascadesCancellation = true; cascadesCancellation = true;
canBeCancelled = this.parent != null && this.parent.canBeCancelled; canBeCancelled = cancellableAncestor != null;
} }
/** /**
@ -216,7 +216,7 @@ public class Context {
Context parent, Context parent,
PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries, PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries,
boolean isCancellable) { boolean isCancellable) {
this.parent = parent; cancellableAncestor = cancellableAncestor(parent);
this.keyValueEntries = keyValueEntries; this.keyValueEntries = keyValueEntries;
cascadesCancellation = true; cascadesCancellation = true;
canBeCancelled = isCancellable; canBeCancelled = isCancellable;
@ -230,7 +230,7 @@ public class Context {
PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries, PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries,
boolean cascadesCancellation, boolean cascadesCancellation,
boolean isCancellable) { boolean isCancellable) {
this.parent = parent; cancellableAncestor = cancellableAncestor(parent);
this.keyValueEntries = keyValueEntries; this.keyValueEntries = keyValueEntries;
this.cascadesCancellation = cascadesCancellation; this.cascadesCancellation = cascadesCancellation;
canBeCancelled = isCancellable; canBeCancelled = isCancellable;
@ -374,7 +374,7 @@ public class Context {
* cancellation. * cancellation.
*/ */
public Context fork() { public Context fork() {
return new Context(this); return new Context(keyValueEntries);
} }
boolean canBeCancelled() { boolean canBeCancelled() {
@ -438,10 +438,10 @@ public class Context {
* Is this context cancelled. * Is this context cancelled.
*/ */
public boolean isCancelled() { public boolean isCancelled() {
if (parent == null || !cascadesCancellation) { if (cancellableAncestor == null || !cascadesCancellation) {
return false; return false;
} else { } else {
return parent.isCancelled(); return cancellableAncestor.isCancelled();
} }
} }
@ -454,10 +454,10 @@ public class Context {
* should generally assume that it has already been handled and logged properly. * should generally assume that it has already been handled and logged properly.
*/ */
public Throwable cancellationCause() { public Throwable cancellationCause() {
if (parent == null || !cascadesCancellation) { if (cancellableAncestor == null || !cascadesCancellation) {
return null; return null;
} else { } else {
return parent.cancellationCause(); return cancellableAncestor.cancellationCause();
} }
} }
@ -488,7 +488,9 @@ public class Context {
// we can cascade listener notification. // we can cascade listener notification.
listeners = new ArrayList<ExecutableListener>(); listeners = new ArrayList<ExecutableListener>();
listeners.add(executableListener); listeners.add(executableListener);
parent.addListener(parentListener, DirectExecutor.INSTANCE); if (cancellableAncestor != null) {
cancellableAncestor.addListener(parentListener, DirectExecutor.INSTANCE);
}
} else { } else {
listeners.add(executableListener); listeners.add(executableListener);
} }
@ -516,7 +518,9 @@ public class Context {
} }
// We have no listeners so no need to listen to our parent // We have no listeners so no need to listen to our parent
if (listeners.isEmpty()) { if (listeners.isEmpty()) {
parent.removeListener(parentListener); if (cancellableAncestor != null) {
cancellableAncestor.removeListener(parentListener);
}
listeners = null; listeners = null;
} }
} }
@ -553,7 +557,9 @@ public class Context {
tmpListeners.get(i).deliver(); tmpListeners.get(i).deliver();
} }
} }
parent.removeListener(parentListener); if (cancellableAncestor != null) {
cancellableAncestor.removeListener(parentListener);
}
} }
// Used in tests to ensure that listeners are defined and released when cancellation cascades. // Used in tests to ensure that listeners are defined and released when cancellation cascades.
@ -1000,4 +1006,20 @@ public class Context {
return "Context.DirectExecutor"; return "Context.DirectExecutor";
} }
} }
/**
* Returns {@code parent} if it is a {@link CancellableContext}, otherwise returns the parent's
* {@link #cancellableAncestor}.
*/
static CancellableContext cancellableAncestor(Context parent) {
if (parent == null || !parent.canBeCancelled()) {
return null;
}
if (parent instanceof CancellableContext) {
return (CancellableContext) parent;
}
// The parent simply cascades cancellations.
// Bypass the parent and reference the ancestor directly.
return parent.cancellableAncestor;
}
} }

View File

@ -16,6 +16,7 @@
package io.grpc; package io.grpc;
import static io.grpc.Context.cancellableAncestor;
import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -875,6 +876,52 @@ public class ContextTest {
} }
} }
@Test
public void cancellableAncestorTest() {
assertEquals(null, cancellableAncestor(null));
Context c = Context.current();
assertFalse(c.canBeCancelled());
assertEquals(null, 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);
}
// UsedReflectively // UsedReflectively
public static final class LoadMeWithStaticTestingClassLoader implements Runnable { public static final class LoadMeWithStaticTestingClassLoader implements Runnable {
@Override @Override