Skip to content

Commit fb21845

Browse files
authored
Fix deadlock caused by spurious wakeups. (#51)
1 parent ff39159 commit fb21845

File tree

10 files changed

+250
-55
lines changed

10 files changed

+250
-55
lines changed

core/src/main/kotlin/org/pastalab/fray/core/RunContext.kt

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,11 @@ class RunContext(val config: Configuration) {
123123
thread.pendingOperation =
124124
when (pendingOperation) {
125125
is ObjectWaitBlock -> {
126-
ObjectWakeBlocked(pendingOperation.o, false)
126+
ObjectWakeBlocked(pendingOperation.o, true)
127127
}
128128
is ConditionAwaitBlocked -> {
129-
ConditionWakeBlocked(pendingOperation.condition, false)
129+
ConditionWakeBlocked(
130+
pendingOperation.condition, pendingOperation.canInterrupt, true)
130131
}
131132
is ObjectWakeBlocked -> {
132133
pendingOperation
@@ -311,6 +312,7 @@ class RunContext(val config: Configuration) {
311312
context.state = ThreadState.Enabled
312313
scheduleNextOperation(true)
313314

315+
context.pendingOperation = ThreadResumeOperation(true)
314316
// First we need to check if current thread is interrupted.
315317
if (canInterrupt) {
316318
context.checkInterrupt()
@@ -379,11 +381,11 @@ class RunContext(val config: Configuration) {
379381
// We will unblock here only if the scheduler
380382
// decides to run it.
381383
while (context.state != ThreadState.Running) {
382-
syncManager.signal(lockObject)
383384
try {
384385
if (context.interruptSignaled) {
385386
Thread.interrupted()
386387
}
388+
syncManager.signal(lockObject)
387389
if (waitingObject is Condition) {
388390
// TODO(aoli): Is this necessary?
389391
if (canInterrupt) {
@@ -400,19 +402,19 @@ class RunContext(val config: Configuration) {
400402
}
401403
// If a thread is enabled, the lock must be available.
402404
assert(lockManager.lock(lockObject, context, false, true, false))
405+
val pendingOperation = context.pendingOperation
406+
assert(pendingOperation is ThreadResumeOperation)
403407
if (canInterrupt) {
404408
context.checkInterrupt()
405409
}
406-
val pendingOperation = context.pendingOperation
407-
assert(pendingOperation is ThreadResumeOperation)
408410
return (pendingOperation as ThreadResumeOperation).noTimeout
409411
}
410412

411413
fun threadInterrupt(t: Thread) {
412414
val context = registeredThreads[t.id]!!
413415
context.interruptSignaled = true
414416

415-
if (context.state == ThreadState.Running || context.state == ThreadState.Enabled) {
417+
if (context.state == ThreadState.Running) {
416418
return
417419
}
418420

@@ -422,24 +424,31 @@ class RunContext(val config: Configuration) {
422424
is ObjectWaitBlock -> {
423425
if (lockManager.objectWaitUnblockedWithoutNotify(
424426
pendingOperation.o, pendingOperation.o, context, false)) {
425-
syncManager.createWait(pendingOperation.o, 1)
426427
waitingObject = pendingOperation.o
427428
}
428429
}
430+
is ObjectWakeBlocked -> {
431+
if (context.state == ThreadState.Enabled) {
432+
waitingObject = pendingOperation.o
433+
}
434+
}
435+
is ConditionWakeBlocked -> {
436+
if (pendingOperation.canInterrupt && context.state == ThreadState.Enabled) {
437+
waitingObject = lockManager.lockFromCondition(pendingOperation.condition)
438+
}
439+
}
429440
is ConditionAwaitBlocked -> {
430441
if (pendingOperation.canInterrupt) {
431442
val lock = lockManager.lockFromCondition(pendingOperation.condition)
432443
if (lockManager.objectWaitUnblockedWithoutNotify(
433444
pendingOperation.condition, lock, context, false)) {
434-
syncManager.createWait(lock, 1)
435445
waitingObject = lock
436446
}
437447
}
438448
}
439449
is CountDownLatchAwaitBlocking -> {
440450
context.pendingOperation = ThreadResumeOperation(true)
441451
context.state = ThreadState.Enabled
442-
syncManager.createWait(pendingOperation.latch, 1)
443452
waitingObject = pendingOperation.latch
444453
}
445454
is ParkBlocked -> {
@@ -452,6 +461,7 @@ class RunContext(val config: Configuration) {
452461
}
453462

454463
if (waitingObject != null) {
464+
syncManager.createWait(waitingObject, 1)
455465
registeredThreads[Thread.currentThread().id]!!.pendingOperation =
456466
InterruptPendingOperation(waitingObject)
457467
}
@@ -524,9 +534,13 @@ class RunContext(val config: Configuration) {
524534
val context = registeredThreads[t]!!
525535
lockManager.addWakingThread(lockObject, context)
526536
if (waitingObject == lockObject) {
527-
context.pendingOperation = ObjectWakeBlocked(waitingObject, false)
537+
context.pendingOperation = ObjectWakeBlocked(waitingObject, true)
528538
} else {
529-
context.pendingOperation = ConditionWakeBlocked(waitingObject as Condition, false)
539+
context.pendingOperation =
540+
ConditionWakeBlocked(
541+
waitingObject as Condition,
542+
(context.pendingOperation as ConditionAwaitBlocked).canInterrupt,
543+
true)
530544
}
531545
it.remove(t)
532546
if (it.size == 0) {
@@ -554,9 +568,13 @@ class RunContext(val config: Configuration) {
554568
// We cannot enable the thread immediately because
555569
// the thread is still waiting for the monitor lock.
556570
if (waitingObject == lockObject) {
557-
context.pendingOperation = ObjectWakeBlocked(waitingObject, false)
571+
context.pendingOperation = ObjectWakeBlocked(waitingObject, true)
558572
} else {
559-
context.pendingOperation = ConditionWakeBlocked(waitingObject as Condition, false)
573+
context.pendingOperation =
574+
ConditionWakeBlocked(
575+
waitingObject as Condition,
576+
(context.pendingOperation as ConditionAwaitBlocked).canInterrupt,
577+
true)
560578
}
561579
lockManager.addWakingThread(lockObject, context)
562580
}
@@ -828,10 +846,12 @@ class RunContext(val config: Configuration) {
828846
val pendingOperation = thread.pendingOperation
829847
when (pendingOperation) {
830848
is ObjectWaitBlock -> {
831-
thread.pendingOperation = ObjectWakeBlocked(pendingOperation.o, false)
849+
thread.pendingOperation = ObjectWakeBlocked(pendingOperation.o, true)
832850
}
833851
is ConditionAwaitBlocked -> {
834-
thread.pendingOperation = ConditionWakeBlocked(pendingOperation.condition, false)
852+
thread.pendingOperation =
853+
ConditionWakeBlocked(
854+
pendingOperation.condition, pendingOperation.canInterrupt, true)
835855
}
836856
is CountDownLatchAwaitBlocking -> {
837857
val releasedThreads = latchManager.release(pendingOperation.latch)
@@ -917,6 +937,9 @@ class RunContext(val config: Configuration) {
917937
.toList()
918938
.filter { it.state == ThreadState.Enabled }
919939
.sortedBy { it.thread.id }
940+
if (mainExiting && (currentThreadId == mainThreadId || enabledOperations.size > 1)) {
941+
enabledOperations = enabledOperations.filter { it.thread.id != mainThreadId }
942+
}
920943
}
921944

922945
if (enabledOperations.isEmpty()) {

core/src/main/kotlin/org/pastalab/fray/core/RuntimeDelegate.kt

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -615,11 +615,11 @@ class RuntimeDelegate(val context: RunContext) : org.pastalab.fray.runtime.Deleg
615615
}
616616

617617
override fun onLatchAwaitTimeout(latch: CountDownLatch, timeout: Long, unit: TimeUnit): Boolean {
618-
if (onLatchAwaitImpl(latch, true)) {
619-
onSkipMethodDone("Latch.await")
620-
return latch.await(timeout, unit)
621-
}
622618
try {
619+
if (onLatchAwaitImpl(latch, true)) {
620+
onSkipMethodDone("Latch.await")
621+
return latch.await(timeout, unit)
622+
}
623623
latch.await()
624624
} catch (e: InterruptedException) {
625625
// Do nothing
@@ -692,8 +692,11 @@ class RuntimeDelegate(val context: RunContext) : org.pastalab.fray.runtime.Deleg
692692
LockSupport.parkNanos(nanos)
693693
return
694694
}
695-
LockSupport.park()
696-
onThreadParkDoneImpl(true)
695+
try {
696+
LockSupport.park()
697+
} finally {
698+
onThreadParkDoneImpl(true)
699+
}
697700
}
698701

699702
override fun onThreadParkUntil(deadline: Long) {
@@ -702,8 +705,11 @@ class RuntimeDelegate(val context: RunContext) : org.pastalab.fray.runtime.Deleg
702705
LockSupport.parkUntil(deadline)
703706
return
704707
}
705-
LockSupport.park()
706-
onThreadParkDoneImpl(true)
708+
try {
709+
LockSupport.park()
710+
} finally {
711+
onThreadParkDoneImpl(true)
712+
}
707713
}
708714

709715
override fun onThreadParkNanosWithBlocker(blocker: Any?, nanos: Long) {
@@ -712,51 +718,81 @@ class RuntimeDelegate(val context: RunContext) : org.pastalab.fray.runtime.Deleg
712718
LockSupport.parkNanos(blocker, nanos)
713719
return
714720
}
715-
LockSupport.park()
716-
onThreadParkDoneImpl(true)
721+
try {
722+
LockSupport.park()
723+
} finally {
724+
onThreadParkDoneImpl(true)
725+
}
717726
}
718727

719728
override fun onThreadParkUntilWithBlocker(blocker: Any?, deadline: Long) {
720729
if (onThreadParkImpl()) {
721-
onSkipMethodDone("Thread.park")
722-
LockSupport.parkUntil(blocker, deadline)
730+
try {
731+
LockSupport.parkUntil(blocker, deadline)
732+
} finally {
733+
onSkipMethodDone("Thread.park")
734+
}
723735
return
724736
}
725-
LockSupport.park()
726-
onThreadParkDoneImpl(true)
737+
try {
738+
LockSupport.park()
739+
} finally {
740+
onThreadParkDoneImpl(true)
741+
}
727742
}
728743

729744
override fun onConditionAwaitTime(o: Condition, time: Long, unit: TimeUnit): Boolean {
730-
if (onConditionAwaitImpl(o, true, true)) {
731-
val result = o.await(time, unit)
732-
onSkipMethodDone("Condition.await")
733-
return result
745+
try {
746+
if (onConditionAwaitImpl(o, true, true)) {
747+
try {
748+
return o.await(time, unit)
749+
} finally {
750+
onSkipMethodDone("Condition.await")
751+
}
752+
}
753+
o.await()
754+
} catch (e: Throwable) {
755+
onConditionAwaitDoneImpl(o, true)
756+
throw e
734757
}
735-
o.await()
736758
return onConditionAwaitDoneImpl(o, true)
737759
}
738760

739761
override fun onConditionAwaitNanos(o: Condition, nanos: Long): Long {
740-
if (onConditionAwaitImpl(o, true, true)) {
741-
val result = o.awaitNanos(nanos)
742-
onSkipMethodDone("Condition.await")
743-
return result
762+
try {
763+
if (onConditionAwaitImpl(o, true, true)) {
764+
try {
765+
return o.awaitNanos(nanos)
766+
} finally {
767+
onSkipMethodDone("Condition.await")
768+
}
769+
}
770+
o.await()
771+
} catch (e: Throwable) {
772+
onConditionAwaitDoneImpl(o, true)
773+
throw e
744774
}
745-
o.await()
746-
if (onConditionAwaitDoneImpl(o, true)) {
747-
return 0
775+
return if (onConditionAwaitDoneImpl(o, true)) {
776+
0
748777
} else {
749-
return nanos - 1
778+
nanos - 1
750779
}
751780
}
752781

753782
override fun onConditionAwaitUntil(o: Condition, deadline: Date): Boolean {
754-
if (onConditionAwaitImpl(o, true, true)) {
755-
val result = o.awaitUntil(deadline)
756-
onSkipMethodDone("Condition.await")
757-
return result
783+
try {
784+
if (onConditionAwaitImpl(o, true, true)) {
785+
try {
786+
return o.awaitUntil(deadline)
787+
} finally {
788+
onSkipMethodDone("Condition.await")
789+
}
790+
}
791+
o.await()
792+
} catch (e: Throwable) {
793+
onConditionAwaitDoneImpl(o, true)
794+
throw e
758795
}
759-
o.await()
760796
return onConditionAwaitDoneImpl(o, true)
761797
}
762798

core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/LockManager.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock
66
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock
77
import org.pastalab.fray.core.ThreadContext
88
import org.pastalab.fray.core.ThreadState
9+
import org.pastalab.fray.core.concurrency.operations.ConditionAwaitBlocked
910
import org.pastalab.fray.core.concurrency.operations.ConditionWakeBlocked
1011
import org.pastalab.fray.core.concurrency.operations.ObjectWakeBlocked
1112

@@ -87,7 +88,11 @@ class LockManager {
8788
if (waitingObject == lockObject) {
8889
context.pendingOperation = ObjectWakeBlocked(waitingObject, !isTimeout)
8990
} else {
90-
context.pendingOperation = ConditionWakeBlocked(waitingObject as Condition, !isTimeout)
91+
context.pendingOperation =
92+
ConditionWakeBlocked(
93+
waitingObject as Condition,
94+
(context.pendingOperation as ConditionAwaitBlocked).canInterrupt,
95+
!isTimeout)
9196
}
9297
if (lockContext.canLock(context.thread.id)) {
9398
context.state = ThreadState.Enabled

core/src/main/kotlin/org/pastalab/fray/core/concurrency/operations/ConditionWakeBlocked.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@ package org.pastalab.fray.core.concurrency.operations
22

33
import java.util.concurrent.locks.Condition
44

5-
class ConditionWakeBlocked(val condition: Condition, val noTimeout: Boolean) :
6-
NonRacingOperation() {}
5+
class ConditionWakeBlocked(
6+
val condition: Condition,
7+
val canInterrupt: Boolean,
8+
val noTimeout: Boolean
9+
) : NonRacingOperation() {}

core/src/main/kotlin/org/pastalab/fray/core/observers/ScheduleVerifier.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package org.pastalab.fray.core.observers
22

3+
import java.io.File
4+
import kotlinx.serialization.json.Json
35
import org.pastalab.fray.core.ThreadContext
46

57
class ScheduleVerifier(val schedules: List<ScheduleRecording>) : ScheduleObserver {
8+
constructor(
9+
path: String
10+
) : this(Json.decodeFromString<List<ScheduleRecording>>(File(path).readText()))
11+
612
var index = 0
713

814
override fun onExecutionStart() {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.pastalab.fray.test.success.condition;
2+
3+
import java.util.concurrent.atomic.AtomicBoolean;
4+
import java.util.concurrent.locks.Condition;
5+
import java.util.concurrent.locks.Lock;
6+
import java.util.concurrent.locks.ReentrantLock;
7+
8+
public class ConditionAwaitTimeoutInterrupt {
9+
public static void main(String[] args) {
10+
Lock l = new ReentrantLock();
11+
Condition c = l.newCondition();
12+
AtomicBoolean flag = new AtomicBoolean(false);
13+
Thread t = new Thread(() -> {
14+
l.lock();
15+
try {
16+
c.await(1000, java.util.concurrent.TimeUnit.MILLISECONDS);
17+
} catch (InterruptedException e) {
18+
flag.set(true);
19+
}
20+
l.unlock();
21+
});
22+
t.start();
23+
Thread.yield();
24+
t.interrupt();
25+
l.lock();
26+
if (!t.isInterrupted()) {
27+
assert(flag.get());
28+
}
29+
l.unlock();
30+
}
31+
}

integration-test/src/main/java/org/pastalab/fray/test/success/condition/ConditionAwaitTimeoutNoDeadlock.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static void main(String[] args) {
1111
l.lock();
1212
try {
1313
boolean result = c.await(1000, java.util.concurrent.TimeUnit.MILLISECONDS);
14-
assert(!result);
14+
assert(result == false);
1515

1616
boolean result2 = c.awaitUntil(new java.util.Date(System.currentTimeMillis() + 1000));
1717
assert(!result2);

0 commit comments

Comments
 (0)