Compare commits

..

23 Commits

Author SHA1 Message Date
d46cc3b09c remove redundant commentaries 2025-03-25 17:45:53 +06:00
dfd23f8d60 update kotlin 2025-03-25 17:42:32 +06:00
28eb1a11e6 add more tests for smart key rw locker tests 2025-03-25 17:27:34 +06:00
04f82a03bf add docs to SmartKeyRWLocker and SmartRWLocker 2025-03-24 14:21:36 +06:00
aac545074b fill changelog 2025-03-24 14:02:12 +06:00
87a3a925ee add multilocking for key-based lockers 2025-03-24 13:36:04 +06:00
5447bf9691 add one more test for SmartKeyRWLocker 2025-03-24 09:17:38 +06:00
761070b9b7 potentially first version of SmartKeyRWLocker 2025-03-23 11:16:45 +06:00
4c9e435df8 temporal realization of SmartKeyRWLocker 2025-03-22 16:02:08 +06:00
4b7d65e8b4 small logic fix in transactions dsl 2025-03-20 18:04:19 +06:00
0515b49b98 add transactions tests 2025-03-20 18:02:44 +06:00
edb97215ef add TransactionsDSL 2025-03-20 17:28:28 +06:00
5577a24548 fix of docs for HEXAColor 2025-03-19 18:10:20 +06:00
cfaa2a8927 add alsoWithUnlockingOnSuccess and alsoWithUnlockingOnSuccessAsync 2025-03-15 20:43:17 +06:00
78494b6036 start 0.25.3 2025-03-15 20:33:55 +06:00
f9ea7eca61 Merge pull request #557 from InsanusMokrassar/0.25.2
0.25.2
2025-03-14 16:40:12 +06:00
d69fee1732 add @OverrideRequireManualInvalidation to all invalidates supposed to be called in init block 2025-03-14 16:07:07 +06:00
178518db5e alsoInvalidateSync, alsoInvalidateSyncLogging, alsoDoInvalidate -> alsoInvalidateAsync 2025-03-14 15:01:22 +06:00
6fb20fb973 get back reflect in jvmMain of koin module 2025-03-14 14:16:46 +06:00
831bf44e34 alsoInvalidate, alsoDoInvalidate, singleSuspend, factorySuspend 2025-03-14 14:13:58 +06:00
a4c6c367e3 update exposed 2025-03-12 08:53:05 +06:00
bc98e59709 start 0.25.2 2025-03-12 08:41:51 +06:00
ef287bc331 Merge pull request #556 from InsanusMokrassar/0.25.1
0.25.1
2025-03-08 12:26:41 +06:00
41 changed files with 910 additions and 37 deletions

View File

@@ -1,5 +1,26 @@
# 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
* `Versions`:
* `Exposed`: `0.59.0` -> `0.60.0`
* `Repo`:
* `Cache`:
* Add extensions `alsoInvalidate`, `alsoInvalidateAsync`, `alsoInvalidateSync` and `alsoInvalidateSyncLogging`
* `Koin`:
* Add extensions `singleSuspend` and `factorySuspend` for defining of dependencies with suspendable blocks
## 0.25.1 ## 0.25.1
* `Coroutines`: * `Coroutines`:

View File

@@ -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(),

View File

@@ -71,7 +71,7 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap) it.invoke(markersMap)
} }
val job = subscribeSafelyWithoutExceptions(subscope) { data -> val job = subscribeLoggingDropExceptions(subscope) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
actor.send( actor.send(
AsyncSubscriptionCommandClearReceiver(marker) AsyncSubscriptionCommandClearReceiver(marker)

View File

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

View File

@@ -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()
_readSemaphore.acquire(readPermits) try {
_readSemaphore.acquire(readPermits)
} catch (e: CancellationException) {
_writeMutex.unlock()
throw e
}
} }
/** /**

View File

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

View File

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

View 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()
}
}
}

View 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 }
}
}

View File

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

View File

@@ -7,7 +7,9 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
val objectToSynchronize = Object() val objectToSynchronize = Object()
synchronized(objectToSynchronize) { synchronized(objectToSynchronize) {
launch(start = CoroutineStart.UNDISPATCHED) { launch(start = CoroutineStart.UNDISPATCHED) {
result = safelyWithResult(block) result = runCatching {
block()
}
}.invokeOnCompletion { }.invokeOnCompletion {
synchronized(objectToSynchronize) { synchronized(objectToSynchronize) {
objectToSynchronize.notifyAll() objectToSynchronize.notifyAll()

View File

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

View File

@@ -1,13 +1,13 @@
[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"
kslog = "1.4.1" kslog = "1.4.1"
jb-compose = "1.7.3" jb-compose = "1.7.3"
jb-exposed = "0.59.0" jb-exposed = "0.60.0"
jb-dokka = "2.0.0" jb-dokka = "2.0.0"
sqlite = "3.49.1.0" sqlite = "3.49.1.0"
@@ -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"

View File

@@ -17,11 +17,13 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
api libs.kt.reflect api libs.kt.reflect
api project(":micro_utils.coroutines")
} }
} }
androidMain { androidMain {
dependencies { dependencies {
api libs.kt.reflect api libs.kt.reflect
api project(":micro_utils.coroutines")
} }
} }
} }

View File

@@ -0,0 +1,32 @@
package dev.inmo.micro_utils.koin
import dev.inmo.micro_utils.coroutines.doSynchronously
import kotlinx.coroutines.CoroutineScope
import org.koin.core.module.Module
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.Qualifier
import org.koin.core.qualifier.StringQualifier
import org.koin.core.scope.Scope
import kotlin.reflect.KClass
inline fun <reified T : Any> Module.factorySuspend(
qualifier: Qualifier? = null,
coroutineScope: CoroutineScope? = null,
noinline definition: suspend Scope.(ParametersHolder) -> T
) = factory(
qualifier,
if (coroutineScope == null) {
{
doSynchronously {
definition(it)
}
}
} else {
{
coroutineScope.doSynchronously {
definition(it)
}
}
}
)

View File

@@ -0,0 +1,32 @@
package dev.inmo.micro_utils.koin
import dev.inmo.micro_utils.coroutines.doSynchronously
import kotlinx.coroutines.CoroutineScope
import org.koin.core.module.Module
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.StringQualifier
import org.koin.core.scope.Scope
inline fun <reified T : Any> Module.singleSuspend(
qualifier: StringQualifier,
createdAtStart: Boolean = false,
coroutineScope: CoroutineScope? = null,
noinline definition: suspend Scope.(ParametersHolder) -> T
) = single(
qualifier,
createdAtStart,
if (coroutineScope == null) {
{
doSynchronously {
definition(it)
}
}
} else {
{
coroutineScope.doSynchronously {
definition(it)
}
}
}
)

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.repos.annotations
@RequiresOptIn(
"Overriding of this invalidate message requires manual launching of invalidation on class initialization process",
RequiresOptIn.Level.WARNING
)
@Target(
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY,
AnnotationTarget.FUNCTION,
)
annotation class OverrideRequireManualInvalidation

View File

@@ -4,6 +4,7 @@ import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
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.cache.cache.KVCache import dev.inmo.micro_utils.repos.cache.cache.KVCache
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
@@ -39,6 +40,7 @@ open class ReadCRUDCacheRepo<ObjectType, IdType>(
kvCache.contains(id) kvCache.contains(id)
} || parentRepo.contains(id) } || parentRepo.contains(id)
@OverrideRequireManualInvalidation
override suspend fun invalidate() = locker.withWriteLock { override suspend fun invalidate() = locker.withWriteLock {
kvCache.clear() kvCache.clear()
} }
@@ -117,6 +119,7 @@ open class WriteCRUDCacheRepo<ObjectType, IdType, InputValueType>(
return created return created
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() = locker.withWriteLock { override suspend fun invalidate() = locker.withWriteLock {
kvCache.clear() kvCache.clear()
} }
@@ -150,6 +153,7 @@ WriteCRUDRepo<ObjectType, IdType, InputValueType> by WriteCRUDCacheRepo(
idGetter idGetter
), ),
CRUDRepo<ObjectType, IdType, InputValueType> { CRUDRepo<ObjectType, IdType, InputValueType> {
@OverrideRequireManualInvalidation
override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker)
} }

View File

@@ -1,5 +1,10 @@
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
interface InvalidatableRepo { interface InvalidatableRepo {
/** /**
* Invalidates its internal data. It __may__ lead to autoreload of data. In case when repo makes autoreload, * Invalidates its internal data. It __may__ lead to autoreload of data. In case when repo makes autoreload,
@@ -8,4 +13,14 @@ interface InvalidatableRepo {
suspend fun invalidate() suspend fun invalidate()
} }
suspend fun <T : InvalidatableRepo> T.alsoInvalidate() = also {
invalidate()
}
fun <T : InvalidatableRepo> T.alsoInvalidateAsync(scope: CoroutineScope) = also {
scope.launchLoggingDropExceptions {
invalidate()
}
}
typealias CacheRepo = InvalidatableRepo typealias CacheRepo = InvalidatableRepo

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
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.cache.cache.KVCache import dev.inmo.micro_utils.repos.cache.cache.KVCache
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
@@ -49,6 +50,7 @@ open class ReadKeyValueCacheRepo<Key,Value>(
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker)
} }

View File

@@ -6,6 +6,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.* import dev.inmo.micro_utils.pagination.utils.*
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.cache.cache.KVCache import dev.inmo.micro_utils.repos.cache.cache.KVCache
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
@@ -48,6 +49,7 @@ open class ReadKeyValuesCacheRepo<Key,Value>(
kvCache.contains(k) kvCache.contains(k)
} || parentRepo.contains(k) } || parentRepo.contains(k)
@OverrideRequireManualInvalidation
override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker) override suspend fun invalidate() = kvCache.actualizeAll(parentRepo, locker = locker)
} }

View File

@@ -6,6 +6,7 @@ import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.ReadCRUDRepo import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
@@ -90,6 +91,7 @@ open class AutoRecacheReadCRUDRepo<RegisteredObject, Id>(
kvCache.set(idGetter(it), it) kvCache.set(idGetter(it), it)
} ?: kvCache.get(id) } ?: kvCache.get(id)
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@@ -2,6 +2,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.crud
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
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.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -53,6 +54,7 @@ open class AutoRecacheWriteCRUDRepo<RegisteredObject, Id, InputObject>(
kvCache.set(idGetter(it), it) kvCache.set(idGetter(it), it)
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.clear() kvCache.clear()
} }

View File

@@ -6,6 +6,7 @@ import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValueRepo import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
@@ -100,6 +101,7 @@ open class AutoRecacheReadKeyValueRepo<Id, RegisteredObject>(
originalRepo.keys(v, pagination, reversed) originalRepo.keys(v, pagination, reversed)
}.getOrElse { kvCache.keys(v, pagination, reversed) } }.getOrElse { kvCache.keys(v, pagination, reversed) }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@@ -2,6 +2,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.keyvalue
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
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.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -44,6 +45,7 @@ open class AutoRecacheWriteKeyValueRepo<Id, RegisteredObject>(
kvCache.set(toSet) kvCache.set(toSet)
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.clear() kvCache.clear()
} }

View File

@@ -13,6 +13,7 @@ import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.ReadKeyValuesRepo import dev.inmo.micro_utils.repos.ReadKeyValuesRepo
import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation
import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper
import dev.inmo.micro_utils.repos.cache.util.actualizeAll import dev.inmo.micro_utils.repos.cache.util.actualizeAll
import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
@@ -140,6 +141,7 @@ open class AutoRecacheReadKeyValuesRepo<Id, RegisteredObject>(
}) ?: (kvCache.get(k) ?.contains(v) == true) }) ?: (kvCache.get(k) ?.contains(v) == true)
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@@ -3,6 +3,7 @@ package dev.inmo.micro_utils.repos.cache.fallback.keyvalues
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
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.cache.FallbackCacheRepo import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo
import dev.inmo.micro_utils.repos.pagination.maxPagePagination import dev.inmo.micro_utils.repos.pagination.maxPagePagination
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -89,6 +90,7 @@ open class AutoRecacheWriteKeyValuesRepo<Id, RegisteredObject>(
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.clear() kvCache.clear()
} }

View File

@@ -8,6 +8,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination 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.cache.* import dev.inmo.micro_utils.repos.cache.*
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
@@ -133,6 +134,7 @@ open class FullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@@ -8,6 +8,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination 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.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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -86,6 +87,7 @@ open class FullReadKeyValueCacheRepo<Key,Value>(
{ if (it.results.isNotEmpty()) actualizeAll() } { if (it.results.isNotEmpty()) actualizeAll() }
) )
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }
@@ -160,6 +162,7 @@ open class FullKeyValueCacheRepo<Key,Value>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker) kvCache.actualizeAll(parentRepo, locker)
} }

View File

@@ -8,9 +8,9 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.* import dev.inmo.micro_utils.pagination.utils.*
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.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.*
@@ -153,6 +153,7 @@ open class FullReadKeyValuesCacheRepo<Key,Value>(
{ if (it.results.isNotEmpty()) actualizeAll() } { if (it.results.isNotEmpty()) actualizeAll() }
) )
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }
@@ -235,6 +236,7 @@ open class FullKeyValuesCacheRepo<Key,Value>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker = locker) kvCache.actualizeAll(parentRepo, locker = locker)
} }

View File

@@ -1,11 +1,13 @@
package dev.inmo.micro_utils.repos.cache.full.direct package dev.inmo.micro_utils.repos.cache.full.direct
import dev.inmo.micro_utils.common.Warning
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
import dev.inmo.micro_utils.pagination.Pagination 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.cache.* import dev.inmo.micro_utils.repos.cache.*
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
@@ -96,6 +98,8 @@ open class DirectFullCRUDCacheRepo<ObjectType, IdType, InputValueType>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }

View File

@@ -7,9 +7,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.Pagination 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.cache.full.FullKeyValueCacheRepo import dev.inmo.micro_utils.repos.annotations.OverrideRequireManualInvalidation
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
@@ -54,6 +52,7 @@ open class DirectFullReadKeyValueCacheRepo<Key, Value>(
kvCache.keys(v, pagination, reversed) kvCache.keys(v, pagination, reversed)
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }
@@ -86,6 +85,7 @@ open class DirectFullWriteKeyValueCacheRepo<Key, Value>(
} }
}.launchIn(scope) }.launchIn(scope)
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
locker.withWriteLock { locker.withWriteLock {
kvCache.clear() kvCache.clear()
@@ -135,6 +135,7 @@ open class DirectFullKeyValueCacheRepo<Key, Value>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker) kvCache.actualizeAll(parentRepo, locker)
} }

View File

@@ -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
@@ -8,6 +7,7 @@ import dev.inmo.micro_utils.coroutines.withWriteLock
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.pagination.utils.* import dev.inmo.micro_utils.pagination.utils.*
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.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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -88,6 +88,7 @@ open class DirectFullReadKeyValuesCacheRepo<Key,Value>(
return result ?: emptyPaginationResult() return result ?: emptyPaginationResult()
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
actualizeAll() actualizeAll()
} }
@@ -121,6 +122,7 @@ open class DirectFullWriteKeyValuesCacheRepo<Key,Value>(
} }
}.launchIn(scope) }.launchIn(scope)
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
locker.withWriteLock { locker.withWriteLock {
kvCache.clear() kvCache.clear()
@@ -170,6 +172,7 @@ open class DirectFullKeyValuesCacheRepo<Key,Value>(
locker.unlockWrite() locker.unlockWrite()
} }
} }
@OverrideRequireManualInvalidation
override suspend fun invalidate() { override suspend fun invalidate() {
kvCache.actualizeAll(parentRepo, locker = locker) kvCache.actualizeAll(parentRepo, locker = locker)
} }

View File

@@ -0,0 +1,54 @@
package dev.inmo.micro_utils.repos.cache
import dev.inmo.kslog.common.KSLog
import dev.inmo.micro_utils.coroutines.doSynchronously
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import kotlinx.coroutines.CoroutineScope
fun <T : InvalidatableRepo> T.alsoInvalidateSync(
scope: CoroutineScope,
onFailure: suspend (Throwable) -> Unit = {},
) = also {
scope.doSynchronously {
runCatching {
invalidate()
}.onFailure {
onFailure(it)
}
}
}
fun <T : InvalidatableRepo> T.alsoInvalidateSync(
onFailure: suspend (Throwable) -> Unit = {},
) = also {
doSynchronously {
runCatching {
invalidate()
}.onFailure {
onFailure(it)
}
}
}
fun <T : InvalidatableRepo> T.alsoInvalidateSyncLogging(
scope: CoroutineScope,
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
) = also {
scope.doSynchronously {
runCatchingLogging(errorMessageBuilder, logger) {
invalidate()
}
}
}
fun <T : InvalidatableRepo> T.alsoInvalidateSyncLogging(
errorMessageBuilder: CoroutineScope.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
) = also {
doSynchronously {
runCatchingLogging(errorMessageBuilder, logger) {
invalidate()
}
}
}

View File

@@ -73,7 +73,7 @@ class KtorCRUDRepoTests : CommonCRUDRepoTests() {
} }
val server = io.ktor.server.engine.embeddedServer( val server = io.ktor.server.engine.embeddedServer(
CIO, CIO,
34567, 34568,
"127.0.0.1" "127.0.0.1"
) { ) {
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -100,7 +100,7 @@ class KtorCRUDRepoTests : CommonCRUDRepoTests() {
} }
} }
val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>( val crudClient = KtorCRUDRepoClient<ComplexData, Int, SimpleData>(
"http://127.0.0.1:34567", "http://127.0.0.1:34568",
client, client,
ContentType.Application.Json ContentType.Application.Json
) { ) {

View File

@@ -63,7 +63,7 @@ class KtorKeyValueRepoTests : CommonKeyValueRepoTests() {
val repo = MapKeyValueRepo<Int, ComplexData>(map) val repo = MapKeyValueRepo<Int, ComplexData>(map)
val server = io.ktor.server.engine.embeddedServer( val server = io.ktor.server.engine.embeddedServer(
CIO, CIO,
34567, 34569,
"127.0.0.1" "127.0.0.1"
) { ) {
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -91,7 +91,7 @@ class KtorKeyValueRepoTests : CommonKeyValueRepoTests() {
} }
} }
val crudClient = KtorKeyValueRepoClient<Int, ComplexData>( val crudClient = KtorKeyValueRepoClient<Int, ComplexData>(
"http://127.0.0.1:34567", "http://127.0.0.1:34569",
client, client,
ContentType.Application.Json, ContentType.Application.Json,
Int.serializer(), Int.serializer(),

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64Project"

View 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)
}
}
}
}

View 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)
}
}
}