mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-11-01 17:43:03 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a81d28426b | ||
|
|
3bd1b5da5c | ||
|
|
19ca5e1487 | ||
|
|
41772231e4 | ||
|
|
208fc4a0fc | ||
|
|
a01137f398 | ||
|
|
29578863ef | ||
|
|
2461bd9e61 | ||
|
|
651ee37c19 | ||
|
|
1f4bb04ec9 | ||
|
|
67f4e1b9ff | ||
|
|
ae8af4868e | ||
|
|
c400ed4c3e | ||
|
|
41d92561a8 | ||
|
|
721056d550 | ||
|
|
52a801096c | ||
|
|
08400381a1 | ||
|
|
4ec19d0da3 |
No files matched your search
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.33.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `1.5.1` -> `1.6.0`
|
||||||
|
* `Serialization`: `1.11.0` -> `1.12.0`
|
||||||
|
* `Compose`: `1.12.1` -> `1.12.2`
|
||||||
|
* `SQLite`: `3.53.4.0` -> `3.54.1.0`
|
||||||
|
* `Ktor`: `3.6.0` -> `3.6.1`
|
||||||
|
* `Common`:
|
||||||
|
* Add JS `MutationObserverInit` factory that omits unspecified options and preserves browser defaults.
|
||||||
|
* Use safe `MutationObserver` options in `onRemoved` and cover DOM observation and outside-click cleanup with browser tests.
|
||||||
|
|
||||||
|
## 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`:
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ plugins {
|
|||||||
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
|
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
js {
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
useKarma {
|
||||||
|
useChromeHeadlessNoSandbox()
|
||||||
|
useConfigDirectory(rootProject.relativeProjectPath("gradle/karma.config.d"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
filter.excludeTestsMatching("dev.inmo.micro_utils.common.MutationObserverBrowserTests*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.MutationObserver
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
|
||||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||||
lateinit var observer: MutationObserver
|
lateinit var observer: MutationObserver
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.w3c.dom.MutationObserverInit as NativeMutationObserverInit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates options for [org.w3c.dom.MutationObserver] without adding unspecified properties.
|
||||||
|
*
|
||||||
|
* Unlike the Kotlin DOM factory, this factory leaves absent options out of the JavaScript object. In particular,
|
||||||
|
* an absent [attributeFilter] must not become a null sequence, and absent [attributes] and [characterData] must not
|
||||||
|
* override the browser defaults implied by their filters or old-value options.
|
||||||
|
*
|
||||||
|
* Null arguments are omitted. Explicit false values and empty arrays are preserved. The browser remains responsible
|
||||||
|
* for validating the options passed to [org.w3c.dom.MutationObserver.observe].
|
||||||
|
*/
|
||||||
|
fun MutationObserverInit(
|
||||||
|
childList: Boolean? = null,
|
||||||
|
attributes: Boolean? = null,
|
||||||
|
characterData: Boolean? = null,
|
||||||
|
subtree: Boolean? = null,
|
||||||
|
attributeOldValue: Boolean? = null,
|
||||||
|
characterDataOldValue: Boolean? = null,
|
||||||
|
attributeFilter: Array<String>? = null
|
||||||
|
): NativeMutationObserverInit = js("{}").unsafeCast<NativeMutationObserverInit>().apply {
|
||||||
|
if (childList != null) this.childList = childList
|
||||||
|
if (attributes != null) this.attributes = attributes
|
||||||
|
if (characterData != null) this.characterData = characterData
|
||||||
|
if (subtree != null) this.subtree = subtree
|
||||||
|
if (attributeOldValue != null) this.attributeOldValue = attributeOldValue
|
||||||
|
if (characterDataOldValue != null) this.characterDataOldValue = characterDataOldValue
|
||||||
|
if (attributeFilter != null) this.attributeFilter = attributeFilter
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.MutationObserver
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.dom.events.EventListener
|
||||||
|
import kotlin.js.Promise
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertSame
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/** Runs only in the browser: jsNodeTest excludes this class rather than reporting DOM checks as passing. */
|
||||||
|
class MutationObserverBrowserTests {
|
||||||
|
@Test
|
||||||
|
fun childListSubtreeRecordsCanBeDrainedAndDisconnected() {
|
||||||
|
val root = document.createElement("div")
|
||||||
|
val nested = document.createElement("div")
|
||||||
|
val child = document.createElement("span")
|
||||||
|
root.appendChild(nested)
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(root, MutationObserverInit(childList = true, subtree = true))
|
||||||
|
nested.appendChild(child)
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("childList", record.type)
|
||||||
|
assertSame(nested, record.target)
|
||||||
|
assertSame(child, record.addedNodes.item(0))
|
||||||
|
assertEquals(0, record.removedNodes.length)
|
||||||
|
assertNull(record.attributeName)
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
observer.disconnect()
|
||||||
|
nested.removeChild(child)
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attributeFilterEnablesObservationAndPreservesOldValues() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
target.setAttribute("data-state", "old")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(attributeFilter = arrayOf("data-state"), attributeOldValue = true))
|
||||||
|
target.setAttribute("ignored", "value")
|
||||||
|
target.setAttribute("data-state", "new")
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("attributes", record.type)
|
||||||
|
assertSame(target, record.target)
|
||||||
|
assertEquals("data-state", record.attributeName)
|
||||||
|
assertEquals("old", record.oldValue)
|
||||||
|
assertNull(record.attributeNamespace)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun characterDataOldValueEnablesObservation() {
|
||||||
|
val target = document.createTextNode("old")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(characterDataOldValue = true))
|
||||||
|
target.data = "new"
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("characterData", record.type)
|
||||||
|
assertSame(target, record.target)
|
||||||
|
assertEquals("old", record.oldValue)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nullAttributeFilterIsOmittedInsteadOfConvertedToASequence() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(childList = true, attributeFilter = null))
|
||||||
|
target.appendChild(document.createElement("span"))
|
||||||
|
assertEquals("childList", observer.takeRecords().single().type)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyAttributeFilterDoesNotObserveAllAttributes() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(attributeFilter = emptyArray()))
|
||||||
|
target.setAttribute("data-state", "new")
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nativeValidationRejectsContradictoryOptions() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
assertFails { observer.observe(target, MutationObserverInit()) }
|
||||||
|
assertFails {
|
||||||
|
observer.observe(target, MutationObserverInit(attributes = false, attributeFilter = arrayOf("class")))
|
||||||
|
}
|
||||||
|
assertFails {
|
||||||
|
observer.observe(target, MutationObserverInit(characterData = false, characterDataOldValue = true))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onRemovedCallsBackOnceAndDisconnects(): Promise<Unit> {
|
||||||
|
val parent = document.createElement("div")
|
||||||
|
val child = document.createElement("span")
|
||||||
|
var calls = 0
|
||||||
|
var observer: MutationObserver? = null
|
||||||
|
return Promise.resolve(Unit).then {
|
||||||
|
parent.appendChild(child)
|
||||||
|
document.body!!.appendChild(parent)
|
||||||
|
observer = child.onRemoved { calls++ }
|
||||||
|
parent.removeChild(child)
|
||||||
|
}.then {
|
||||||
|
assertEquals(1, calls)
|
||||||
|
parent.appendChild(child)
|
||||||
|
parent.removeChild(child)
|
||||||
|
}.then {
|
||||||
|
assertEquals(1, calls)
|
||||||
|
}.finally {
|
||||||
|
observer?.disconnect()
|
||||||
|
parent.parentNode?.removeChild(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onClickOutsideStopsListeningAfterRemoval(): Promise<Unit> {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
var calls = 0
|
||||||
|
var listener: EventListener? = null
|
||||||
|
return Promise.resolve(Unit).then {
|
||||||
|
document.body!!.appendChild(target)
|
||||||
|
listener = target.onClickOutside { calls++ }
|
||||||
|
document.dispatchEvent(Event("click"))
|
||||||
|
assertEquals(1, calls)
|
||||||
|
target.parentNode!!.removeChild(target)
|
||||||
|
}.then {
|
||||||
|
document.dispatchEvent(Event("click"))
|
||||||
|
assertEquals(1, calls)
|
||||||
|
}.finally {
|
||||||
|
listener?.let { document.removeEventListener("click", it) }
|
||||||
|
target.parentNode?.removeChild(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.w3c.dom.MutationObserverInit as NativeMutationObserverInit
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContentEquals
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertSame
|
||||||
|
|
||||||
|
class MutationObserverInitTests {
|
||||||
|
private fun keys(options: NativeMutationObserverInit): List<String> =
|
||||||
|
js("Object.keys(options)").unsafeCast<Array<String>>().sorted()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyOptionsHaveNoProperties() {
|
||||||
|
assertEquals(emptyList(), keys(MutationObserverInit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun childListOptionsOmitUnspecifiedProperties() {
|
||||||
|
val options = MutationObserverInit(childList = true, subtree = true)
|
||||||
|
assertEquals(listOf("childList", "subtree"), keys(options))
|
||||||
|
assertEquals(true, options.childList)
|
||||||
|
assertEquals(true, options.subtree)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitNullOptionsAreOmitted() {
|
||||||
|
val options = MutationObserverInit(
|
||||||
|
childList = null,
|
||||||
|
attributes = null,
|
||||||
|
characterData = null,
|
||||||
|
subtree = null,
|
||||||
|
attributeOldValue = null,
|
||||||
|
characterDataOldValue = null,
|
||||||
|
attributeFilter = null
|
||||||
|
)
|
||||||
|
assertEquals(emptyList(), keys(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitFalseAndEmptyFilterArePreserved() {
|
||||||
|
val filter = emptyArray<String>()
|
||||||
|
val options = MutationObserverInit(false, false, false, false, false, false, filter)
|
||||||
|
assertEquals(
|
||||||
|
listOf("attributeFilter", "attributeOldValue", "attributes", "characterData", "characterDataOldValue", "childList", "subtree"),
|
||||||
|
keys(options)
|
||||||
|
)
|
||||||
|
assertEquals(false, options.childList)
|
||||||
|
assertEquals(false, options.attributes)
|
||||||
|
assertEquals(false, options.characterData)
|
||||||
|
assertEquals(false, options.subtree)
|
||||||
|
assertEquals(false, options.attributeOldValue)
|
||||||
|
assertEquals(false, options.characterDataOldValue)
|
||||||
|
assertSame(filter, options.attributeFilter)
|
||||||
|
assertContentEquals(emptyArray(), options.attributeFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filtersAndOldValueOptionsDoNotForceObservationFlags() {
|
||||||
|
val filter = arrayOf("class", "data-state")
|
||||||
|
val options = MutationObserverInit(
|
||||||
|
attributeFilter = filter,
|
||||||
|
attributeOldValue = true,
|
||||||
|
characterDataOldValue = true
|
||||||
|
)
|
||||||
|
assertEquals(listOf("attributeFilter", "attributeOldValue", "characterDataOldValue"), keys(options))
|
||||||
|
assertSame(filter, options.attributeFilter)
|
||||||
|
assertEquals(true, options.attributeOldValue)
|
||||||
|
assertEquals(true, options.characterDataOldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun eachCallCreatesIndependentOptions() {
|
||||||
|
val first = MutationObserverInit(childList = true)
|
||||||
|
val second = MutationObserverInit(attributes = true)
|
||||||
|
first.subtree = true
|
||||||
|
assertEquals(listOf("childList", "subtree"), keys(first))
|
||||||
|
assertEquals(listOf("attributes"), keys(second))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+68
-48
@@ -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()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (changed) {
|
||||||
|
_freePermitsStateFlow.value += checkedPermits
|
||||||
|
}
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
if (shouldContinue) {
|
|
||||||
waitRelease(checkedPermits - acquiredPermits)
|
|
||||||
}
|
|
||||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
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 }
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
+78
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -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.33.0
|
||||||
android_code_version=319
|
android_code_version=321
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "2.4.21"
|
kt = "2.4.21"
|
||||||
kt-serialization = "1.11.0"
|
kt-serialization = "1.12.0"
|
||||||
kt-coroutines = "1.11.0"
|
kt-coroutines = "1.11.0"
|
||||||
|
|
||||||
kotlinx-browser = "0.5.0"
|
kotlinx-browser = "0.5.0"
|
||||||
|
|
||||||
kslog = "2.1.0"
|
kslog = "2.1.0"
|
||||||
|
|
||||||
jb-compose = "1.12.1"
|
jb-compose = "1.12.2"
|
||||||
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.6.0"
|
||||||
jb-dokka = "2.2.0"
|
jb-dokka = "2.2.0"
|
||||||
|
|
||||||
sqlite = "3.53.4.0"
|
sqlite = "3.54.1.0"
|
||||||
|
|
||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "3.6.0"
|
ktor = "3.6.1"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ project.group = "$group"
|
|||||||
apply plugin: 'org.jetbrains.dokka'
|
apply plugin: 'org.jetbrains.dokka'
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kotlin {
|
|||||||
browser {
|
browser {
|
||||||
testTask {
|
testTask {
|
||||||
useKarma {
|
useKarma {
|
||||||
useChromeHeadless()
|
useChromeHeadlessNoSandbox()
|
||||||
useConfigDirectory(rootProject.relativeProjectPath("gradle/karma.config.d"))
|
useConfigDirectory(rootProject.relativeProjectPath("gradle/karma.config.d"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -18,3 +18,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -20,3 +20,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -31,3 +31,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user