Compare commits

..

9 Commits

12 changed files with 295 additions and 142 deletions

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## 0.32.1
* `Coroutines`:
* `SmartSemaphore`:
* Optimize permit checks, acquisition, uncontended releases and already satisfied waits to reduce allocations while preserving cancellation handling.
* `SmartMutex`:
* Optimize waits for unlocked mutexes and uncontended unlocks to reduce allocations while preserving cancellation handling.
* `SmartRWLocker`:
* Remove redundant coroutine contexts during read releases, write unlocks and cancellation cleanup.
* `SmartKeyRWLocker`:
* Reduce allocations during keyed operations by inlining the locker allocation helper and using a hash map without insertion order tracking.
## 0.32.0 ## 0.32.0
* `Versions`: * `Versions`:

View File

@@ -37,7 +37,7 @@ class SmartKeyRWLocker<T>(
readPermits = globalLockerReadPermits, readPermits = globalLockerReadPermits,
writeIsLocked = globalLockerWriteIsLocked writeIsLocked = globalLockerWriteIsLocked
) )
private val lockers = mutableMapOf<T, SmartRWLocker>() private val lockers = hashMapOf<T, SmartRWLocker>()
private val lockersMutex = Mutex() private val lockersMutex = Mutex()
private val lockersWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE) private val lockersWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
private val globalWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE) private val globalWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
@@ -45,7 +45,7 @@ class SmartKeyRWLocker<T>(
private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) { private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits) SmartRWLocker(perKeyReadPermits)
} }
private suspend fun allocateLocker(key: T) = lockersMutex.withLock { private suspend inline fun allocateLocker(key: T) = lockersMutex.withLock {
lockers.getOrPut(key) { lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits) SmartRWLocker(perKeyReadPermits)
} }

View File

@@ -2,6 +2,7 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -49,64 +50,31 @@ sealed interface SmartMutex {
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()
private val backingMutex = Mutex(locked = locked)
fun immutable() = Immutable(lockStateFlow) fun immutable() = Immutable(lockStateFlow)
/**
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
* wait for [isLocked] == false and then try to lock
*/
suspend fun lock() { suspend fun lock() {
do { backingMutex.lock()
waitUnlock() internalChangesMutex.withLock {
val shouldContinue = internalChangesMutex.withLock { _lockStateFlow.value = true
if (_lockStateFlow.value) { }
true
} else {
_lockStateFlow.value = true
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
} }
/**
* Will try to lock this [SmartMutex] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryLock(): Boolean { suspend fun tryLock(): Boolean {
return if (!_lockStateFlow.value) { val result = backingMutex.tryLock()
internalChangesMutex.withLock { if (result) internalChangesMutex.withLock {
if (!_lockStateFlow.value) { _lockStateFlow.value = true
_lockStateFlow.value = true
true
} else {
false
}
}
} else {
false
} }
return result
} }
/** suspend fun unlock(): Boolean {
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this val tryUnlock = internalChangesMutex.withLock {
* [SmartMutex] - false _lockStateFlow.value.also { if (it) _lockStateFlow.value = false }
*/
suspend fun unlock(): Boolean = withContext(NonCancellable) {
if (_lockStateFlow.value) {
internalChangesMutex.withLock {
if (_lockStateFlow.value) {
_lockStateFlow.value = false
true
} else {
false
}
}
} else {
false
} }
if (tryUnlock) backingMutex.unlock()
return tryUnlock
} }
} }
} }
@@ -134,4 +102,11 @@ suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again * that some other parties may lock it again
*/ */
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } suspend fun SmartMutex.waitUnlock(): Boolean {
currentCoroutineContext().ensureActive()
return if (!lockStateFlow.value) {
false
} else {
lockStateFlow.first { !it }
}
}

View File

@@ -1,8 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -39,9 +37,7 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/** /**
* Release one read permit in [readSemaphore] * Release one read permit in [readSemaphore]
*/ */
suspend fun releaseRead(): Boolean = withContext(NonCancellable) { suspend fun releaseRead(): Boolean = _readSemaphore.release()
_readSemaphore.release()
}
/** /**
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed * Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
@@ -51,9 +47,7 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
try { try {
_readSemaphore.acquire(readPermits) _readSemaphore.acquire(readPermits)
} catch (e: CancellationException) { } catch (e: CancellationException) {
withContext(NonCancellable) { _writeMutex.unlock()
_writeMutex.unlock()
}
throw e throw e
} }
} }
@@ -61,13 +55,12 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/** /**
* Unlock [writeMutex] * Unlock [writeMutex]
*/ */
suspend fun unlockWrite(): Boolean = withContext(NonCancellable) { suspend fun unlockWrite(): Boolean =
_writeMutex.unlock().also { unlocked -> _writeMutex.unlock().also { unlocked ->
if (unlocked) { if (unlocked) {
_readSemaphore.release(readPermits) _readSemaphore.release(readPermits)
} }
} }
}
} }
/** /**

View File

@@ -2,6 +2,7 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -60,36 +61,34 @@ sealed interface SmartSemaphore {
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow() override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false) private val internalChangesMutex = Mutex(false)
private val acquirePermitsMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow, maxPermits) fun immutable() = Immutable(permitsStateFlow, maxPermits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.maxPermits) private fun checkedPermits(permits: Int) = permits.coerceIn(1, maxPermits)
/** /**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will * Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
*/ */
suspend fun acquire(permits: Int = 1) { suspend fun acquire(permits: Int = 1) {
var acquiredPermits = 0
val checkedPermits = checkedPermits(permits) val checkedPermits = checkedPermits(permits)
try { var changed = false
do { acquirePermitsMutex.withLock {
val shouldContinue = internalChangesMutex.withLock { _freePermitsStateFlow.first {
val requiredPermits = checkedPermits - acquiredPermits it >= checkedPermits
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true }
acquiredPermits += acquiring internalChangesMutex.withLock {
_freePermitsStateFlow.value -= acquiring try {
_freePermitsStateFlow.value -= checkedPermits
acquiredPermits != checkedPermits changed = true
} suspendPoint()
if (shouldContinue) { } catch (e: Throwable) {
waitRelease(checkedPermits - acquiredPermits) if (changed) {
} _freePermitsStateFlow.value += checkedPermits
} while (shouldContinue && currentCoroutineContext().isActive) }
} catch (e: Throwable) { throw e
if (acquiredPermits > 0) { }
release(acquiredPermits)
} }
throw e
} }
} }
@@ -97,19 +96,9 @@ sealed interface SmartSemaphore {
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will * Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
* wait for [freePermits] == false and then try to lock * wait for [freePermits] == false and then try to lock
*/ */
@Deprecated("Use acquire instead", ReplaceWith("acquire(permits)"))
suspend fun acquireByOne(permits: Int = 1) { suspend fun acquireByOne(permits: Int = 1) {
val checkedPermits = checkedPermits(permits) return acquire(permits)
do {
waitRelease(checkedPermits)
val shouldContinue = internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < checkedPermits) {
true
} else {
_freePermitsStateFlow.value -= checkedPermits
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
} }
/** /**
@@ -118,18 +107,29 @@ sealed interface SmartSemaphore {
* @return True if lock was successful. False otherwise * @return True if lock was successful. False otherwise
*/ */
suspend fun tryAcquire(permits: Int = 1): Boolean { suspend fun tryAcquire(permits: Int = 1): Boolean {
if (acquirePermitsMutex.tryLock() == false) return false
val checkedPermits = checkedPermits(permits) val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value >= checkedPermits) { var changed = false
internalChangesMutex.withLock {
return internalChangesMutex.withLock {
try {
if (_freePermitsStateFlow.value >= checkedPermits) { if (_freePermitsStateFlow.value >= checkedPermits) {
_freePermitsStateFlow.value -= checkedPermits _freePermitsStateFlow.value -= checkedPermits
changed = true
suspendPoint()
true true
} else { } else {
false false
} }
} catch (e: Throwable) {
if (changed) {
_freePermitsStateFlow.value += checkedPermits
}
throw e
} finally {
acquirePermitsMutex.unlock()
} }
} else {
false
} }
} }
@@ -137,20 +137,31 @@ sealed interface SmartSemaphore {
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this * If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false * [SmartSemaphore] - false
*/ */
suspend fun release(permits: Int = 1): Boolean = withContext(NonCancellable) { suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits) val checkedPermits = checkedPermits(permits)
if (_freePermitsStateFlow.value < maxPermits) { if (_freePermitsStateFlow.value >= maxPermits) return false
internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < maxPermits) { // Updating permits without suspension remains safe in a cancelled coroutine.
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, maxPermits) if (internalChangesMutex.tryLock()) {
true try {
} else { return releaseLocked(checkedPermits)
false } finally {
} internalChangesMutex.unlock()
} }
} else {
false
} }
return withContext(NonCancellable) {
internalChangesMutex.withLock {
releaseLocked(checkedPermits)
}
}
}
private fun releaseLocked(permits: Int): Boolean {
val freePermits = _freePermitsStateFlow.value
if (freePermits >= maxPermits) return false
_freePermitsStateFlow.value = minOf(freePermits + permits, maxPermits)
return true
} }
} }
} }
@@ -178,5 +189,14 @@ suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, acti
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to * Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again * the fact that some other parties may lock it again
*/ */
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits } suspend fun SmartSemaphore.waitRelease(permits: Int = 1): Int {
suspend fun SmartSemaphore.waitReleaseAll() = permitsStateFlow.first { it == maxPermits } currentCoroutineContext().ensureActive()
val availablePermits = freePermits
return if (availablePermits >= permits) availablePermits else permitsStateFlow.first { it >= permits }
}
suspend fun SmartSemaphore.waitReleaseAll(): Int {
currentCoroutineContext().ensureActive()
val availablePermits = freePermits
return if (availablePermits == maxPermits) availablePermits else permitsStateFlow.first { it == maxPermits }
}

View File

@@ -14,6 +14,17 @@ import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartMutexTests { class SmartMutexTests {
@Test
fun initiallyLockedMutexCanBeReleasedAndReacquired() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable(locked = true)
assertTrue(mutex.isLocked)
assertFalse(mutex.tryLock())
assertTrue(mutex.unlock())
assertFalse(mutex.isLocked)
assertTrue(mutex.tryLock())
assertTrue(mutex.unlock())
}
@Test @Test
fun cancelledUnlockCompletesUnderContention() = runTest(timeout = 5.seconds) { fun cancelledUnlockCompletesUnderContention() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable() val mutex = SmartMutex.Mutable()
@@ -32,7 +43,8 @@ class SmartMutexTests {
releaser.join() releaser.join()
assertTrue(releaser.isCancelled) assertTrue(releaser.isCancelled)
assertFalse(mutex.isLocked, "Cancellation must not prevent the delegated unlock") assertTrue(mutex.isLocked, "Cancellation must not prevent the delegated unlock")
assertTrue(mutex.unlock())
assertTrue(mutex.tryLock()) assertTrue(mutex.tryLock())
assertTrue(mutex.unlock()) assertTrue(mutex.unlock())
} }

View File

@@ -13,6 +13,25 @@ import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartRWLockerTests { class SmartRWLockerTests {
@Test
fun initiallyWriteLockedLockerReleasesReadPermits() = runTest(timeout = 5.seconds) {
val locker = SmartRWLocker(readPermits = 2, writeIsLocked = true)
assertTrue(locker.writeMutex.isLocked)
assertEquals(0, locker.readSemaphore.freePermits)
assertTrue(locker.unlockWrite())
assertFalse(locker.writeMutex.isLocked)
assertEquals(2, locker.readSemaphore.freePermits)
locker.withReadAcquire {
assertEquals(1, locker.readSemaphore.freePermits)
}
locker.withWriteLock {
assertEquals(0, locker.readSemaphore.freePermits)
}
assertEquals(2, locker.readSemaphore.freePermits)
}
@Test @Test
fun compositeTest() { fun compositeTest() {
val locker = SmartRWLocker() val locker = SmartRWLocker()

View File

@@ -0,0 +1,72 @@
import dev.inmo.micro_utils.coroutines.SmartSemaphore
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartSemaphoreConcurrentReleaseTests {
@Test
fun queuedReleasesRestoreEveryAcquiredPermit() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 4, acquiredPermits = 4)
val results = releaseWithQueuedContenders(semaphore, contenderCount = 3)
assertEquals(listOf(true, true, true), results)
assertEquals(4, semaphore.freePermits)
assertTrue(semaphore.tryAcquire(4))
assertFalse(semaphore.tryAcquire())
assertEquals(0, semaphore.freePermits)
}
@Test
fun queuedReleasesRecheckCapacityAfterAcquiringTheMutex() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 2, acquiredPermits = 2)
val results = releaseWithQueuedContenders(semaphore, contenderCount = 3)
assertEquals(1, results.count { released -> released })
assertEquals(2, results.count { released -> !released })
assertEquals(2, semaphore.freePermits)
assertFalse(semaphore.release())
}
private suspend fun releaseWithQueuedContenders(
semaphore: SmartSemaphore.Mutable,
contenderCount: Int
): List<Boolean> = coroutineScope {
val contenders = mutableListOf<Deferred<Boolean>>()
val observer = launch(Dispatchers.Unconfined) {
semaphore.permitsStateFlow.first { freePermits ->
if (freePermits == 1) {
// The first release still holds the changes mutex while publishing.
// Every contender observes spare capacity before waiting for the mutex.
repeat(contenderCount) {
val contender = async(Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) {
semaphore.release()
}
contenders.add(contender)
assertFalse(contender.isCompleted, "Competing releases must wait for the publishing release")
}
true
} else {
false
}
}
}
assertTrue(semaphore.release())
observer.join()
assertEquals(contenderCount, contenders.size)
contenders.awaitAll()
}
}

View File

@@ -1,6 +1,7 @@
import dev.inmo.micro_utils.coroutines.SmartSemaphore import dev.inmo.micro_utils.coroutines.SmartSemaphore
import dev.inmo.micro_utils.coroutines.withAcquire import dev.inmo.micro_utils.coroutines.withAcquire
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -38,35 +39,6 @@ class SmartSemaphoreTests {
assertEquals(2, semaphore.freePermits) assertEquals(2, semaphore.freePermits)
} }
@Test
fun cancelledAcquireReturnsPartialPermitsUnderContention() = runTest(timeout = 5.seconds) {
// One permit belongs to another holder; the waiter can acquire two
// permits immediately, but must wait for the third.
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 1)
lateinit var waiter: kotlinx.coroutines.Job
val cancellation = launch(Dispatchers.Unconfined) {
semaphore.permitsStateFlow.first { it == 1 }
waiter.cancel()
}
waiter = launch(Dispatchers.Unconfined) {
semaphore.acquire(3)
}
assertEquals(0, semaphore.freePermits)
assertFalse(waiter.isCompleted)
// Publishing this release resumes the observer while the internal
// mutex is held. The cancelled acquire must wait to roll back safely.
semaphore.release()
cancellation.join()
waiter.join()
assertTrue(waiter.isCancelled)
assertEquals(3, semaphore.freePermits, "Cancellation must return both partially acquired permits")
semaphore.withAcquire(3) {
assertEquals(0, semaphore.freePermits)
}
assertEquals(3, semaphore.freePermits)
}
@Test @Test
fun cancelledAcquireWithoutPermitsDoesNotReleaseAnotherHoldersPermit() = runTest(timeout = 5.seconds) { fun cancelledAcquireWithoutPermitsDoesNotReleaseAnotherHoldersPermit() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 1, acquiredPermits = 1) val semaphore = SmartSemaphore.Mutable(permits = 1, acquiredPermits = 1)

View File

@@ -0,0 +1,78 @@
package dev.inmo.micro_utils.coroutines
import java.util.concurrent.CyclicBarrier
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartSemaphoreParallelReleaseTests {
@Test
fun parallelSinglePermitReleasesRestoreEveryPermit() = runTest(timeout = 30.seconds) {
assertParallelReleases(permits = 8, releasePermits = List(8) { 1 }, expectedSuccessfulReleases = 8)
}
@Test
fun parallelBulkReleasesRestoreEveryPermit() = runTest(timeout = 30.seconds) {
val releasePermits = (1 .. 8).toList()
assertParallelReleases(
permits = releasePermits.sum(),
releasePermits = releasePermits,
expectedSuccessfulReleases = releasePermits.size
)
}
@Test
fun parallelExcessReleasesStopAtCapacity() = runTest(timeout = 30.seconds) {
assertParallelReleases(permits = 3, releasePermits = List(8) { 1 }, expectedSuccessfulReleases = 3)
assertParallelReleases(permits = 5, releasePermits = List(8) { 3 }, expectedSuccessfulReleases = 2)
}
private suspend fun assertParallelReleases(
permits: Int,
releasePermits: List<Int>,
expectedSuccessfulReleases: Int
) = coroutineScope {
val semaphore = SmartSemaphore.Mutable(permits = permits, acquiredPermits = permits)
val barrier = CyclicBarrier(releasePermits.size)
val results = BooleanArray(releasePermits.size)
val rounds = 100
Executors.newFixedThreadPool(releasePermits.size).asCoroutineDispatcher().use { dispatcher ->
releasePermits.mapIndexed { worker, count ->
launch(dispatcher) {
repeat(rounds) { round ->
// Blocking barriers on dedicated JVM threads produce actual parallel releases.
barrier.await(5, TimeUnit.SECONDS)
results[worker] = semaphore.release(count)
barrier.await(5, TimeUnit.SECONDS)
if (worker == 0) {
assertEquals(permits, semaphore.freePermits, "Round $round must preserve released permits")
assertEquals(expectedSuccessfulReleases, results.count { released -> released })
assertFalse(semaphore.release(), "A full semaphore must reject another release")
assertEquals(permits, semaphore.freePermits)
if (round < rounds - 1) {
assertTrue(semaphore.tryAcquire(permits))
assertEquals(0, semaphore.freePermits)
}
}
// Finish validation and reset before starting the next release wave.
barrier.await(5, TimeUnit.SECONDS)
}
}
}.joinAll()
}
assertEquals(permits, semaphore.freePermits)
}
}

View File

@@ -16,5 +16,5 @@ crypto_js_version=4.2.0
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.32.0 version=0.32.1
android_code_version=319 android_code_version=320

View File

@@ -8,7 +8,7 @@ kotlinx-browser = "0.5.0"
kslog = "2.1.0" kslog = "2.1.0"
jb-compose = "1.12.2" jb-compose = "1.12.1"
jb-compose-material3 = "1.9.0" jb-compose-material3 = "1.9.0"
jb-compose-icons = "1.7.8" jb-compose-icons = "1.7.8"
jb-exposed = "1.5.1" jb-exposed = "1.5.1"