mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-10 10:56:36 +00:00
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# 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
|
## 0.25.2
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
@@ -13,7 +13,7 @@ import kotlin.math.floor
|
|||||||
*
|
*
|
||||||
* Anyway it is recommended to use
|
* 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
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
@@ -21,18 +21,18 @@ value class HEXAColor (
|
|||||||
val hexaUInt: UInt
|
val hexaUInt: UInt
|
||||||
) : Comparable<HEXAColor> {
|
) : 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
|
val hexa: String
|
||||||
get() = "#${hexaUInt.toString(16).padStart(8, '0')}"
|
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
|
val hex: String
|
||||||
get() = hexa.take(7)
|
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
|
val ahex: String
|
||||||
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
|
get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}"
|
||||||
@@ -140,12 +140,12 @@ value class HEXAColor (
|
|||||||
}.lowercase().toUInt(16).let(::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)
|
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(
|
fun fromAhex(uint: UInt) = HEXAColor(
|
||||||
a = ((uint and 0xff000000u) / 0x1000000u).toInt(),
|
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
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@@ -7,9 +8,10 @@ import kotlin.contracts.contract
|
|||||||
/**
|
/**
|
||||||
* Composite mutex which works with next rules:
|
* 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]
|
* * [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]
|
* * [unlockWrite] will just unlock [writeMutex]
|
||||||
*/
|
*/
|
||||||
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
|
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() {
|
suspend fun lockWrite() {
|
||||||
_writeMutex.lock()
|
_writeMutex.lock()
|
||||||
|
try {
|
||||||
_readSemaphore.acquire(readPermits)
|
_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
|
* [Mutable] creator
|
||||||
*/
|
*/
|
||||||
sealed interface SmartSemaphore {
|
sealed interface SmartSemaphore {
|
||||||
|
val maxPermits: Int
|
||||||
val permitsStateFlow: StateFlow<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]
|
* 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
|
* 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]
|
* @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)
|
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
|
||||||
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
|
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
|
||||||
|
|
||||||
private val internalChangesMutex = Mutex(false)
|
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
|
* 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 {
|
suspend fun release(permits: Int = 1): Boolean {
|
||||||
val checkedPermits = checkedPermits(permits)
|
val checkedPermits = checkedPermits(permits)
|
||||||
return if (_freePermitsStateFlow.value < this.permits) {
|
return if (_freePermitsStateFlow.value < this.maxPermits) {
|
||||||
internalChangesMutex.withLock {
|
internalChangesMutex.withLock {
|
||||||
if (_freePermitsStateFlow.value < this.permits) {
|
if (_freePermitsStateFlow.value < this.maxPermits) {
|
||||||
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits)
|
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
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
|
* the fact that some other parties may lock it again
|
||||||
*/
|
*/
|
||||||
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
|
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = 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 kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class SmartRWLockerTests {
|
class SmartRWLockerTests {
|
||||||
@Test
|
@Test
|
||||||
@@ -148,4 +151,17 @@ class SmartRWLockerTests {
|
|||||||
assertEquals(false, locker.writeMutex.isLocked)
|
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
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.25.2
|
version=0.25.3
|
||||||
android_code_version=292
|
android_code_version=293
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "2.1.10"
|
kt = "2.1.20"
|
||||||
kt-serialization = "1.8.0"
|
kt-serialization = "1.8.0"
|
||||||
kt-coroutines = "1.10.1"
|
kt-coroutines = "1.10.1"
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ koin = "4.0.2"
|
|||||||
|
|
||||||
okio = "3.10.2"
|
okio = "3.10.2"
|
||||||
|
|
||||||
ksp = "2.1.10-1.0.31"
|
ksp = "2.1.20-1.0.31"
|
||||||
kotlin-poet = "1.18.1"
|
kotlin-poet = "1.18.1"
|
||||||
|
|
||||||
versions = "0.51.0"
|
versions = "0.51.0"
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package dev.inmo.micro_utils.repos.cache
|
package dev.inmo.micro_utils.repos.cache
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||||
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
|
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
interface InvalidatableRepo {
|
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.annotations.OverrideRequireManualInvalidation
|
||||||
import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode
|
import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode
|
||||||
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
|
import dev.inmo.micro_utils.repos.cache.util.actualizeAll
|
||||||
import dev.inmo.micro_utils.repos.pagination.maxPagePagination
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
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.pagination.PaginationResult
|
||||||
import dev.inmo.micro_utils.repos.*
|
import dev.inmo.micro_utils.repos.*
|
||||||
import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation
|
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 dev.inmo.micro_utils.repos.cache.util.actualizeAll
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package dev.inmo.micro_utils.repos.cache.full.direct
|
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.SmartRWLocker
|
||||||
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
|
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
|
||||||
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
import dev.inmo.micro_utils.coroutines.withReadAcquire
|
||||||
|
@@ -3,6 +3,7 @@ rootProject.name='micro_utils'
|
|||||||
String[] includes = [
|
String[] includes = [
|
||||||
":common",
|
":common",
|
||||||
":common:compose",
|
":common:compose",
|
||||||
|
":transactions",
|
||||||
":matrix",
|
":matrix",
|
||||||
":safe_wrapper",
|
":safe_wrapper",
|
||||||
":crypto",
|
":crypto",
|
||||||
|
@@ -1,13 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "application"
|
|
||||||
id "com.google.devtools.ksp"
|
id "com.google.devtools.ksp"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppJvmJsLinuxMingwProject"
|
apply from: "$mppJvmJsLinuxMingwProject"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
binaries {
|
||||||
|
executable {
|
||||||
|
mainClass.set("dev.inmo.micro_utils.startup.launcher.MainKt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -23,10 +29,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
|
||||||
mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = 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