Skip to content

Commit b2a7f97

Browse files
committed
Preserve mutex invariant: throw ISE when incorrect access with owenr is detected
Signed-off-by: Nikita Koval <[email protected]>
1 parent f8af950 commit b2a7f97

File tree

3 files changed

+63
-18
lines changed

3 files changed

+63
-18
lines changed

Diff for: kotlinx-coroutines-core/common/src/sync/Mutex.kt

+47-14
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,41 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
165165
lockSuspend(owner)
166166
}
167167

168-
private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable<Unit> { cont ->
169-
cont as CancellableContinuationImpl<Unit>
168+
private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable { cont ->
170169
val contWithOwner = CancellableContinuationWithOwner(cont, owner)
171170
acquire(contWithOwner)
172171
}
173172

174-
override fun tryLock(owner: Any?): Boolean =
175-
if (tryAcquire()) {
176-
assert { this.owner.value === NO_OWNER }
177-
this.owner.value = owner
178-
true
179-
} else {
180-
false
173+
override fun tryLock(owner: Any?): Boolean = when (tryLockImpl(owner)) {
174+
TRY_LOCK_SUCCESS -> true
175+
TRY_LOCK_FAILED -> false
176+
TRY_LOCK_ALREADY_LOCKED_BY_OWNER -> error("This mutex is already locked by the specified owner: $owner")
177+
else -> error("unexpected")
178+
}
179+
180+
private fun tryLockImpl(owner: Any?): Int {
181+
while (true) {
182+
if (tryAcquire()) {
183+
assert { this.owner.value === NO_OWNER }
184+
this.owner.value = owner
185+
return TRY_LOCK_SUCCESS
186+
} else {
187+
// The semaphore permit acquisition has failed.
188+
// However, we need to check that this mutex is not
189+
// locked by our owner.
190+
if (owner != null) {
191+
// Is this mutex locked by our owner?
192+
if (holdsLock(owner)) return TRY_LOCK_ALREADY_LOCKED_BY_OWNER
193+
// This mutex is either locked by another owner or unlocked.
194+
// In the latter case, it is possible that it WAS locked by
195+
// our owner when the semaphore permit acquisition has failed.
196+
// To preserve linearizability, the operation restarts in this case.
197+
if (!isLocked) continue
198+
}
199+
return TRY_LOCK_FAILED
200+
}
181201
}
202+
}
182203

183204
override fun unlock(owner: Any?) {
184205
while (true) {
@@ -206,19 +227,26 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
206227
)
207228

208229
protected open fun onLockRegFunction(select: SelectInstance<*>, owner: Any?) {
209-
onAcquireRegFunction(SelectInstanceWithOwner(select as SelectInstanceInternal<*>, owner), owner)
230+
if (owner != null && holdsLock(owner)) {
231+
select.selectInRegistrationPhase(ON_LOCK_ALREADY_LOCKED_BY_OWNER)
232+
} else {
233+
onAcquireRegFunction(SelectInstanceWithOwner(select, owner), owner)
234+
}
210235
}
211236

212237
protected open fun onLockProcessResult(owner: Any?, result: Any?): Any? {
238+
if (result == ON_LOCK_ALREADY_LOCKED_BY_OWNER) {
239+
error("This mutex is already locked by the specified owner: $owner")
240+
}
213241
return this
214242
}
215243

216244
private inner class CancellableContinuationWithOwner(
217245
@JvmField
218-
val cont: CancellableContinuationImpl<Unit>,
246+
val cont: CancellableContinuation<Unit>,
219247
@JvmField
220248
val owner: Any?
221-
) : CancellableContinuation<Unit> by cont, Waiter by cont {
249+
) : CancellableContinuation<Unit> by cont {
222250
override fun tryResume(value: Unit, idempotent: Any?, onCancellation: ((cause: Throwable) -> Unit)?): Any? {
223251
assert { this@MutexImpl.owner.value === NO_OWNER }
224252
val token = cont.tryResume(value, idempotent) {
@@ -242,10 +270,10 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
242270

243271
private inner class SelectInstanceWithOwner<Q>(
244272
@JvmField
245-
val select: SelectInstanceInternal<Q>,
273+
val select: SelectInstance<Q>,
246274
@JvmField
247275
val owner: Any?
248-
) : SelectInstanceInternal<Q> by select {
276+
) : SelectInstanceInternal<Q> by select as SelectInstanceInternal<Q> {
249277
override fun trySelect(clauseObject: Any, result: Any?): Boolean {
250278
assert { this@MutexImpl.owner.value === NO_OWNER }
251279
return select.trySelect(clauseObject, result).also { success ->
@@ -264,3 +292,8 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
264292
}
265293

266294
private val NO_OWNER = Symbol("NO_OWNER")
295+
private val ON_LOCK_ALREADY_LOCKED_BY_OWNER = Symbol("ALREADY_LOCKED_BY_OWNER")
296+
297+
private const val TRY_LOCK_SUCCESS = 0
298+
private const val TRY_LOCK_FAILED = 1
299+
private const val TRY_LOCK_ALREADY_LOCKED_BY_OWNER = 2

Diff for: kotlinx-coroutines-core/common/test/sync/MutexTest.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
package kotlinx.coroutines.sync
66

7-
import kotlinx.atomicfu.*
87
import kotlinx.coroutines.*
8+
import kotlinx.coroutines.selects.*
99
import kotlin.test.*
1010

1111
class MutexTest : TestBase() {
@@ -138,4 +138,14 @@ class MutexTest : TestBase() {
138138
assertTrue(mutex.holdsLock(owner2))
139139
finish(4)
140140
}
141+
142+
@Test
143+
fun testIllegalStateInvariant() = runTest {
144+
val mutex = Mutex()
145+
val owner = Any()
146+
assertTrue(mutex.tryLock(owner))
147+
assertFailsWith<IllegalStateException> { mutex.tryLock(owner) }
148+
assertFailsWith<IllegalStateException> { mutex.lock(owner) }
149+
assertFailsWith<IllegalStateException> { select { mutex.onLock(owner) {} } }
150+
}
141151
}

Diff for: kotlinx-coroutines-core/jvm/test/lincheck/MutexLincheckTest.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ import org.jetbrains.kotlinx.lincheck.paramgen.*
1616
class MutexLincheckTest : AbstractLincheckTest() {
1717
private val mutex = Mutex()
1818

19-
@Operation
19+
@Operation(handleExceptionsAsResult = [IllegalStateException::class])
2020
fun tryLock(@Param(name = "owner") owner: Int) = mutex.tryLock(owner.asOwnerOrNull)
2121

22+
// TODO: `lock()` with non-null owner is non-linearizable
2223
@Operation(promptCancellation = true)
23-
suspend fun lock(@Param(name = "owner") owner: Int) = mutex.lock(owner.asOwnerOrNull)
24+
suspend fun lock() = mutex.lock(null)
2425

26+
// TODO: `onLock` with non-null owner is non-linearizable
2527
// onLock may suspend in case of clause re-registration.
2628
@Operation(allowExtraSuspension = true, promptCancellation = true)
27-
suspend fun onLock(@Param(name = "owner") owner: Int) = select<Unit> { mutex.onLock(owner.asOwnerOrNull) {} }
29+
suspend fun onLock() = select<Unit> { mutex.onLock(null) {} }
2830

2931
@Operation(handleExceptionsAsResult = [IllegalStateException::class])
3032
fun unlock(@Param(name = "owner") owner: Int) = mutex.unlock(owner.asOwnerOrNull)

0 commit comments

Comments
 (0)