Compare commits

...

7 Commits

14 changed files with 532 additions and 159 deletions

View File

@@ -1,5 +1,18 @@
# Changelog # Changelog
## 0.25.8.2
* `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
## 0.25.8.1
* `Coroutines`:
* New function `suspendPoint` to check coroutine cancellation status
* `SpecialMutableStateFlow` renamed to `MutableRedeliverStateFlow` (old name deprecated with `ReplaceWith`)
* `SmartSemaphore`:
* Fix of `waitRelease` call to pass correct number of permits
## 0.25.8 ## 0.25.8
* `Pagination`: * `Pagination`:

View File

@@ -2,7 +2,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -16,8 +16,8 @@ class LoadableComponentTests {
@Test @Test
@TestOnly @TestOnly
fun testSimpleLoad() = runComposeUiTest { fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0) val loadingFlow = MutableRedeliverStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0) val loadedFlow = MutableRedeliverStateFlow<Int>(0)
setContent { setContent {
LoadableComponent<Int>({ LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first() loadingFlow.filter { it == 1 }.first()

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -16,7 +16,7 @@ import org.jetbrains.compose.web.css.StyleSheet
* to add `Style(stylesheet)` on every compose function call * to add `Style(stylesheet)` on every compose function call
*/ */
object StyleSheetsAggregator { object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet()) private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow() val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable @Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.* import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test @Test
@TestOnly @TestOnly
fun simpleTest() = runComposeUiTest { fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0) val flowState = MutableRedeliverStateFlow(0)
setContent { setContent {
Button({ flowState.value++ }) { Text("Click") } Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString()) Text(flowState.collectAsState().value.toString())

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/** /**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to * Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber * each active subscriber
*/ */
open class SpecialMutableStateFlow<T>( open class MutableRedeliverStateFlow<T>(
initialValue: T initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)

View File

@@ -2,11 +2,27 @@ package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging( inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" }, noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
block: R.() -> T block: R.() -> T
) = runCatching(block).onFailure { ) = runCatching(block).onFailure {
logger.e(it) { errorMessageBuilder(it) } when (it) {
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
} }

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow] * @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/ */
class Mutable(locked: Boolean = false) : SmartMutex { class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked) private val _lockStateFlow = MutableRedeliverStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -47,7 +46,7 @@ sealed interface SmartSemaphore {
*/ */
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits override val maxPermits: Int = permits
private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits) private val _freePermitsStateFlow = MutableRedeliverStateFlow<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)
@@ -73,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits acquiredPermits != checkedPermits
} }
if (shouldContinue) { if (shouldContinue) {
waitRelease() waitRelease(checkedPermits - acquiredPermits)
} }
} while (shouldContinue && currentCoroutineContext().isActive) } while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests { class SmartKeyRWLockerTests {
@Test private lateinit var locker: SmartKeyRWLocker<String>
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
assertTrue { locker.isWriteLocked() } @BeforeTest
fun setup() {
assertFails { locker = SmartKeyRWLocker()
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) }
} }
// ==================== Global Read Tests ====================
@Test @Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val locker = SmartKeyRWLocker<String>() val results = mutableListOf<Boolean>()
val testKey = "test"
locker.acquireRead() locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) val jobs = List(5) {
launch {
assertFails { locker.acquireRead()
realWithTimeout(1.seconds) { delay(100.milliseconds)
locker.lockWrite(testKey) results.add(true)
locker.releaseRead()
} }
} }
assertFalse { locker.isWriteLocked(testKey) }
jobs.joinAll()
locker.releaseRead()
assertEquals(5, results.size)
}
@Test
fun testGlobalReadBlocksGlobalWrite() = runTest {
locker.acquireRead()
var writeAcquired = false
val writeJob = launch {
locker.lockWrite()
writeAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write should be blocked by global read")
locker.releaseRead() locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) writeJob.join()
realWithTimeout(1.seconds) { assertTrue(writeAcquired, "Write should succeed after read released")
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
@Test @Test
fun readLockFailedOnWriteLockKeyTest() = runTest { fun testGlobalReadBlocksAllKeyWrites() = runTest {
val locker = SmartKeyRWLocker<String>() locker.acquireRead()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) } val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
assertFails { val jobs = keys.map { key ->
realWithTimeout(1.seconds) { launch {
locker.lockWrite(key)
writeFlags[key] = true
locker.unlockWrite(key)
}
}
delay(200.milliseconds)
assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active")
locker.releaseRead()
jobs.joinAll()
assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released")
}
// ==================== Global Write Tests ====================
@Test
fun testGlobalWriteBlocksAllOperations() = runTest {
locker.lockWrite()
var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
val jobs = listOf(
launch {
locker.acquireRead() locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
} }
} )
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
locker.unlockWrite(testKey) delay(200.milliseconds)
assertFalse { locker.isWriteLocked(testKey) } assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
realWithTimeout(1.seconds) { locker.unlockWrite()
locker.acquireRead() jobs.joinAll()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) assertTrue(globalReadAcquired)
assertTrue { locker.releaseRead() } assertTrue(keyReadAcquired)
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits) assertTrue(keyWriteAcquired)
} }
@Test @Test
fun writeLockFailedOnWriteLockKeyTest() = runTest { fun testGlobalWriteIsExclusive() = runTest {
val locker = SmartKeyRWLocker<String>() locker.lockWrite()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) } var secondWriteAcquired = false
val job = launch {
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.lockWrite() locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
} }
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() } delay(200.milliseconds)
assertFalse(locker.isWriteLocked()) assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
} }
// ==================== Key Read Tests ====================
@Test @Test
fun readsBlockingGlobalWrite() = runTest { fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" } locker.acquireRead(key)
for (i in testKeys.indices) { val jobs = List(5) {
val it = testKeys[i] launch {
locker.acquireRead(it) locker.acquireRead(key)
val previous = testKeys.take(i) delay(50.milliseconds)
val next = testKeys.drop(i + 1) results.add(true)
locker.releaseRead(key)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
} }
} }
for (i in testKeys.indices) { jobs.joinAll()
val it = testKeys[i] locker.releaseRead(key)
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() } assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
locker.acquireRead("key1")
val jobs = listOf("key2", "key3", "key4").map { key ->
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results[key] = true
locker.releaseRead(key)
} }
val readPermitsBeforeLock = locker.readSemaphore().freePermits }
realWithTimeout(1.seconds) { locker.acquireRead() }
jobs.joinAll()
locker.releaseRead("key1")
assertEquals(3, results.size)
}
@Test
fun testKeyReadBlocksWriteForSameKey() = runTest {
val key = "testKey"
locker.acquireRead(key)
var writeAcquired = false
val job = launch {
locker.lockWrite(key)
writeAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write for same key should be blocked")
locker.releaseRead(key)
job.join()
assertTrue(writeAcquired)
}
@Test
fun testKeyReadBlocksGlobalWrite() = runTest {
locker.acquireRead("key1")
var globalWriteAcquired = false
val job = launch {
locker.lockWrite()
globalWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(globalWriteAcquired, "Global write should be blocked by key read")
locker.releaseRead("key1")
job.join()
assertTrue(globalWriteAcquired)
}
@Test
fun testKeyReadAllowsWriteForDifferentKey() = runTest {
locker.acquireRead("key1")
var writeAcquired = false
val job = launch {
locker.lockWrite("key2")
writeAcquired = true
locker.unlockWrite("key2")
}
job.join()
assertTrue(writeAcquired, "Write for different key should succeed")
locker.releaseRead("key1")
}
// ==================== Key Write Tests ====================
@Test
fun testKeyWriteBlocksReadForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
var readAcquired = false
val job = launch {
locker.acquireRead(key)
readAcquired = true
locker.releaseRead(key)
}
delay(200.milliseconds)
assertFalse(readAcquired, "Read for same key should be blocked")
locker.unlockWrite(key)
job.join()
assertTrue(readAcquired)
}
@Test
fun testKeyWriteBlocksGlobalRead() = runTest {
locker.lockWrite("key1")
var globalReadAcquired = false
val job = launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead() locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() } assertFalse(globalReadAcquired, "Global read should be blocked by key write")
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.unlockWrite("key1")
} job.join()
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } assertTrue(globalReadAcquired)
} }
@Test @Test
fun writesBlockingGlobalWrite() = runTest { fun testKeyWriteIsExclusiveForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
locker.lockWrite(key)
val testKeys = (0 until 100).map { "test$it" } var secondWriteAcquired = false
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach { locker.unlockWrite(key)
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
val jobs = listOf(
launch {
locker.acquireRead("key2")
results["read-key2"] = true
locker.releaseRead("key2")
},
launch {
locker.lockWrite("key3")
results["write-key3"] = true
locker.unlockWrite("key3")
} }
next.forEach { )
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
jobs.joinAll()
assertEquals(2, results.size, "Operations on different keys should succeed")
locker.unlockWrite("key1")
}
// ==================== Complex Scenarios ====================
@Test
fun testMultipleReadersThenWriter() = runTest {
val key = "testKey"
val readCount = 5
val readers = mutableListOf<Job>()
repeat(readCount) {
readers.add(launch {
locker.acquireRead(key)
delay(100.milliseconds)
locker.releaseRead(key)
})
}
delay(50.milliseconds) // Let readers acquire
var writerExecuted = false
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
delay(50.milliseconds)
assertFalse(writerExecuted, "Writer should wait for all readers")
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should execute after all readers done")
}
@Test
fun testWriterThenMultipleReaders() = runTest {
val key = "testKey"
locker.lockWrite(key)
val readerFlags = mutableListOf<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
} }
} }
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
val readPermitsBeforeLock = locker.readSemaphore().freePermits locker.unlockWrite(key)
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } readers.joinAll()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(it) assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(50.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails { launch {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
} }
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
assertTrue(executed["write-key1-start"] == true)
assertTrue(executed["read-key2"] == true)
assertTrue(executed["write-key1-end"] == true)
}
@Test
fun testReleaseWithoutAcquireReturnsFalse() = runTest {
assertFalse(locker.releaseRead(), "Release without acquire should return false")
assertFalse(locker.releaseRead("key1"), "Release without acquire should return false")
}
@Test
fun testUnlockWithoutLockReturnsFalse() = runTest {
assertFalse(locker.unlockWrite(), "Unlock without lock should return false")
assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false")
}
@Test
fun testProperReleaseReturnsTrue() = runTest {
locker.acquireRead()
assertTrue(locker.releaseRead(), "Release after acquire should return true")
locker.acquireRead("key1")
assertTrue(locker.releaseRead("key1"), "Release after acquire should return true")
}
@Test
fun testProperUnlockReturnsTrue() = runTest {
locker.lockWrite()
assertTrue(locker.unlockWrite(), "Unlock after lock should return true")
locker.lockWrite("key1")
assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true")
}
// ==================== Stress Tests ====================
@Test
fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) {
val operations = 100
val keys = listOf("key1", "key2", "key3", "key4", "key5")
val jobs = mutableListOf<Job>()
repeat(operations) { i ->
val key = keys[i % keys.size]
when (i % 4) {
0 -> jobs.add(launch {
locker.acquireRead(key)
delay(10.milliseconds)
locker.releaseRead(key)
})
1 -> jobs.add(launch {
locker.lockWrite(key)
delay(10.milliseconds)
locker.unlockWrite(key)
})
2 -> jobs.add(launch {
locker.acquireRead()
delay(10.milliseconds)
locker.releaseRead()
})
3 -> jobs.add(launch {
locker.lockWrite()
delay(10.milliseconds)
locker.unlockWrite()
})
}
}
jobs.joinAll()
// If we reach here without deadlock or exceptions, test passes
}
@Test
fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) {
val key = "testKey"
var writerExecuted = false
// Start continuous readers
val readers = List(10) {
launch {
repeat(5) {
locker.acquireRead(key)
delay(50.milliseconds)
locker.releaseRead(key)
delay(10.milliseconds)
}
}
}
delay(100.milliseconds)
// Try to acquire write lock
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should eventually execute")
} }
} }

View File

@@ -1,4 +1,4 @@
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -8,17 +8,17 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class SpecialMutableStateFlowTests { class MutableRedeliverStateFlowTests {
@Test @Test
fun simpleTest() = runTest { fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val specialMutableStateFlow = MutableRedeliverStateFlow(0)
specialMutableStateFlow.value = 1 specialMutableStateFlow.value = 1
specialMutableStateFlow.first { it == 1 } specialMutableStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value) assertEquals(1, specialMutableStateFlow.value)
} }
@Test @Test
fun specialTest() = runTest { fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0) val specialMutableStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) { subscriberJob = specialMutableStateFlow.subscribe(this) {
when (it) { when (it) {

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.8 version=0.25.8.2
android_code_version=298 android_code_version=298

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.pagination.compose package dev.inmo.micro_utils.pagination.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.runCatchingLogging import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
@@ -27,8 +27,8 @@ class InfinityPagedComponentContext<T> internal constructor(
private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T> private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>
) { ) {
internal val startPage = SimplePagination(page, size) internal val startPage = SimplePagination(page, size)
internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null) internal val latestLoadedPage = MutableRedeliverStateFlow<PaginationResult<T>?>(null)
internal val dataState = SpecialMutableStateFlow<List<T>?>(null) internal val dataState = MutableRedeliverStateFlow<List<T>?>(null)
internal var loadingJob: Job? = null internal var loadingJob: Job? = null
internal val loadingMutex = Mutex() internal val loadingMutex = Mutex()

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.pagination.compose package dev.inmo.micro_utils.pagination.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -28,8 +28,8 @@ class PagedComponentContext<T> internal constructor(
private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T> private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>
) { ) {
internal val startPage = SimplePagination(initialPage, size) internal val startPage = SimplePagination(initialPage, size)
internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null) internal val latestLoadedPage = MutableRedeliverStateFlow<PaginationResult<T>?>(null)
internal val dataState = SpecialMutableStateFlow<PaginationResult<T>?>(null) internal val dataState = MutableRedeliverStateFlow<PaginationResult<T>?>(null)
internal var loadingJob: Job? = null internal var loadingJob: Job? = null
internal val loadingMutex = Mutex() internal val loadingMutex = Mutex()