Skip to content

Commit 8c4ff51

Browse files
qwwdfsadndkoval
andauthored
Preserve mutex invariant: throw ISE when incorrect access with owner is detected (#3620)
* Behaviour compatible with the previous implementation is restored * For everything else, there is #3626 Signed-off-by: Nikita Koval <[email protected]> Co-authored-by: Nikita Koval <[email protected]>
1 parent b6e1839 commit 8c4ff51

File tree

3 files changed

+58
-12
lines changed

3 files changed

+58
-12
lines changed

kotlinx-coroutines-core/common/src/sync/Mutex.kt

+42-8
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,36 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
170170
acquire(contWithOwner)
171171
}
172172

173-
override fun tryLock(owner: Any?): Boolean =
174-
if (tryAcquire()) {
175-
assert { this.owner.value === NO_OWNER }
176-
this.owner.value = owner
177-
true
178-
} else {
179-
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+
}
180201
}
202+
}
181203

182204
override fun unlock(owner: Any?) {
183205
while (true) {
@@ -205,10 +227,17 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
205227
)
206228

207229
protected open fun onLockRegFunction(select: SelectInstance<*>, owner: Any?) {
208-
onAcquireRegFunction(SelectInstanceWithOwner(select, 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+
}
209235
}
210236

211237
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+
}
212241
return this
213242
}
214243

@@ -263,3 +292,8 @@ internal open class MutexImpl(locked: Boolean) : SemaphoreImpl(1, if (locked) 1
263292
}
264293

265294
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

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
}

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)