mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-03-13 05:42:23 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdbb00d51f | |||
| af5bb50f19 | |||
| 651233bb84 | |||
| 30fcd16074 | |||
| dd0d445519 | |||
| 0359b3a380 | |||
| df5fa86c8e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
.kotlin
|
.kotlin
|
||||||
out/*
|
out/*
|
||||||
*.iml
|
*.iml
|
||||||
|
|||||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,34 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.26.2
|
## 0.25.8.2
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Ktor`: `3.2.2` -> `3.2.3`
|
|
||||||
* `Okio`: `3.15.0` -> `3.16.0`
|
|
||||||
* `Coroutines`:
|
* `Coroutines`:
|
||||||
* Rename `SpecialMutableStateFlow` to `MutableRedeliverStateFlow`
|
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions
|
||||||
|
|
||||||
## 0.26.1
|
## 0.25.8.1
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Compose`: `1.8.1` -> `1.8.2`
|
|
||||||
* `Ktor`: `3.2.1` -> `3.2.2`
|
|
||||||
* `Coroutines`:
|
* `Coroutines`:
|
||||||
* Add opportunity to pass logger in subscribe async
|
* New function `suspendPoint` to check coroutine cancellation status
|
||||||
|
* `SpecialMutableStateFlow` renamed to `MutableRedeliverStateFlow` (old name deprecated with `ReplaceWith`)
|
||||||
## 0.26.0
|
* `SmartSemaphore`:
|
||||||
|
* Fix of `waitRelease` call to pass correct number of permits
|
||||||
**WARNING!!! SINCE THIS VERSION IF YOU WANT TO USE SOME OF KSP MODULES, SET `ksp.useKSP2=false` IN YOUR `gradle.properties`** (see [gh issue 2491](https://github.com/google/ksp/issues/2491))
|
|
||||||
|
|
||||||
* `Versions`:
|
|
||||||
* `Kotlin`: `2.1.21` -> `2.2.0`
|
|
||||||
* `Serialization`: `1.8.1` -> `1.9.0`
|
|
||||||
* `KSLog`: `1.4.2` -> `1.5.0`
|
|
||||||
* `Ktor`: `3.1.3` -> `3.2.1`
|
|
||||||
* `Koin`: `4.0.4` -> `4.1.0`
|
|
||||||
* `Okio`: `3.12.0` -> `3.15.0`
|
|
||||||
* `KSP`: `2.1.20-1.0.31` -> `2.2.0-2.0.2`
|
|
||||||
* `kotlin-poet`: `1.18.1` -> `2.2.0`
|
|
||||||
|
|
||||||
## 0.25.8
|
## 0.25.8
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != n
|
|||||||
centralPortal {
|
centralPortal {
|
||||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||||
validationTimeout = Duration.ofHours(4)
|
verificationTimeout = Duration.ofHours(4)
|
||||||
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
|
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ 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.MutableRedeliverStateFlow
|
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
|
||||||
|
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
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import org.jetbrains.annotations.TestOnly
|
import org.jetbrains.annotations.TestOnly
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import dev.inmo.kslog.common.KSLog
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
@@ -8,15 +7,13 @@ import kotlinx.coroutines.flow.consumeAsFlow
|
|||||||
fun <T> CoroutineScope.actorAsync(
|
fun <T> CoroutineScope.actorAsync(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
markerFactory: suspend (T) -> Any? = { null },
|
markerFactory: suspend (T) -> Any? = { null },
|
||||||
logger: KSLog = KSLog,
|
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
): Channel<T> {
|
): Channel<T> {
|
||||||
val channel = Channel<T>(channelCapacity)
|
val channel = Channel<T>(channelCapacity)
|
||||||
channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
|
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
|
|
||||||
inline fun <T> CoroutineScope.safeActorAsync(
|
inline fun <T> CoroutineScope.safeActorAsync(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import dev.inmo.kslog.common.KSLog
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.*
|
import kotlinx.coroutines.channels.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -9,7 +8,6 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
|
|
||||||
private class SubscribeAsyncReceiver<T>(
|
private class SubscribeAsyncReceiver<T>(
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
val logger: KSLog,
|
|
||||||
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
||||||
) {
|
) {
|
||||||
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
||||||
@@ -17,7 +15,7 @@ private class SubscribeAsyncReceiver<T>(
|
|||||||
get() = dataChannel
|
get() = dataChannel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launchLoggingDropExceptions(logger = logger) {
|
scope.launchLoggingDropExceptions {
|
||||||
for (data in dataChannel) {
|
for (data in dataChannel) {
|
||||||
output(data)
|
output(data)
|
||||||
}
|
}
|
||||||
@@ -35,16 +33,13 @@ private data class AsyncSubscriptionCommandData<T, M>(
|
|||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
val markerFactory: suspend (T) -> M,
|
val markerFactory: suspend (T) -> M,
|
||||||
val block: suspend (T) -> Unit,
|
val block: suspend (T) -> Unit,
|
||||||
val logger: KSLog,
|
|
||||||
val onEmpty: suspend (M) -> Unit
|
val onEmpty: suspend (M) -> Unit
|
||||||
) : AsyncSubscriptionCommand<T, M> {
|
) : AsyncSubscriptionCommand<T, M> {
|
||||||
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||||
val marker = markerFactory(data)
|
val marker = markerFactory(data)
|
||||||
markersMap.getOrPut(marker) {
|
markersMap.getOrPut(marker) {
|
||||||
SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
|
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) {
|
||||||
runCatchingLogging(logger = logger) {
|
safelyWithoutExceptions { block(it) }
|
||||||
block(it)
|
|
||||||
}
|
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
onEmpty(marker)
|
onEmpty(marker)
|
||||||
}
|
}
|
||||||
@@ -68,7 +63,6 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
|
|||||||
fun <T, M> Flow<T>.subscribeAsync(
|
fun <T, M> Flow<T>.subscribeAsync(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
markerFactory: suspend (T) -> M,
|
markerFactory: suspend (T) -> M,
|
||||||
logger: KSLog = KSLog,
|
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
): Job {
|
): Job {
|
||||||
val subscope = scope.LinkedSupervisorScope()
|
val subscope = scope.LinkedSupervisorScope()
|
||||||
@@ -77,14 +71,8 @@ fun <T, M> Flow<T>.subscribeAsync(
|
|||||||
it.invoke(markersMap)
|
it.invoke(markersMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
|
val job = subscribeLoggingDropExceptions(subscope) { data ->
|
||||||
val dataCommand = AsyncSubscriptionCommandData(
|
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||||
data = data,
|
|
||||||
scope = subscope,
|
|
||||||
markerFactory = markerFactory,
|
|
||||||
block = block,
|
|
||||||
logger = logger
|
|
||||||
) { marker ->
|
|
||||||
actor.send(
|
actor.send(
|
||||||
AsyncSubscriptionCommandClearReceiver(marker)
|
AsyncSubscriptionCommandClearReceiver(marker)
|
||||||
)
|
)
|
||||||
@@ -97,20 +85,17 @@ fun <T, M> Flow<T>.subscribeAsync(
|
|||||||
return job
|
return job
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
|
|
||||||
fun <T, M> Flow<T>.subscribeSafelyAsync(
|
fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
markerFactory: suspend (T) -> M,
|
markerFactory: suspend (T) -> M,
|
||||||
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
logger: KSLog = KSLog,
|
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
) = subscribeAsync(scope, markerFactory, logger) {
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
safely(onException) {
|
safely(onException) {
|
||||||
block(it)
|
block(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
|
|
||||||
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
markerFactory: suspend (T) -> M,
|
markerFactory: suspend (T) -> M,
|
||||||
@@ -122,22 +107,11 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
markerFactory: suspend (T) -> M,
|
|
||||||
logger: KSLog = KSLog,
|
|
||||||
block: suspend (T) -> Unit
|
|
||||||
) = subscribeAsync(scope, markerFactory, logger) {
|
|
||||||
block(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, logger, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
|
|
||||||
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
markerFactory: suspend (T) -> M,
|
markerFactory: suspend (T) -> M,
|
||||||
logger: KSLog = KSLog,
|
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
) = subscribeAsync(scope, markerFactory, logger) {
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
safelyWithoutExceptions({ /* do nothing */}) {
|
safelyWithoutExceptions({ /* do nothing */}) {
|
||||||
block(it)
|
block(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,3 @@ open class MutableRedeliverStateFlow<T>(
|
|||||||
|
|
||||||
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
|
override suspend fun collect(collector: FlowCollector<T>) = sharingFlow.collect(collector)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Renamed to MutableRedeliverStateFlow", ReplaceWith("MutableRedeliverStateFlow<T>"))
|
|
||||||
typealias SpecialMutableStateFlow<T> = MutableRedeliverStateFlow<T>
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
|
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
|
||||||
|
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
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
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.assertTrue
|
||||||
|
|
||||||
class SpecialMutableStateFlowTests {
|
class MutableRedeliverStateFlowTests {
|
||||||
@Test
|
@Test
|
||||||
fun simpleTest() = runTest {
|
fun simpleTest() = runTest {
|
||||||
val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
|
val specialMutableStateFlow = MutableRedeliverStateFlow(0)
|
||||||
mutableRedeliverStateFlow.value = 1
|
specialMutableStateFlow.value = 1
|
||||||
mutableRedeliverStateFlow.first { it == 1 }
|
specialMutableStateFlow.first { it == 1 }
|
||||||
assertEquals(1, mutableRedeliverStateFlow.value)
|
assertEquals(1, specialMutableStateFlow.value)
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
fun specialTest() = runTest {
|
fun specialTest() = runTest {
|
||||||
val mutableRedeliverStateFlow = MutableRedeliverStateFlow(0)
|
val specialMutableStateFlow = MutableRedeliverStateFlow(0)
|
||||||
lateinit var subscriberJob: Job
|
lateinit var subscriberJob: Job
|
||||||
subscriberJob = mutableRedeliverStateFlow.subscribe(this) {
|
subscriberJob = specialMutableStateFlow.subscribe(this) {
|
||||||
when (it) {
|
when (it) {
|
||||||
1 -> mutableRedeliverStateFlow.value = 2
|
1 -> specialMutableStateFlow.value = 2
|
||||||
2 -> subscriberJob.cancel()
|
2 -> subscriberJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutableRedeliverStateFlow.value = 1
|
specialMutableStateFlow.value = 1
|
||||||
subscriberJob.join()
|
subscriberJob.join()
|
||||||
assertEquals(2, mutableRedeliverStateFlow.value)
|
assertEquals(2, specialMutableStateFlow.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,6 @@ android.useAndroidX=true
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
|
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
|
||||||
|
|
||||||
# https://github.com/google/ksp/issues/2491
|
|
||||||
ksp.useKSP2=false
|
|
||||||
|
|
||||||
# JS NPM
|
# JS NPM
|
||||||
|
|
||||||
crypto_js_version=4.1.1
|
crypto_js_version=4.1.1
|
||||||
@@ -18,5 +15,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.26.2
|
version=0.25.8.2
|
||||||
android_code_version=301
|
android_code_version=298
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "2.2.0"
|
kt = "2.1.21"
|
||||||
kt-serialization = "1.9.0"
|
kt-serialization = "1.8.1"
|
||||||
kt-coroutines = "1.10.2"
|
kt-coroutines = "1.10.2"
|
||||||
|
|
||||||
kotlinx-browser = "0.3"
|
kotlinx-browser = "0.3"
|
||||||
|
|
||||||
kslog = "1.5.0"
|
kslog = "1.4.2"
|
||||||
|
|
||||||
jb-compose = "1.8.2"
|
jb-compose = "1.8.1"
|
||||||
jb-exposed = "0.61.0"
|
jb-exposed = "0.61.0"
|
||||||
jb-dokka = "2.0.0"
|
jb-dokka = "2.0.0"
|
||||||
|
|
||||||
@@ -17,19 +17,19 @@ sqlite = "3.50.1.0"
|
|||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "3.2.3"
|
ktor = "3.1.3"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
koin = "4.1.0"
|
koin = "4.0.4"
|
||||||
|
|
||||||
okio = "3.16.0"
|
okio = "3.12.0"
|
||||||
|
|
||||||
ksp = "2.2.0-2.0.2"
|
ksp = "2.1.20-1.0.31"
|
||||||
kotlin-poet = "2.2.0"
|
kotlin-poet = "1.18.1"
|
||||||
|
|
||||||
versions = "0.52.0"
|
versions = "0.51.0"
|
||||||
nmcp = "1.0.2"
|
nmcp = "0.1.5"
|
||||||
|
|
||||||
android-gradle = "8.9.+"
|
android-gradle = "8.9.+"
|
||||||
dexcount = "4.0.0"
|
dexcount = "4.0.0"
|
||||||
@@ -38,8 +38,8 @@ android-coreKtx = "1.16.0"
|
|||||||
android-recyclerView = "1.4.0"
|
android-recyclerView = "1.4.0"
|
||||||
android-appCompat = "1.7.1"
|
android-appCompat = "1.7.1"
|
||||||
android-fragment = "1.8.8"
|
android-fragment = "1.8.8"
|
||||||
android-espresso = "3.7.0"
|
android-espresso = "3.6.1"
|
||||||
android-test = "1.3.0"
|
android-test = "1.2.1"
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "36"
|
android-props-compileSdk = "36"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.koin")
|
api project(":micro_utils.koin")
|
||||||
api project(":micro_utils.ksp.generator")
|
|
||||||
api libs.kotlin.poet
|
api libs.kotlin.poet
|
||||||
api libs.ksp
|
api libs.ksp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import com.squareup.kotlinpoet.asTypeName
|
|||||||
import com.squareup.kotlinpoet.ksp.toClassName
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
import com.squareup.kotlinpoet.ksp.toTypeName
|
import com.squareup.kotlinpoet.ksp.toTypeName
|
||||||
import com.squareup.kotlinpoet.ksp.writeTo
|
import com.squareup.kotlinpoet.ksp.writeTo
|
||||||
import dev.inmo.micro_ksp.generator.safeClassName
|
|
||||||
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
|
||||||
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
|
||||||
import org.koin.core.Koin
|
import org.koin.core.Koin
|
||||||
@@ -238,7 +237,15 @@ class Processor(
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
|
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
|
||||||
val type = safeClassName { it.type }
|
val type = runCatching {
|
||||||
|
it.type.asTypeName()
|
||||||
|
}.getOrElse { e ->
|
||||||
|
if (e is KSTypeNotPresentException) {
|
||||||
|
e.ksType.toClassName()
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
val targetType = runCatching {
|
val targetType = runCatching {
|
||||||
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
|
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
|
||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package dev.inmo.micro_utils.koin.generator.test
|
|||||||
|
|
||||||
import kotlin.Any
|
import kotlin.Any
|
||||||
import kotlin.Boolean
|
import kotlin.Boolean
|
||||||
|
import kotlin.Deprecated
|
||||||
import kotlin.String
|
import kotlin.String
|
||||||
import org.koin.core.Koin
|
import org.koin.core.Koin
|
||||||
import org.koin.core.definition.Definition
|
import org.koin.core.definition.Definition
|
||||||
@@ -29,59 +30,95 @@ public val Koin.sampleInfo: Test<String>
|
|||||||
/**
|
/**
|
||||||
* @return Definition by key "sampleInfo" with [parameters]
|
* @return Definition by key "sampleInfo" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
|
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||||
|
get(named("sampleInfo"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Definition by key "sampleInfo" with [parameters]
|
* @return Definition by key "sampleInfo" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
|
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> =
|
||||||
|
get(named("sampleInfo"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||||
*/
|
*/
|
||||||
public fun Module.singleSampleInfo(createdAtStart: Boolean = false, definition: Definition<Test<String>>): KoinDefinition<Test<String>> = single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
@Deprecated(
|
||||||
|
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
|
||||||
|
ReplaceWith("singleSampleInfo"),
|
||||||
|
)
|
||||||
|
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||||
|
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
|
||||||
|
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
|
||||||
|
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||||
*/
|
*/
|
||||||
public fun Module.factorySampleInfo(definition: Definition<Test<String>>): KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
@Deprecated(
|
||||||
|
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
|
||||||
|
ReplaceWith("factorySampleInfo"),
|
||||||
|
)
|
||||||
|
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
|
||||||
|
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
|
||||||
|
*/
|
||||||
|
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
|
||||||
|
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Definition by key "test" with [parameters]
|
* @return Definition by key "test" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
|
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T
|
||||||
|
= get(named("test"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Definition by key "test" with [parameters]
|
* @return Definition by key "test" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
|
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T
|
||||||
|
= get(named("test"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "test"
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "test"
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart = createdAtStart, definition = definition)
|
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline
|
||||||
|
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
|
||||||
|
createdAtStart, definition = definition)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("test"), definition = definition)
|
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>):
|
||||||
|
KoinDefinition<T> = factory(named("test"), definition = definition)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Definition by key "testNullable" with [parameters]
|
* @return Definition by key "testNullable" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
|
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? =
|
||||||
|
null): T? = getOrNull(named("testNullable"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Definition by key "testNullable" with [parameters]
|
* @return Definition by key "testNullable" with [parameters]
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
|
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? =
|
||||||
|
null): T? = getOrNull(named("testNullable"), parameters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
|
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"), createdAtStart = createdAtStart, definition = definition)
|
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false,
|
||||||
|
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
|
||||||
|
createdAtStart = createdAtStart, definition = definition)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
|
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("testNullable"), definition = definition)
|
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>):
|
||||||
|
KoinDefinition<T> = factory(named("testNullable"), definition = definition)
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package dev.inmo.micro_ksp.generator
|
|
||||||
|
|
||||||
import com.google.devtools.ksp.KSTypeNotPresentException
|
|
||||||
import com.google.devtools.ksp.KspExperimental
|
|
||||||
import com.squareup.kotlinpoet.ClassName
|
|
||||||
import com.squareup.kotlinpoet.asTypeName
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
@OptIn(KspExperimental::class)
|
|
||||||
inline fun safeClassName(classnameGetter: () -> KClass<*>) = runCatching {
|
|
||||||
classnameGetter().asTypeName()
|
|
||||||
}.getOrElse { e ->
|
|
||||||
if (e is KSTypeNotPresentException) {
|
|
||||||
ClassName(
|
|
||||||
e.ksType.declaration.packageName.asString(),
|
|
||||||
e.ksType.declaration.qualifiedName ?.asString() ?.replaceFirst(e.ksType.declaration.packageName.asString(), "")
|
|
||||||
?: e.ksType.declaration.simpleName.asString()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package dev.inmo.micro_utils.pagination.compose
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow
|
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.pagination.*
|
import dev.inmo.micro_utils.pagination.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|||||||
Reference in New Issue
Block a user