context: fix race between CancellableContext and Context

The `pendingDeadline` variable is modified from the ctor of CancellableContext, but it isn't final.  The cancellation can happen before the variable is assigned.  It's generally bad practice to leak the this reference from the ctor to other threads anyways.

This code refactors the deadline calculation and scheduling so that `pendingDeadline` is modified under the lock, and the `this` reference is not exposed.

Discovered by TSAN.
This commit is contained in:
Carl Mastrangelo 2019-07-17 01:12:21 -07:00 committed by GitHub
parent b477cc2a47
commit 8a9afd618a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 37 additions and 31 deletions

View File

@ -296,11 +296,21 @@ public class Context {
* }
* </pre>
*/
public CancellableContext withDeadline(Deadline deadline,
ScheduledExecutorService scheduler) {
checkNotNull(deadline, "deadline");
public CancellableContext withDeadline(Deadline newDeadline, ScheduledExecutorService scheduler) {
checkNotNull(newDeadline, "deadline");
checkNotNull(scheduler, "scheduler");
return new CancellableContext(this, deadline, scheduler);
Deadline existingDeadline = getDeadline();
boolean scheduleDeadlineCancellation = true;
if (existingDeadline != null && existingDeadline.compareTo(newDeadline) <= 0) {
// The new deadline won't have an effect, so ignore it
newDeadline = existingDeadline;
scheduleDeadlineCancellation = false;
}
CancellableContext newCtx = new CancellableContext(this, newDeadline);
if (scheduleDeadlineCancellation) {
newCtx.setUpDeadlineCancellation(newDeadline, scheduler);
}
return newCtx;
}
/**
@ -713,37 +723,33 @@ public class Context {
/**
* Create a cancellable context that has a deadline.
*/
private CancellableContext(Context parent, Deadline deadline,
ScheduledExecutorService scheduler) {
private CancellableContext(Context parent, Deadline deadline) {
super(parent, parent.keyValueEntries);
Deadline parentDeadline = parent.getDeadline();
if (parentDeadline != null && parentDeadline.compareTo(deadline) <= 0) {
// The new deadline won't have an effect, so ignore it
deadline = parentDeadline;
} else {
// The new deadline has an effect
if (!deadline.isExpired()) {
// The parent deadline was after the new deadline so we need to install a listener
// on the new earlier deadline to trigger expiration for this context.
pendingDeadline = deadline.runOnExpiration(new Runnable() {
@Override
public void run() {
try {
cancel(new TimeoutException("context timed out"));
} catch (Throwable t) {
log.log(Level.SEVERE, "Cancel threw an exception, which should not happen", t);
}
}
}, scheduler);
} else {
// Cancel immediately if the deadline is already expired.
cancel(new TimeoutException("context timed out"));
}
}
this.deadline = deadline;
uncancellableSurrogate = new Context(this, keyValueEntries);
this.uncancellableSurrogate = new Context(this, keyValueEntries);
}
private void setUpDeadlineCancellation(Deadline deadline, ScheduledExecutorService scheduler) {
if (!deadline.isExpired()) {
final class CancelOnExpiration implements Runnable {
@Override
public void run() {
try {
cancel(new TimeoutException("context timed out"));
} catch (Throwable t) {
log.log(Level.SEVERE, "Cancel threw an exception, which should not happen", t);
}
}
}
synchronized (this) {
pendingDeadline = deadline.runOnExpiration(new CancelOnExpiration(), scheduler);
}
} else {
// Cancel immediately if the deadline is already expired.
cancel(new TimeoutException("context timed out"));
}
}
@Override
public Context attach() {