Compare commits

..

13 Commits

11 changed files with 144 additions and 22 deletions

View File

@@ -1,9 +1,23 @@
# Changelog
## 0.25.4
* `Versions`:
* `Ktor`: `3.1.1` -> `3.1.2`
* `Koin`: `4.0.2` -> `4.0.4`
* `Coroutines`:
* Add `SmartKeyRWLocker.withWriteLocks` extension with vararg keys
* `Transactions`:
* Fix order of rollback actions calling
## 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`

View File

@@ -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<T>(
globalLockerReadPermits: Int = Int.MAX_VALUE,
globalLockerWriteIsLocked: Boolean = false,
@@ -152,6 +168,27 @@ suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquire(key: T, action: ()
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquires(keys: Iterable<T>, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val acquired = mutableSetOf<T>()
try {
keys.forEach {
acquireRead(it)
acquired.add(it)
}
return action()
} finally {
acquired.forEach {
releaseRead(it)
}
}
}
suspend inline fun <T, R> SmartKeyRWLocker<T>.withReadAcquires(vararg keys: T, action: () -> R): R = withReadAcquires(keys.asIterable(), action)
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLock(key: T, action: () -> R): R {
contract {
@@ -164,4 +201,26 @@ suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLock(key: T, action: () -
} finally {
unlockWrite(key)
}
}
}
@OptIn(ExperimentalContracts::class)
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(keys: Iterable<T>, action: () -> R): R {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
val locked = mutableSetOf<T>()
try {
keys.forEach {
lockWrite(it)
locked.add(it)
}
return action()
} finally {
locked.forEach {
unlockWrite(it)
}
}
}
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLocks(vararg keys: T, action: () -> R): R = withWriteLocks(keys.asIterable(), action)

View File

@@ -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) {

View File

@@ -64,6 +64,56 @@ class SmartKeyRWLockerTests {
assertFalse { locker.isWriteLocked(testKey) }
}
@Test
fun readLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
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<String>()
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<String>()

View File

@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.25.3
android_code_version=293
version=0.25.4
android_code_version=294

View File

@@ -1,6 +1,6 @@
[versions]
kt = "2.1.10"
kt = "2.1.20"
kt-serialization = "1.8.0"
kt-coroutines = "1.10.1"
@@ -15,15 +15,15 @@ sqlite = "3.49.1.0"
korlibs = "5.4.0"
uuid = "0.8.4"
ktor = "3.1.1"
ktor = "3.1.2"
gh-release = "2.5.2"
koin = "4.0.2"
koin = "4.0.4"
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"

View File

@@ -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.*

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,7 @@ package dev.inmo.micro_utils.transactions
typealias TransactionDSLRollbackLambda = suspend (Throwable) -> Unit
class TransactionsDSL internal constructor() {
internal val rollbackActions = LinkedHashSet<TransactionDSLRollbackLambda>()
internal val rollbackActions = ArrayList<TransactionDSLRollbackLambda>()
internal fun addRollbackAction(rollbackAction: TransactionDSLRollbackLambda) {
rollbackActions.add(rollbackAction)
@@ -71,9 +71,10 @@ suspend fun <T> doSuspendTransaction(
return runCatching {
transactionsDSL.block()
}.onFailure { e ->
transactionsDSL.rollbackActions.forEach {
for (i in transactionsDSL.rollbackActions.lastIndex downTo 0) {
val rollbackAction = transactionsDSL.rollbackActions[i]
runCatching {
it.invoke(e)
rollbackAction.invoke(e)
}.onFailure { ee ->
onRollbackStepError(ee)
}