From 78494b60365c2375045b5a5d82f6518ebbbc73f4 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 15 Mar 2025 20:33:55 +0600 Subject: [PATCH 01/15] start 0.25.3 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea531e3d55..b55b2dc3453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.25.3 + ## 0.25.2 * `Versions`: diff --git a/gradle.properties b/gradle.properties index f987d5fa29b..e934dd22763 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.25.2 -android_code_version=292 +version=0.25.3 +android_code_version=293 From cfaa2a892701e9aaa58bc02bc15e296b224e66df Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 15 Mar 2025 20:43:17 +0600 Subject: [PATCH 02/15] add alsoWithUnlockingOnSuccess and alsoWithUnlockingOnSuccessAsync --- CHANGELOG.md | 3 +++ .../coroutines/SmartRWLockerExtensions.kt | 23 +++++++++++++++++++ .../inmo/micro_utils/repos/cache/CacheRepo.kt | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLockerExtensions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b55b2dc3453..bf7fc5ca09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.25.3 +* `Coroutines`: + * Add extensions `SmartRWLocker.alsoWithUnlockingOnSuccessAsync` and `SmartRWLocker.alsoWithUnlockingOnSuccess` + ## 0.25.2 * `Versions`: diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLockerExtensions.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLockerExtensions.kt new file mode 100644 index 00000000000..f644a4f0206 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLockerExtensions.kt @@ -0,0 +1,23 @@ +package dev.inmo.micro_utils.coroutines + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +suspend inline fun alsoWithUnlockingOnSuccess( + vararg lockers: SmartRWLocker, + block: suspend () -> Unit +): Result { + return runCatching { + block() + }.onSuccess { + lockers.forEach { it.unlockWrite() } + } +} + +fun alsoWithUnlockingOnSuccessAsync( + scope: CoroutineScope, + vararg lockers: SmartRWLocker, + block: suspend () -> Unit +): Job = scope.launchLoggingDropExceptions { + alsoWithUnlockingOnSuccess(*lockers, block = block) +} \ No newline at end of file diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt index bf709c4657a..ed4fc1e078b 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/CacheRepo.kt @@ -1,7 +1,9 @@ package dev.inmo.micro_utils.repos.cache +import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job interface InvalidatableRepo { /** From 5577a24548d50fff0df7465fa715abdb002f6676 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 19 Mar 2025 18:10:20 +0600 Subject: [PATCH 03/15] fix of docs for HEXAColor --- colors/common/src/commonMain/kotlin/HEXAColor.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/colors/common/src/commonMain/kotlin/HEXAColor.kt b/colors/common/src/commonMain/kotlin/HEXAColor.kt index b2aa70fec29..f39b7bdb5fb 100644 --- a/colors/common/src/commonMain/kotlin/HEXAColor.kt +++ b/colors/common/src/commonMain/kotlin/HEXAColor.kt @@ -13,7 +13,7 @@ import kotlin.math.floor * * Anyway it is recommended to use * - * @param hexaUInt rgba [UInt] in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha + * @param hexaUInt rgba [UInt] in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha */ @Serializable @JvmInline @@ -21,18 +21,18 @@ value class HEXAColor ( val hexaUInt: UInt ) : Comparable { /** - * @returns [hexaUInt] as a string with format `#FFEEBBAA` where FF - red, EE - green, BB - blue and AA - alpha + * @returns [hexaUInt] as a string with format `#RRGGBBAA` where RR - red, GG - green, BB - blue and AA - alpha */ val hexa: String get() = "#${hexaUInt.toString(16).padStart(8, '0')}" /** - * @returns [hexaUInt] as a string with format `#FFEEBB` where FF - red, EE - green and BB - blue + * @returns [hexaUInt] as a string with format `#RRGGBB` where RR - red, GG - green and BB - blue */ val hex: String get() = hexa.take(7) /** - * @returns [hexaUInt] as a string with format `#AAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue + * @returns [hexaUInt] as a string with format `#AARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue */ val ahex: String get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}" @@ -140,12 +140,12 @@ value class HEXAColor ( }.lowercase().toUInt(16).let(::HEXAColor) /** - * Creates [HEXAColor] from [uint] presume it is in format `0xFFEEBBAA` where FF - red, EE - green, BB - blue` and AA - alpha + * Creates [HEXAColor] from [uint] presume it is in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha */ fun fromHexa(uint: UInt) = HEXAColor(uint) /** - * Creates [HEXAColor] from [uint] presume it is in format `0xAAFFEEBB` where AA - alpha, FF - red, EE - green and BB - blue` + * Creates [HEXAColor] from [uint] presume it is in format `0xAARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue` */ fun fromAhex(uint: UInt) = HEXAColor( a = ((uint and 0xff000000u) / 0x1000000u).toInt(), From edb97215ef9c0336905da40a5315531bbed7d589 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 20 Mar 2025 17:28:28 +0600 Subject: [PATCH 04/15] add TransactionsDSL --- CHANGELOG.md | 2 + settings.gradle | 1 + transactions/build.gradle | 7 ++ .../src/commonMain/kotlin/TransactionsDSL.kt | 82 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 transactions/build.gradle create mode 100644 transactions/src/commonMain/kotlin/TransactionsDSL.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7fc5ca09d..2072fc62925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * `Coroutines`: * Add extensions `SmartRWLocker.alsoWithUnlockingOnSuccessAsync` and `SmartRWLocker.alsoWithUnlockingOnSuccess` +* `Transactions`: + * Add `TransactionsDSL` ## 0.25.2 diff --git a/settings.gradle b/settings.gradle index a8e6a646598..99d4168788a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ rootProject.name='micro_utils' String[] includes = [ ":common", ":common:compose", + ":transactions", ":matrix", ":safe_wrapper", ":crypto", diff --git a/transactions/build.gradle b/transactions/build.gradle new file mode 100644 index 00000000000..9e169e84113 --- /dev/null +++ b/transactions/build.gradle @@ -0,0 +1,7 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project" diff --git a/transactions/src/commonMain/kotlin/TransactionsDSL.kt b/transactions/src/commonMain/kotlin/TransactionsDSL.kt new file mode 100644 index 00000000000..c268891b5b5 --- /dev/null +++ b/transactions/src/commonMain/kotlin/TransactionsDSL.kt @@ -0,0 +1,82 @@ +package dev.inmo.micro_utils.transactions + +typealias TransactionDSLRollbackLambda = suspend (Throwable) -> Unit +class TransactionsDSL internal constructor() { + internal val rollbackActions = LinkedHashSet() + + internal fun addRollbackAction(rollbackAction: TransactionDSLRollbackLambda) { + rollbackActions.add(rollbackAction) + } +} +class RollbackContext internal constructor ( + val actionResult: T, + val error: Throwable +) + +/** + * Calls [action] and, if it succeeded - saving [rollback] action for future usage for cases when some other + * action or even main one throwing an error + * + * @param rollback Will be called if + */ +suspend fun TransactionsDSL.rollbackableOperation( + rollback: suspend RollbackContext.() -> Unit, + action: suspend () -> T +): T { + return runCatching { action() } + .onSuccess { + addRollbackAction { e -> + val context = RollbackContext(it, e) + context.rollback() + } + } + .getOrThrow() +} + +/** + * Starts transaction with opportunity to add actions [rollbackableOperation]. How to use: + * + * ```kotlin + * doSuspendTransaction { + * println("start of action") + * + * withRollback({ // it - result of action + * // some rollback action + * }) { + * // Some action with rollback + * } + * + * withRollback({ + * repository.delete(it) // it - result of createSomething, if it completes successfully + * }) { + * repository.createSomething() + * } + * + * withRollback({ + * // will not be triggered due to error in action + * }) { + * error("It is just a simple error") // Will trigger rolling back previously successfully completed actions + * } + * } + * ``` + * + * @param onRollbackStepError Will be called if rollback action throwing some error + */ +suspend fun doSuspendTransaction( + onRollbackStepError: suspend (TransactionDSLRollbackLambda, Throwable) -> Unit = { _, _ -> }, + block: suspend TransactionsDSL.() -> T +): Result { + val transactionsDSL = TransactionsDSL() + + return runCatching { + transactionsDSL.block() + }.onFailure { e -> + transactionsDSL.rollbackActions.forEach { + runCatching { + it.invoke(e) + }.onFailure { ee -> + onRollbackStepError(it, ee) + } + } + } +} From 0515b49b984c4e407de382cd1d47816614bc85da Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 20 Mar 2025 18:02:44 +0600 Subject: [PATCH 05/15] add transactions tests --- .../commonTest/kotlin/TransactionsDSLTests.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 transactions/src/commonTest/kotlin/TransactionsDSLTests.kt diff --git a/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt b/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt new file mode 100644 index 00000000000..5c1ab7c9973 --- /dev/null +++ b/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt @@ -0,0 +1,93 @@ +import dev.inmo.micro_utils.transactions.doSuspendTransaction +import dev.inmo.micro_utils.transactions.rollbackableOperation +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TransactionsDSLTests { + @Test + fun successfulTest() = runTest { + val dataCollections = Array(100) { + Triple( + it, // expected data + false, // has rollback happen or not + -1 // actual data + ) + } + + val actionResult = doSuspendTransaction { + dataCollections.forEachIndexed { i, _ -> + val resultData = rollbackableOperation({ + dataCollections[i] = actionResult.copy(second = true) + }) { + val result = dataCollections[i] + dataCollections[i] = result.copy( + third = i + ) + dataCollections[i] + } + assertEquals(dataCollections[i], resultData) + assertTrue(dataCollections[i] === resultData) + } + true + }.getOrThrow() + + dataCollections.forEachIndexed { i, triple -> + assertFalse(triple.second) + assertEquals(triple.first, i) + assertEquals(i, triple.third) + } + assertTrue(actionResult) + } + @Test + fun fullTest() = runTest { + val testsCount = 100 + for (testNumber in 0 until testsCount) { + val error = IllegalStateException("Test must fail at $testNumber") + val dataCollections = Array(testsCount) { + Triple( + it, // expected data + false, // has rollback happen or not + -1 // actual data + ) + } + + val actionResult = doSuspendTransaction { + dataCollections.forEachIndexed { i, _ -> + val resultData = rollbackableOperation({ + assertTrue(error === this.error) + dataCollections[i] = actionResult.copy(second = true) + }) { + if (i == testNumber) throw error + val result = dataCollections[i] + dataCollections[i] = result.copy( + third = i + ) + dataCollections[i] + } + assertEquals(dataCollections[i], resultData) + assertTrue(dataCollections[i] === resultData) + } + true + }.getOrElse { + assertTrue(it === error) + true + } + + dataCollections.forEachIndexed { i, triple -> + if (i < testNumber) { + assertTrue(triple.second) + assertEquals(triple.first, i) + assertEquals(i, triple.third) + } else { + assertFalse(triple.second) + assertEquals(triple.first, i) + assertEquals(-1, triple.third) + } + } + assertTrue(actionResult) + } + } +} \ No newline at end of file From 4b7d65e8b4843e5c351da443618a5de6234806a9 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 20 Mar 2025 18:04:19 +0600 Subject: [PATCH 06/15] small logic fix in transactions dsl --- transactions/src/commonMain/kotlin/TransactionsDSL.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transactions/src/commonMain/kotlin/TransactionsDSL.kt b/transactions/src/commonMain/kotlin/TransactionsDSL.kt index c268891b5b5..566a4aca628 100644 --- a/transactions/src/commonMain/kotlin/TransactionsDSL.kt +++ b/transactions/src/commonMain/kotlin/TransactionsDSL.kt @@ -63,7 +63,7 @@ suspend fun TransactionsDSL.rollbackableOperation( * @param onRollbackStepError Will be called if rollback action throwing some error */ suspend fun doSuspendTransaction( - onRollbackStepError: suspend (TransactionDSLRollbackLambda, Throwable) -> Unit = { _, _ -> }, + onRollbackStepError: suspend (Throwable) -> Unit = { }, block: suspend TransactionsDSL.() -> T ): Result { val transactionsDSL = TransactionsDSL() @@ -75,7 +75,7 @@ suspend fun doSuspendTransaction( runCatching { it.invoke(e) }.onFailure { ee -> - onRollbackStepError(it, ee) + onRollbackStepError(ee) } } } From 4c9e435df83722dafc603e951188d39e73c3cc03 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 22 Mar 2025 16:01:59 +0600 Subject: [PATCH 07/15] temporal realization of SmartKeyRWLocker --- .../coroutines/SmartKeyRWLocker.kt | 79 ++++++++++++++++++ .../src/commonTest/kotlin/RealTimeOut.kt | 12 +++ .../kotlin/SmartKeyRWLockerTests.kt | 80 +++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt create mode 100644 coroutines/src/commonTest/kotlin/RealTimeOut.kt create mode 100644 coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt new file mode 100644 index 00000000000..eb006810bf4 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt @@ -0,0 +1,79 @@ +package dev.inmo.micro_utils.coroutines + +import kotlinx.coroutines.sync.withLock +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +class SmartKeyRWLocker( + private val perKeyReadPermits: Int = Int.MAX_VALUE +) { + private val internalRWLocker = SmartRWLocker() + private val lockers = mutableMapOf() + + private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) { + SmartRWLocker(perKeyReadPermits) + } + + suspend fun writeMutex(key: T): SmartMutex.Immutable = internalRWLocker.withReadAcquire { + allocateLockerWithoutLock(key).writeMutex + } + suspend fun readSemaphore(key: T): SmartSemaphore.Immutable = internalRWLocker.withReadAcquire { + allocateLockerWithoutLock(key).readSemaphore + } + fun writeMutexOrNull(key: T): SmartMutex.Immutable? = lockers[key] ?.writeMutex + fun readSemaphoreOrNull(key: T): SmartSemaphore.Immutable? = lockers[key] ?.readSemaphore + + suspend fun acquireRead(key: T) { + internalRWLocker.withReadAcquire { + val locker = allocateLockerWithoutLock(key) + locker.acquireRead() + } + } + suspend fun releaseRead(key: T): Boolean { + return internalRWLocker.withReadAcquire { + lockers[key] + } ?.releaseRead() == true + } + + suspend fun lockWrite(key: T) { + internalRWLocker.withWriteLock { + val locker = allocateLockerWithoutLock(key) + locker.lockWrite() + } + } + suspend fun unlockWrite(key: T): Boolean { + return internalRWLocker.withWriteLock { + lockers[key] + } ?.unlockWrite() == true + } + fun isWriteLocked(key: T): Boolean = lockers[key] ?.writeMutex ?.isLocked == true +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withReadAcquire(key: T, action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + acquireRead(key) + try { + return action() + } finally { + releaseRead(key) + } +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withWriteLock(key: T, action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + lockWrite(key) + try { + return action() + } finally { + unlockWrite(key) + } +} \ No newline at end of file diff --git a/coroutines/src/commonTest/kotlin/RealTimeOut.kt b/coroutines/src/commonTest/kotlin/RealTimeOut.kt new file mode 100644 index 00000000000..ee598385977 --- /dev/null +++ b/coroutines/src/commonTest/kotlin/RealTimeOut.kt @@ -0,0 +1,12 @@ +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +suspend fun realWithTimeout(time: Duration, block: suspend () -> T): T { + return withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(time) { + block() + } + } +} diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt new file mode 100644 index 00000000000..d4c8981a87b --- /dev/null +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -0,0 +1,80 @@ +import dev.inmo.micro_utils.coroutines.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class SmartKeyRWLockerTests { + @Test + fun lockKeyFailedOnGlobalLockTest() = runTest { + val locker = SmartKeyRWLocker() + val testKey = "test" + locker.lockWrite() + + assertTrue { locker.isWriteLocked() } + + assertFails { + realWithTimeout(1.seconds) { + locker.lockWrite(testKey) + } + } + assertFalse { locker.isWriteLocked(testKey) } + + locker.unlockWrite() + assertFalse { locker.isWriteLocked() } + + realWithTimeout(1.seconds) { + locker.lockWrite(testKey) + } + assertTrue { locker.isWriteLocked(testKey) } + assertTrue { locker.unlockWrite(testKey) } + assertFalse { locker.isWriteLocked(testKey) } + } + @Test + fun readsBlockingGlobalWrite() = runTest { + val locker = SmartKeyRWLocker() + + val testKeys = (0 until 100).map { "test$it" } + + for (i in testKeys.indices) { + val it = testKeys[i] + locker.acquireRead(it) + val previous = testKeys.take(i) + val next = testKeys.drop(i + 1) + + previous.forEach { + assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 } + } + next.forEach { + assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null } + } + } + + for (i in testKeys.indices) { + val it = testKeys[i] + assertFails { + realWithTimeout(13.milliseconds) { locker.lockWrite() } + } + realWithTimeout(1.seconds) { locker.acquireRead() } + locker.releaseRead() + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + + locker.releaseRead(it) + } + + realWithTimeout(1.seconds) { locker.lockWrite() } + assertFails { + realWithTimeout(13.milliseconds) { locker.acquireRead() } + } + assertTrue { locker.unlockWrite() } + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + } +} From 761070b9b71241e2a99a082f708ad04150728fd1 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 23 Mar 2025 11:16:45 +0600 Subject: [PATCH 08/15] potentially first version of SmartKeyRWLocker --- .../coroutines/SmartKeyRWLocker.kt | 116 +++++++++++++++--- .../micro_utils/coroutines/SmartRWLocker.kt | 8 +- .../micro_utils/coroutines/SmartSemaphore.kt | 17 +-- .../kotlin/SmartKeyRWLockerTests.kt | 43 ++++++- .../commonTest/kotlin/SmartRWLockerTests.kt | 16 +++ .../src/commonMain/kotlin/TransactionsDSL.kt | 4 +- .../commonTest/kotlin/TransactionsDSLTests.kt | 6 +- 7 files changed, 182 insertions(+), 28 deletions(-) diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt index eb006810bf4..ac296566760 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt @@ -1,55 +1,143 @@ package dev.inmo.micro_utils.coroutines +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract class SmartKeyRWLocker( + globalLockerReadPermits: Int = Int.MAX_VALUE, + globalLockerWriteIsLocked: Boolean = false, private val perKeyReadPermits: Int = Int.MAX_VALUE ) { - private val internalRWLocker = SmartRWLocker() + private val globalRWLocker: SmartRWLocker = SmartRWLocker( + readPermits = globalLockerReadPermits, + writeIsLocked = globalLockerWriteIsLocked + ) private val lockers = mutableMapOf() + private val lockersMutex = Mutex() + private val lockersWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE) + private val globalWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE) private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) { SmartRWLocker(perKeyReadPermits) } + private suspend fun allocateLocker(key: T) = lockersMutex.withLock { + lockers.getOrPut(key) { + SmartRWLocker(perKeyReadPermits) + } + } - suspend fun writeMutex(key: T): SmartMutex.Immutable = internalRWLocker.withReadAcquire { + suspend fun writeMutex(key: T): SmartMutex.Immutable = globalRWLocker.withReadAcquire { allocateLockerWithoutLock(key).writeMutex } - suspend fun readSemaphore(key: T): SmartSemaphore.Immutable = internalRWLocker.withReadAcquire { + suspend fun readSemaphore(key: T): SmartSemaphore.Immutable = globalRWLocker.withReadAcquire { allocateLockerWithoutLock(key).readSemaphore } fun writeMutexOrNull(key: T): SmartMutex.Immutable? = lockers[key] ?.writeMutex fun readSemaphoreOrNull(key: T): SmartSemaphore.Immutable? = lockers[key] ?.readSemaphore + fun writeMutex(): SmartMutex.Immutable = globalRWLocker.writeMutex + fun readSemaphore(): SmartSemaphore.Immutable = globalRWLocker.readSemaphore + + suspend fun acquireRead() { + globalWritingLocker.acquire() + try { + lockersWritingLocker.waitReleaseAll() + globalRWLocker.acquireRead() + } catch (e: CancellationException) { + globalWritingLocker.release() + throw e + } + } + suspend fun releaseRead(): Boolean { + globalWritingLocker.release() + return globalRWLocker.releaseRead() + } + + suspend fun lockWrite() { + globalRWLocker.lockWrite() + } + suspend fun unlockWrite(): Boolean { + return globalRWLocker.unlockWrite() + } + fun isWriteLocked(): Boolean = globalRWLocker.writeMutex.isLocked == true + + suspend fun acquireRead(key: T) { - internalRWLocker.withReadAcquire { - val locker = allocateLockerWithoutLock(key) + globalRWLocker.acquireRead() + val locker = allocateLocker(key) + try { locker.acquireRead() + } catch (e: CancellationException) { + globalRWLocker.releaseRead() + throw e } } suspend fun releaseRead(key: T): Boolean { - return internalRWLocker.withReadAcquire { - lockers[key] - } ?.releaseRead() == true + val locker = allocateLocker(key) + return locker.releaseRead() && globalRWLocker.releaseRead() } suspend fun lockWrite(key: T) { - internalRWLocker.withWriteLock { - val locker = allocateLockerWithoutLock(key) - locker.lockWrite() + globalWritingLocker.withAcquire(globalWritingLocker.maxPermits) { + lockersWritingLocker.acquire() + } + try { + globalRWLocker.acquireRead() + try { + val locker = allocateLocker(key) + locker.lockWrite() + } catch (e: CancellationException) { + globalRWLocker.releaseRead() + throw e + } + } catch (e: CancellationException) { + lockersWritingLocker.release() + throw e } } suspend fun unlockWrite(key: T): Boolean { - return internalRWLocker.withWriteLock { - lockers[key] - } ?.unlockWrite() == true + val locker = allocateLocker(key) + return (locker.unlockWrite() && globalRWLocker.releaseRead()).also { + if (it) { + lockersWritingLocker.release() + } + } } fun isWriteLocked(key: T): Boolean = lockers[key] ?.writeMutex ?.isLocked == true } +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withReadAcquire(action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + acquireRead() + try { + return action() + } finally { + releaseRead() + } +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withWriteLock(action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + lockWrite() + try { + return action() + } finally { + unlockWrite() + } +} + @OptIn(ExperimentalContracts::class) suspend inline fun SmartKeyRWLocker.withReadAcquire(key: T, action: () -> R): R { contract { diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt index 08069672cd6..60391978d81 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt @@ -1,5 +1,6 @@ package dev.inmo.micro_utils.coroutines +import kotlinx.coroutines.CancellationException import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -39,7 +40,12 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: */ suspend fun lockWrite() { _writeMutex.lock() - _readSemaphore.acquire(readPermits) + try { + _readSemaphore.acquire(readPermits) + } catch (e: CancellationException) { + _writeMutex.unlock() + throw e + } } /** diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt index 29016f00a11..cef80cfab16 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt @@ -24,6 +24,7 @@ import kotlin.contracts.contract * [Mutable] creator */ sealed interface SmartSemaphore { + val maxPermits: Int val permitsStateFlow: StateFlow /** @@ -36,7 +37,7 @@ sealed interface SmartSemaphore { /** * Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow] */ - class Immutable(override val permitsStateFlow: StateFlow) : SmartSemaphore + class Immutable(override val permitsStateFlow: StateFlow, override val maxPermits: Int) : SmartSemaphore /** * Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create @@ -44,15 +45,16 @@ sealed interface SmartSemaphore { * * @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow] */ - class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { + class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { + override val maxPermits: Int = permits private val _freePermitsStateFlow = SpecialMutableStateFlow(permits - acquiredPermits) override val permitsStateFlow: StateFlow = _freePermitsStateFlow.asStateFlow() private val internalChangesMutex = Mutex(false) - fun immutable() = Immutable(permitsStateFlow) + fun immutable() = Immutable(permitsStateFlow, maxPermits) - private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits) + private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.maxPermits) /** * Holds call until this [SmartSemaphore] will be re-locked. That means that current method will @@ -126,10 +128,10 @@ sealed interface SmartSemaphore { */ suspend fun release(permits: Int = 1): Boolean { val checkedPermits = checkedPermits(permits) - return if (_freePermitsStateFlow.value < this.permits) { + return if (_freePermitsStateFlow.value < this.maxPermits) { internalChangesMutex.withLock { - if (_freePermitsStateFlow.value < this.permits) { - _freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits) + if (_freePermitsStateFlow.value < this.maxPermits) { + _freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits) true } else { false @@ -166,3 +168,4 @@ suspend inline fun SmartSemaphore.Mutable.withAcquire(permits: Int = 1, acti * the fact that some other parties may lock it again */ suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits } +suspend fun SmartSemaphore.waitReleaseAll() = permitsStateFlow.first { it == maxPermits } diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt index d4c8981a87b..5fcfb7850f7 100644 --- a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -63,13 +63,54 @@ class SmartKeyRWLockerTests { assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } } + val readPermitsBeforeLock = locker.readSemaphore().freePermits realWithTimeout(1.seconds) { locker.acquireRead() } locker.releaseRead() - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) locker.releaseRead(it) } + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + realWithTimeout(1.seconds) { locker.lockWrite() } + assertFails { + realWithTimeout(13.milliseconds) { locker.acquireRead() } + } + assertTrue { locker.unlockWrite() } + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + } + @Test + fun writesBlockingGlobalWrite() = runTest { + val locker = SmartKeyRWLocker() + + val testKeys = (0 until 100).map { "test$it" } + + for (i in testKeys.indices) { + val it = testKeys[i] + locker.lockWrite(it) + val previous = testKeys.take(i) + val next = testKeys.drop(i + 1) + + previous.forEach { + assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } + } + next.forEach { + assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true } + } + } + + for (i in testKeys.indices) { + val it = testKeys[i] + assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } } + + val readPermitsBeforeLock = locker.readSemaphore().freePermits + assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } + assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) + + locker.unlockWrite(it) + } + + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } realWithTimeout(1.seconds) { locker.lockWrite() } assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } diff --git a/coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt index 75fd251d17b..210ce222b15 100644 --- a/coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt @@ -6,7 +6,10 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds class SmartRWLockerTests { @Test @@ -148,4 +151,17 @@ class SmartRWLockerTests { assertEquals(false, locker.writeMutex.isLocked) } } + + @Test + fun exceptionOnLockingWillNotLockLocker() = runTest { + val locker = SmartRWLocker() + + locker.acquireRead() + assertFails { + realWithTimeout(1.seconds) { + locker.lockWrite() + } + } + assertFalse { locker.writeMutex.isLocked } + } } diff --git a/transactions/src/commonMain/kotlin/TransactionsDSL.kt b/transactions/src/commonMain/kotlin/TransactionsDSL.kt index 566a4aca628..a743b03c4a9 100644 --- a/transactions/src/commonMain/kotlin/TransactionsDSL.kt +++ b/transactions/src/commonMain/kotlin/TransactionsDSL.kt @@ -19,7 +19,7 @@ class RollbackContext internal constructor ( * * @param rollback Will be called if */ -suspend fun TransactionsDSL.rollbackableOperation( +suspend fun TransactionsDSL.rollableBackOperation( rollback: suspend RollbackContext.() -> Unit, action: suspend () -> T ): T { @@ -34,7 +34,7 @@ suspend fun TransactionsDSL.rollbackableOperation( } /** - * Starts transaction with opportunity to add actions [rollbackableOperation]. How to use: + * Starts transaction with opportunity to add actions [rollableBackOperation]. How to use: * * ```kotlin * doSuspendTransaction { diff --git a/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt b/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt index 5c1ab7c9973..cea9a944df3 100644 --- a/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt +++ b/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt @@ -1,5 +1,5 @@ import dev.inmo.micro_utils.transactions.doSuspendTransaction -import dev.inmo.micro_utils.transactions.rollbackableOperation +import dev.inmo.micro_utils.transactions.rollableBackOperation import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -19,7 +19,7 @@ class TransactionsDSLTests { val actionResult = doSuspendTransaction { dataCollections.forEachIndexed { i, _ -> - val resultData = rollbackableOperation({ + val resultData = rollableBackOperation({ dataCollections[i] = actionResult.copy(second = true) }) { val result = dataCollections[i] @@ -56,7 +56,7 @@ class TransactionsDSLTests { val actionResult = doSuspendTransaction { dataCollections.forEachIndexed { i, _ -> - val resultData = rollbackableOperation({ + val resultData = rollableBackOperation({ assertTrue(error === this.error) dataCollections[i] = actionResult.copy(second = true) }) { From 5447bf9691b7fc5194e7c3fbbee77bf9549cb627 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 24 Mar 2025 09:17:38 +0600 Subject: [PATCH 09/15] add one more test for SmartKeyRWLocker --- .../kotlin/SmartKeyRWLockerTests.kt | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt index 5fcfb7850f7..97436532cd6 100644 --- a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -14,7 +14,7 @@ import kotlin.time.Duration.Companion.seconds class SmartKeyRWLockerTests { @Test - fun lockKeyFailedOnGlobalLockTest() = runTest { + fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest { val locker = SmartKeyRWLocker() val testKey = "test" locker.lockWrite() @@ -39,6 +39,31 @@ class SmartKeyRWLockerTests { assertFalse { locker.isWriteLocked(testKey) } } @Test + fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { + val locker = SmartKeyRWLocker() + val testKey = "test" + locker.acquireRead() + + assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) + + assertFails { + realWithTimeout(1.seconds) { + locker.lockWrite(testKey) + } + } + assertFalse { locker.isWriteLocked(testKey) } + + locker.releaseRead() + assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) + + realWithTimeout(1.seconds) { + locker.lockWrite(testKey) + } + assertTrue { locker.isWriteLocked(testKey) } + assertTrue { locker.unlockWrite(testKey) } + assertFalse { locker.isWriteLocked(testKey) } + } + @Test fun readsBlockingGlobalWrite() = runTest { val locker = SmartKeyRWLocker() From 87a3a925ee6cc81b81a3f2b0790048fef97c0f38 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 24 Mar 2025 13:36:04 +0600 Subject: [PATCH 10/15] add multilocking for key-based lockers --- .../coroutines/SmartKeyRWLocker.kt | 41 +++++++++++++++++++ .../cache/full/FullKeyValuesCacheRepo.kt | 1 - .../direct/DirectFullKeyValueCacheRepo.kt | 3 -- .../direct/DirectFullKeyValuesCacheRepo.kt | 1 - 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt index ac296566760..017e1c5009f 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt @@ -152,6 +152,27 @@ suspend inline fun SmartKeyRWLocker.withReadAcquire(key: T, action: () } } +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withReadAcquires(keys: Iterable, action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + val acquired = mutableSetOf() + try { + keys.forEach { + acquireRead(it) + acquired.add(it) + } + return action() + } finally { + acquired.forEach { + releaseRead(it) + } + } +} +suspend inline fun SmartKeyRWLocker.withReadAcquires(vararg keys: T, action: () -> R): R = withReadAcquires(keys.asIterable(), action) + @OptIn(ExperimentalContracts::class) suspend inline fun SmartKeyRWLocker.withWriteLock(key: T, action: () -> R): R { contract { @@ -164,4 +185,24 @@ suspend inline fun SmartKeyRWLocker.withWriteLock(key: T, action: () - } finally { unlockWrite(key) } +} + +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartKeyRWLocker.withWriteLocks(keys: Iterable, action: () -> R): R { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + val locked = mutableSetOf() + try { + keys.forEach { + lockWrite(it) + locked.add(it) + } + return action() + } finally { + locked.forEach { + unlockWrite(it) + } + } } \ No newline at end of file diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt index 524afa732c8..e6cc8f73317 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt @@ -11,7 +11,6 @@ import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode import dev.inmo.micro_utils.repos.cache.util.actualizeAll -import dev.inmo.micro_utils.repos.pagination.maxPagePagination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt index 099d2b71d03..e257ec5c84e 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt @@ -8,9 +8,6 @@ import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.micro_utils.pagination.PaginationResult import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation -import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo -import dev.inmo.micro_utils.repos.cache.full.FullReadKeyValueCacheRepo -import dev.inmo.micro_utils.repos.cache.full.FullWriteKeyValueCacheRepo import dev.inmo.micro_utils.repos.cache.util.actualizeAll import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt index c4d3d827446..7829fd7520a 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt @@ -1,6 +1,5 @@ package dev.inmo.micro_utils.repos.cache.full.direct -import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.coroutines.SmartRWLocker import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.withReadAcquire From aac545074b034890677dcda301c15ddb31fc5f76 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 24 Mar 2025 14:02:12 +0600 Subject: [PATCH 11/15] fill changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2072fc62925..566065eac49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ * `Coroutines`: * Add extensions `SmartRWLocker.alsoWithUnlockingOnSuccessAsync` and `SmartRWLocker.alsoWithUnlockingOnSuccess` + * Fix of `SmartRWLocker.lockWrite` issue when it could lock write mutex without unlocking + * Add tool `SmartKeyRWLocker` + * `SmartSemaphore` got new property `maxPermits` + * New extension `SmartSemaphore.waitReleaseAll()` * `Transactions`: * Add `TransactionsDSL` From 04f82a03bfb5c9b1b6796d9fa17434236fd09e16 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 24 Mar 2025 14:21:22 +0600 Subject: [PATCH 12/15] add docs to SmartKeyRWLocker and SmartRWLocker --- .../micro_utils/coroutines/SmartKeyRWLocker.kt | 16 ++++++++++++++++ .../inmo/micro_utils/coroutines/SmartRWLocker.kt | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt index 017e1c5009f..407eec5add4 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt @@ -7,6 +7,22 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +/** + * Combining [globalRWLocker] and internal map of [SmartRWLocker] associated by [T] to provide next logic: + * + * * Locker by key, for reading: waiting for [globalRWLocker] unlock write by acquiring read permit in it and then + * taking or creating locker for key [T] and lock its reading too + * * Locker by key, for writing: waiting for [globalRWLocker] unlock write by acquiring read permit in it and then + * taking or creating locker for key [T] and lock its writing + * * [globalRWLocker], for reading: using [SmartRWLocker.acquireRead], will be suspended until its + * [SmartRWLocker.lockWrite] will not be unlocked + * * [globalRWLocker], for writing: using [SmartRWLocker.lockWrite], will be paused by other reading and writing + * operations and will pause other operations until the end of operation (calling of [unlockWrite]) + * + * You may see, that lockers by key still will use global locker permits - it is required to prevent [globalRWLocker] + * write locking during all other operations are not done. In fact, all the keys works like a simple [SmartRWLocker] as + * well, as [globalRWLocker], but they are linked with [globalRWLocker] [SmartRWLocker.acquireRead] permissions + */ class SmartKeyRWLocker( globalLockerReadPermits: Int = Int.MAX_VALUE, globalLockerWriteIsLocked: Boolean = false, diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt index 60391978d81..aea220812cd 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartRWLocker.kt @@ -8,9 +8,10 @@ import kotlin.contracts.contract /** * Composite mutex which works with next rules: * - * * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore] + * * [acquireRead] require to [writeMutex] to be free. Then it will take one lock from [readSemaphore] * * [releaseRead] will just free up one permit in [readSemaphore] - * * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed + * * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed. If coroutine will be + * cancelled during read semaphore freeing, locking will be cancelled too with [SmartMutex.Mutable.unlock]ing of [writeMutex] * * [unlockWrite] will just unlock [writeMutex] */ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) { From 28eb1a11e6cdc13831bee507065f39e5448d68cc Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 25 Mar 2025 17:27:34 +0600 Subject: [PATCH 13/15] add more tests for smart key rw locker tests --- .../kotlin/SmartKeyRWLockerTests.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt index 97436532cd6..70038c189b4 100644 --- a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -64,6 +64,56 @@ class SmartKeyRWLockerTests { assertFalse { locker.isWriteLocked(testKey) } } @Test + fun readLockFailedOnWriteLockKeyTest() = runTest { + val locker = SmartKeyRWLocker() + val testKey = "test" + locker.lockWrite(testKey) + + assertTrue { locker.isWriteLocked(testKey) } + + assertFails { + realWithTimeout(1.seconds) { + locker.acquireRead() + } + } + assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) + + locker.unlockWrite(testKey) + assertFalse { locker.isWriteLocked(testKey) } + + realWithTimeout(1.seconds) { + locker.acquireRead() + } + assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) + assertTrue { locker.releaseRead() } + assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits) + } + @Test + fun writeLockFailedOnWriteLockKeyTest() = runTest { + val locker = SmartKeyRWLocker() + val testKey = "test" + locker.lockWrite(testKey) + + assertTrue { locker.isWriteLocked(testKey) } + + assertFails { + realWithTimeout(1.seconds) { + locker.lockWrite() + } + } + assertFalse(locker.isWriteLocked()) + + locker.unlockWrite(testKey) + assertFalse { locker.isWriteLocked(testKey) } + + realWithTimeout(1.seconds) { + locker.lockWrite() + } + assertTrue(locker.isWriteLocked()) + assertTrue { locker.unlockWrite() } + assertFalse(locker.isWriteLocked()) + } + @Test fun readsBlockingGlobalWrite() = runTest { val locker = SmartKeyRWLocker() From dfd23f8d60d8eb28ebc44c422e75ec98f6d63f33 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 25 Mar 2025 17:42:32 +0600 Subject: [PATCH 14/15] update kotlin --- gradle/libs.versions.toml | 4 ++-- startup/launcher/build.gradle | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5cc8805cc4..0f32dc7d040 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kt = "2.1.10" +kt = "2.1.20" kt-serialization = "1.8.0" kt-coroutines = "1.10.1" @@ -23,7 +23,7 @@ koin = "4.0.2" okio = "3.10.2" -ksp = "2.1.10-1.0.31" +ksp = "2.1.20-1.0.31" kotlin-poet = "1.18.1" versions = "0.51.0" diff --git a/startup/launcher/build.gradle b/startup/launcher/build.gradle index f5622eda270..b8199cef920 100644 --- a/startup/launcher/build.gradle +++ b/startup/launcher/build.gradle @@ -1,13 +1,21 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" - id "application" +// id "application" id "com.google.devtools.ksp" } apply from: "$mppJvmJsLinuxMingwProject" kotlin { + jvm { + binaries { + // Configures a JavaExec task named "runJvm" and a Gradle distribution for the "main" compilation in this target + executable { + mainClass.set("dev.inmo.micro_utils.startup.launcher.MainKt") + } + } + } sourceSets { commonMain { dependencies { @@ -23,9 +31,9 @@ kotlin { } } -application { - mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt" -} +//application { +// mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt" +//} java { sourceCompatibility = JavaVersion.VERSION_17 From d46cc3b09c600f4f052137773954fd473ed0c085 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 25 Mar 2025 17:45:53 +0600 Subject: [PATCH 15/15] remove redundant commentaries --- startup/launcher/build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/startup/launcher/build.gradle b/startup/launcher/build.gradle index b8199cef920..b9943d52852 100644 --- a/startup/launcher/build.gradle +++ b/startup/launcher/build.gradle @@ -1,7 +1,6 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" -// id "application" id "com.google.devtools.ksp" } @@ -10,7 +9,6 @@ apply from: "$mppJvmJsLinuxMingwProject" kotlin { jvm { binaries { - // Configures a JavaExec task named "runJvm" and a Gradle distribution for the "main" compilation in this target executable { mainClass.set("dev.inmo.micro_utils.startup.launcher.MainKt") } @@ -31,10 +29,6 @@ kotlin { } } -//application { -// mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt" -//} - java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17