diff --git a/CHANGELOG.md b/CHANGELOG.md index aea531e3d55..566065eac49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.25.3 + +* `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` + ## 0.25.2 * `Versions`: 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(), 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..407eec5add4 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartKeyRWLocker.kt @@ -0,0 +1,224 @@ +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 + +/** + * 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, + private val perKeyReadPermits: Int = Int.MAX_VALUE +) { + 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 = globalRWLocker.withReadAcquire { + allocateLockerWithoutLock(key).writeMutex + } + 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) { + globalRWLocker.acquireRead() + val locker = allocateLocker(key) + try { + locker.acquireRead() + } catch (e: CancellationException) { + globalRWLocker.releaseRead() + throw e + } + } + suspend fun releaseRead(key: T): Boolean { + val locker = allocateLocker(key) + return locker.releaseRead() && globalRWLocker.releaseRead() + } + + suspend fun lockWrite(key: T) { + 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 { + 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 { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + acquireRead(key) + try { + return action() + } finally { + releaseRead(key) + } +} + +@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 { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + lockWrite(key) + try { + return 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/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..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 @@ -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 @@ -7,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) { @@ -39,7 +41,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/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/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/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..70038c189b4 --- /dev/null +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -0,0 +1,196 @@ +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 writeLockKeyFailedOnGlobalWriteLockTest() = 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 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 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() + + 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() } + } + val readPermitsBeforeLock = locker.readSemaphore().freePermits + realWithTimeout(1.seconds) { locker.acquireRead() } + locker.releaseRead() + 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() } + } + assertTrue { locker.unlockWrite() } + assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + } +} 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/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 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/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 { /** 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 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/startup/launcher/build.gradle b/startup/launcher/build.gradle index f5622eda270..b9943d52852 100644 --- a/startup/launcher/build.gradle +++ b/startup/launcher/build.gradle @@ -1,13 +1,19 @@ plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" - id "application" id "com.google.devtools.ksp" } apply from: "$mppJvmJsLinuxMingwProject" kotlin { + jvm { + binaries { + executable { + mainClass.set("dev.inmo.micro_utils.startup.launcher.MainKt") + } + } + } sourceSets { commonMain { dependencies { @@ -23,10 +29,6 @@ kotlin { } } -application { - mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt" -} - java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 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..a743b03c4a9 --- /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.rollableBackOperation( + 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 [rollableBackOperation]. 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 (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(ee) + } + } + } +} diff --git a/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt b/transactions/src/commonTest/kotlin/TransactionsDSLTests.kt new file mode 100644 index 00000000000..cea9a944df3 --- /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.rollableBackOperation +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 = rollableBackOperation({ + 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 = rollableBackOperation({ + 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