mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-11-04 06:00:22 +00:00 
			
		
		
		
	
							
								
								
									
										11
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								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`:
 | 
			
		||||
 
 | 
			
		||||
@@ -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<HEXAColor> {
 | 
			
		||||
    /**
 | 
			
		||||
     * @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(),
 | 
			
		||||
 
 | 
			
		||||
@@ -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<T>(
 | 
			
		||||
    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<T, SmartRWLocker>()
 | 
			
		||||
    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 <T, R> SmartKeyRWLocker<T>.withReadAcquire(action: () -> R): R {
 | 
			
		||||
    contract {
 | 
			
		||||
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    acquireRead()
 | 
			
		||||
    try {
 | 
			
		||||
        return action()
 | 
			
		||||
    } finally {
 | 
			
		||||
        releaseRead()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalContracts::class)
 | 
			
		||||
suspend inline fun <T, R> SmartKeyRWLocker<T>.withWriteLock(action: () -> R): R {
 | 
			
		||||
    contract {
 | 
			
		||||
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lockWrite()
 | 
			
		||||
    try {
 | 
			
		||||
        return action()
 | 
			
		||||
    } finally {
 | 
			
		||||
        unlockWrite()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalContracts::class)
 | 
			
		||||
suspend inline fun <T, R> SmartKeyRWLocker<T>.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 <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 {
 | 
			
		||||
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lockWrite(key)
 | 
			
		||||
    try {
 | 
			
		||||
        return 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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Unit> {
 | 
			
		||||
    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)
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +24,7 @@ import kotlin.contracts.contract
 | 
			
		||||
 * [Mutable] creator
 | 
			
		||||
 */
 | 
			
		||||
sealed interface SmartSemaphore {
 | 
			
		||||
    val maxPermits: Int
 | 
			
		||||
    val permitsStateFlow: StateFlow<Int>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -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<Int>) : SmartSemaphore
 | 
			
		||||
    class Immutable(override val permitsStateFlow: StateFlow<Int>, 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<Int>(permits - acquiredPermits)
 | 
			
		||||
        override val permitsStateFlow: StateFlow<Int> = _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 <T> 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 }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								coroutines/src/commonTest/kotlin/RealTimeOut.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								coroutines/src/commonTest/kotlin/RealTimeOut.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import kotlinx.coroutines.withTimeout
 | 
			
		||||
import kotlin.time.Duration
 | 
			
		||||
 | 
			
		||||
suspend fun <T> realWithTimeout(time: Duration, block: suspend () -> T): T {
 | 
			
		||||
    return withContext(Dispatchers.Default.limitedParallelism(1)) {
 | 
			
		||||
        withTimeout(time) {
 | 
			
		||||
            block()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										196
									
								
								coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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<String>()
 | 
			
		||||
        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<String>()
 | 
			
		||||
        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<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>()
 | 
			
		||||
 | 
			
		||||
        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<String>()
 | 
			
		||||
 | 
			
		||||
        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 }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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.*
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ rootProject.name='micro_utils'
 | 
			
		||||
String[] includes = [
 | 
			
		||||
    ":common",
 | 
			
		||||
    ":common:compose",
 | 
			
		||||
    ":transactions",
 | 
			
		||||
    ":matrix",
 | 
			
		||||
    ":safe_wrapper",
 | 
			
		||||
    ":crypto",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								transactions/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								transactions/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    id "org.jetbrains.kotlin.multiplatform"
 | 
			
		||||
    id "org.jetbrains.kotlin.plugin.serialization"
 | 
			
		||||
    id "com.android.library"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"
 | 
			
		||||
							
								
								
									
										82
									
								
								transactions/src/commonMain/kotlin/TransactionsDSL.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								transactions/src/commonMain/kotlin/TransactionsDSL.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
package dev.inmo.micro_utils.transactions
 | 
			
		||||
 | 
			
		||||
typealias TransactionDSLRollbackLambda = suspend (Throwable) -> Unit
 | 
			
		||||
class TransactionsDSL internal constructor() {
 | 
			
		||||
    internal val rollbackActions = LinkedHashSet<TransactionDSLRollbackLambda>()
 | 
			
		||||
 | 
			
		||||
    internal fun addRollbackAction(rollbackAction: TransactionDSLRollbackLambda) {
 | 
			
		||||
        rollbackActions.add(rollbackAction)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
class RollbackContext<T> 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 <T> TransactionsDSL.rollableBackOperation(
 | 
			
		||||
    rollback: suspend RollbackContext<T>.() -> 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 <T> doSuspendTransaction(
 | 
			
		||||
    onRollbackStepError: suspend (Throwable) -> Unit = { },
 | 
			
		||||
    block: suspend TransactionsDSL.() -> T
 | 
			
		||||
): Result<T> {
 | 
			
		||||
    val transactionsDSL = TransactionsDSL()
 | 
			
		||||
 | 
			
		||||
    return runCatching {
 | 
			
		||||
        transactionsDSL.block()
 | 
			
		||||
    }.onFailure { e ->
 | 
			
		||||
        transactionsDSL.rollbackActions.forEach {
 | 
			
		||||
            runCatching {
 | 
			
		||||
                it.invoke(e)
 | 
			
		||||
            }.onFailure { ee ->
 | 
			
		||||
                onRollbackStepError(ee)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								transactions/src/commonTest/kotlin/TransactionsDSLTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								transactions/src/commonTest/kotlin/TransactionsDSLTests.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user