Compare commits

...

7 Commits

14 changed files with 532 additions and 159 deletions

View File

@@ -1,5 +1,18 @@
# 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
* `Pagination`:

View File

@@ -2,7 +2,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
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.filter
import kotlinx.coroutines.flow.first
@@ -16,8 +16,8 @@ class LoadableComponentTests {
@Test
@TestOnly
fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = SpecialMutableStateFlow<Int>(0)
val loadedFlow = SpecialMutableStateFlow<Int>(0)
val loadingFlow = MutableRedeliverStateFlow<Int>(0)
val loadedFlow = MutableRedeliverStateFlow<Int>(0)
setContent {
LoadableComponent<Int>({
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.collectAsState
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.asStateFlow
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
*/
object StyleSheetsAggregator {
private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet())
private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState
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 kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test
@TestOnly
fun simpleTest() = runComposeUiTest {
val flowState = SpecialMutableStateFlow(0)
val flowState = MutableRedeliverStateFlow(0)
setContent {
Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString())

View File

@@ -1,7 +1,5 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
@@ -11,13 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber
*/
open class SpecialMutableStateFlow<T>(
open class MutableRedeliverStateFlow<T>(
initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@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.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(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog,
block: R.() -> T
) = 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
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -44,7 +43,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/
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()
private val internalChangesMutex = Mutex()

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -47,7 +46,7 @@ sealed interface SmartSemaphore {
*/
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
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()
private val internalChangesMutex = Mutex(false)
@@ -73,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits
}
if (shouldContinue) {
waitRelease()
waitRelease(checkedPermits - acquiredPermits)
}
} while (shouldContinue && currentCoroutineContext().isActive)
} 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.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests {
private lateinit var locker: SmartKeyRWLocker<String>
@BeforeTest
fun setup() {
locker = SmartKeyRWLocker()
}
// ==================== Global Read Tests ====================
@Test
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val results = mutableListOf<Boolean>()
locker.acquireRead()
val jobs = List(5) {
launch {
locker.acquireRead()
delay(100.milliseconds)
results.add(true)
locker.releaseRead()
}
}
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()
writeJob.join()
assertTrue(writeAcquired, "Write should succeed after read released")
}
@Test
fun testGlobalReadBlocksAllKeyWrites() = runTest {
locker.acquireRead()
val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
val jobs = keys.map { key ->
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()
assertTrue { locker.isWriteLocked() }
var globalReadAcquired = false
var keyReadAcquired = false
var keyWriteAcquired = false
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
val jobs = listOf(
launch {
locker.acquireRead()
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
}
}
assertFalse { locker.isWriteLocked(testKey) }
)
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
locker.unlockWrite()
assertFalse { locker.isWriteLocked() }
jobs.joinAll()
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
assertTrue(globalReadAcquired)
assertTrue(keyReadAcquired)
assertTrue(keyWriteAcquired)
}
@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) {
fun testGlobalWriteIsExclusive() = runTest {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
var secondWriteAcquired = false
val job = launch {
locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
}
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() }
assertFalse(locker.isWriteLocked())
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
}
// ==================== Key Read Tests ====================
@Test
fun readsBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" }
locker.acquireRead(key)
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 }
val jobs = List(5) {
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results.add(true)
locker.releaseRead(key)
}
}
for (i in testKeys.indices) {
val it = testKeys[i]
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() }
jobs.joinAll()
locker.releaseRead(key)
assertEquals(5, results.size)
}
val readPermitsBeforeLock = locker.readSemaphore().freePermits
realWithTimeout(1.seconds) { locker.acquireRead() }
@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)
}
}
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()
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 }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked by key write")
locker.unlockWrite("key1")
job.join()
assertTrue(globalReadAcquired)
}
@Test
fun writesBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
fun testKeyWriteIsExclusiveForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
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 }
var secondWriteAcquired = false
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
locker.unlockWrite(key)
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")
}
)
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) {
val it = testKeys[i]
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
delay(200.milliseconds)
assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
val readPermitsBeforeLock = locker.readSemaphore().freePermits
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } }
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(key)
readers.joinAll()
locker.unlockWrite(it)
assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
@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.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
delay(50.milliseconds)
launch {
locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
}
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.subscribe
import kotlinx.coroutines.Job
@@ -8,17 +8,17 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SpecialMutableStateFlowTests {
class MutableRedeliverStateFlowTests {
@Test
fun simpleTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
val specialMutableStateFlow = MutableRedeliverStateFlow(0)
specialMutableStateFlow.value = 1
specialMutableStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value)
}
@Test
fun specialTest() = runTest {
val specialMutableStateFlow = SpecialMutableStateFlow(0)
val specialMutableStateFlow = MutableRedeliverStateFlow(0)
lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) {
when (it) {

View File

@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.25.8
version=0.25.8.2
android_code_version=298

View File

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

View File

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