Skip to content

Commit ce70924

Browse files
committed
Fix a race condition when adding a callback.
The following "unlucky execution" could happen: Deferred in state RUNNING. Thread A (in runCallbacks) | Thread B (in addCallbacks) complete execution of the last callback | CAS state DONE -> RUNNING fails acquire this' monitor | observe that the callback chain is empty | set state to DONE | null out callback chains | release this' monitor | | acquire this' monitor | queue the new callbacks | release this' monitor Now Thread B left the Deferred in state DONE with a callback in the chain that will never be called unless another callback is added to it. The fix consists in swapping the CAS and the acquisition of this' monitor in addCallbacks. In the unlucky timing above, this could have 2 possible outcomes: 1. Thread A acquires this' monitor first and then thread B's CAS will succeed, so B will keep executing the callback chain. 2. Thread B acquires this' monitor first and queues the new callbacks, then thread A will execute them. When swapping the CAS and the acquisition of this' monitor, the CAS becomes unnecessary and can be replaced with a regular volatile-read followed by a regular volatile-write. The purpose of the CAS was originally to avoid acquiring this' monitor when it wasn't necessary but this "optimization" wasn't even effective since the most common code path is to add a callback to a Deferred in state PENDING, and this code path always needed a failed CAS + monitor acquisition. Change-Id: I365cec225e9d15aa09280ce066bf11ce56c1d358
1 parent 69cce19 commit ce70924

File tree

1 file changed

+16
-6
lines changed

1 file changed

+16
-6
lines changed

src/Deferred.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -610,11 +610,20 @@ public <R, R2, E> Deferred<R> addCallbacks(final Callback<R, T> cb,
610610
} else if (eb == null) {
611611
throw new NullPointerException("null errback");
612612
}
613-
// If we're DONE, switch to RUNNING atomically.
614-
if (!casState(State.DONE, State.RUNNING)) {
615-
// We get here if weren't DONE or if we were DONE and another thread
616-
// raced with us and we lost the race.
617-
synchronized (this) {
613+
// We need to synchronize on `this' first before the CAS, to prevent
614+
// runCallbacks from switching our state from RUNNING to DONE right
615+
// before we add another callback.
616+
synchronized (this) {
617+
// If we're DONE, switch to RUNNING atomically.
618+
if (state == State.DONE) {
619+
// This "check-then-act" sequence is safe as this is the only code
620+
// path that transitions from DONE to RUNNING and it's synchronized.
621+
state = State.RUNNING;
622+
} else {
623+
// We get here if weren't DONE (most common code path)
624+
// -or-
625+
// if we were DONE and another thread raced with us to change the
626+
// state and we lost the race (uncommon).
618627
if (callbacks == null) {
619628
callbacks = new LinkedList<Callback>();
620629
errbacks = new LinkedList<Callback>();
@@ -625,9 +634,10 @@ public <R, R2, E> Deferred<R> addCallbacks(final Callback<R, T> cb,
625634
}
626635
callbacks.addLast(cb);
627636
errbacks.addLast(eb);
637+
return (Deferred<R>) ((Deferred) this);
628638
}
629-
return (Deferred<R>) ((Object) this);
630639
}
640+
631641
if (!doCall(result instanceof Exception ? eb : cb)) {
632642
// While we were executing the callback, another thread could have
633643
// added more callbacks. If doCall returned true, it means we're

0 commit comments

Comments
 (0)