Compare commits

..

9 Commits

Author SHA1 Message Date
renovate[bot]
da86c4993b Update dependency com.android.tools.build:gradle to 8.13.+ 2026-07-16 18:03:16 +00:00
a76d04e2a4 Merge pull request #669 from InsanusMokrassar/0.30.0
0.30.0
2026-06-29 15:34:06 +06:00
e0468bc2d7 update dependencies 2026-06-29 15:14:09 +06:00
aeccfbb214 start 0.30.0 2026-06-29 14:47:56 +06:00
c793bea0c3 Merge pull request #663 from InsanusMokrassar/0.29.4
0.29.4
2026-05-23 17:12:28 +06:00
e3913f7600 fix dokka build :( 2026-05-23 16:54:55 +06:00
8dbb35f378 add meta 2026-05-23 12:14:15 +06:00
47d12e3740 start 0.29.4 2026-05-23 11:46:21 +06:00
45bff98a2c Merge pull request #662 from InsanusMokrassar/0.29.3
0.29.3
2026-05-21 20:12:29 +06:00
11 changed files with 33 additions and 285 deletions

View File

@@ -1,23 +1,5 @@
# Changelog # Changelog
## 0.30.2
* `Coroutines`:
* `SmartRWLocker`:
* Fix of `unlockWrite`, `lockWrite` and `releaseRead` calls to pass correct number of permits
* `SmartMutex`:
* Fix `unlock` call
* `SmartSemaphore`:
* Fix same issues to avoid cancellation exceptions handling errors and several other problems
## 0.30.1
* `Versions`:
* `KSLog`: `1.6.1` -> `1.7.0`
* `SQLite`: `3.53.2.0` -> `3.53.2.1`
* `Ktor`: `3.5.1` -> `3.5.2`
* `Okio`: `3.17.0` -> `3.18.1`
## 0.30.0 ## 0.30.0
* `Versions`: * `Versions`:

View File

@@ -41,7 +41,7 @@ allprojects {
mavenCentral() mavenCentral()
google() google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
// maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
mavenLocal() mavenLocal()
} }

View File

@@ -1,6 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -8,7 +7,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -94,8 +92,8 @@ sealed interface SmartMutex {
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this * If [isLocked] == true - will change it to false and return true. If current call will not unlock this
* [SmartMutex] - false * [SmartMutex] - false
*/ */
suspend fun unlock(): Boolean = withContext(NonCancellable) { suspend fun unlock(): Boolean {
if (_lockStateFlow.value) { return if (_lockStateFlow.value) {
internalChangesMutex.withLock { internalChangesMutex.withLock {
if (_lockStateFlow.value) { if (_lockStateFlow.value) {
_lockStateFlow.value = false _lockStateFlow.value = false

View File

@@ -1,8 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -23,7 +21,6 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable() val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable() val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
/** /**
* Do lock in [readSemaphore] inside of [writeMutex] locking * Do lock in [readSemaphore] inside of [writeMutex] locking
*/ */
@@ -35,8 +32,8 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/** /**
* Release one read permit in [readSemaphore] * Release one read permit in [readSemaphore]
*/ */
suspend fun releaseRead(): Boolean = withContext(NonCancellable) { suspend fun releaseRead(): Boolean {
_readSemaphore.release() return _readSemaphore.release()
} }
/** /**
@@ -47,9 +44,7 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
try { try {
_readSemaphore.acquire(readPermits) _readSemaphore.acquire(readPermits)
} catch (e: CancellationException) { } catch (e: CancellationException) {
withContext(NonCancellable) { _writeMutex.unlock()
_writeMutex.unlock()
}
throw e throw e
} }
} }
@@ -57,9 +52,9 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/** /**
* Unlock [writeMutex] * Unlock [writeMutex]
*/ */
suspend fun unlockWrite(): Boolean = withContext(NonCancellable) { suspend fun unlockWrite(): Boolean {
_writeMutex.unlock().also { unlocked -> return _writeMutex.unlock().also {
if (unlocked) { if (it) {
_readSemaphore.release(readPermits) _readSemaphore.release(readPermits)
} }
} }

View File

@@ -1,6 +1,5 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -9,7 +8,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -78,9 +76,7 @@ sealed interface SmartSemaphore {
} }
} while (shouldContinue && currentCoroutineContext().isActive) } while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) { } catch (e: Throwable) {
if (acquiredPermits > 0) { release(acquiredPermits)
release(acquiredPermits)
}
throw e throw e
} }
} }
@@ -111,9 +107,9 @@ sealed interface SmartSemaphore {
*/ */
suspend fun tryAcquire(permits: Int = 1): Boolean { suspend fun tryAcquire(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits) val checkedPermits = checkedPermits(permits)
return if (_freePermitsStateFlow.value >= checkedPermits) { return if (_freePermitsStateFlow.value < checkedPermits) {
internalChangesMutex.withLock { internalChangesMutex.withLock {
if (_freePermitsStateFlow.value >= checkedPermits) { if (_freePermitsStateFlow.value < checkedPermits) {
_freePermitsStateFlow.value -= checkedPermits _freePermitsStateFlow.value -= checkedPermits
true true
} else { } else {
@@ -129,12 +125,12 @@ sealed interface SmartSemaphore {
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this * If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false * [SmartSemaphore] - false
*/ */
suspend fun release(permits: Int = 1): Boolean = withContext(NonCancellable) { suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits) val checkedPermits = checkedPermits(permits)
if (_freePermitsStateFlow.value < maxPermits) { return if (_freePermitsStateFlow.value < this.maxPermits) {
internalChangesMutex.withLock { internalChangesMutex.withLock {
if (_freePermitsStateFlow.value < maxPermits) { if (_freePermitsStateFlow.value < this.maxPermits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, maxPermits) _freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits)
true true
} else { } else {
false false

View File

@@ -1,73 +0,0 @@
import dev.inmo.micro_utils.coroutines.SmartMutex
import dev.inmo.micro_utils.coroutines.withLock
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartMutexTests {
@Test
fun cancelledUnlockCompletesUnderContention() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable()
// Delegate this acquisition's release to another coroutine. The
// unconfined collector runs while lock() still holds its internal mutex.
val releaser = launch(Dispatchers.Unconfined) {
mutex.lockStateFlow.first { it }
currentCoroutineContext().cancel()
mutex.unlock()
}
// Keep acquisition on the normal test dispatcher so the collector
// attempts cancelled cleanup before the internal mutex is released.
mutex.lock()
releaser.join()
assertTrue(releaser.isCancelled)
assertFalse(mutex.isLocked, "Cancellation must not prevent the delegated unlock")
assertTrue(mutex.tryLock())
assertTrue(mutex.unlock())
}
@Test
fun cancellingWithLockBodyReleasesMutex() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable()
val holder = launch(Dispatchers.Unconfined) {
mutex.withLock {
awaitCancellation()
}
}
assertTrue(mutex.isLocked)
holder.cancelAndJoin()
assertFalse(mutex.isLocked)
}
@Test
fun cancelledWaiterDoesNotEnterOrReleaseHeldMutex() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable()
var entered = false
mutex.withLock {
val waiter = launch(Dispatchers.Unconfined) {
mutex.withLock {
entered = true
}
}
waiter.cancelAndJoin()
assertFalse(entered)
assertTrue(mutex.isLocked)
}
assertFalse(mutex.isLocked)
}
}

View File

@@ -9,7 +9,6 @@ import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartRWLockerTests { class SmartRWLockerTests {
@@ -110,59 +109,6 @@ class SmartRWLockerTests {
} }
} }
@Test
fun failureOnReadFreeingRead() = runTest {
val locker = SmartRWLocker()
val job = launch {
locker.withReadAcquire {
while (isActive) {
delay(1.days)
}
}
}
locker.readSemaphore.permitsStateFlow.first {
it == locker.readSemaphore.maxPermits - 1
}
job.cancelAndJoin()
locker.readSemaphore.permitsStateFlow.first {
it == locker.readSemaphore.maxPermits
}
}
@Test
fun cancelledReaderReleasesPermitUnderContention() = runTest(timeout = 5.seconds) {
val locker = SmartRWLocker(readPermits = 2)
val reader = launch(Dispatchers.Unconfined) {
locker.withReadAcquire {
awaitCancellation()
}
}
assertEquals(1, locker.readSemaphore.freePermits)
// Observe the second acquisition synchronously while it still holds the
// semaphore's internal mutex. Cancelling the unconfined reader makes its
// cleanup contend for that mutex before the acquisition can release it.
val cancellation = launch(Dispatchers.Unconfined) {
locker.readSemaphore.permitsStateFlow.first { it == 0 }
reader.cancel()
}
// Keep this acquisition on the normal test dispatcher: making it
// unconfined would change the ordering that forces cleanup contention.
locker.withReadAcquire {
cancellation.join()
reader.join()
assertTrue(reader.isCancelled)
assertEquals(
1,
locker.readSemaphore.freePermits,
"The cancelled reader must release its permit while the other reader still holds one"
)
}
assertEquals(2, locker.readSemaphore.freePermits)
}
@Test @Test
fun simpleWithReadAcquireTest() { fun simpleWithReadAcquireTest() {
val locker = SmartRWLocker() val locker = SmartRWLocker()

View File

@@ -1,104 +0,0 @@
import dev.inmo.micro_utils.coroutines.SmartSemaphore
import dev.inmo.micro_utils.coroutines.withAcquire
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartSemaphoreTests {
@Test
fun cancelledHolderReleasesPermitUnderContention() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 2)
val holder = launch(Dispatchers.Unconfined) {
semaphore.withAcquire {
awaitCancellation()
}
}
assertEquals(1, semaphore.freePermits)
// The synchronous observer cancels the holder while the second
// acquisition still owns the semaphore's internal changes mutex.
val cancellation = launch(Dispatchers.Unconfined) {
semaphore.permitsStateFlow.first { it == 0 }
holder.cancel()
}
semaphore.withAcquire {
cancellation.join()
holder.join()
assertTrue(holder.isCancelled)
assertEquals(1, semaphore.freePermits, "The cancelled holder must return its permit")
}
assertEquals(2, semaphore.freePermits)
}
@Test
fun cancelledAcquireReturnsPartialPermitsUnderContention() = runTest(timeout = 5.seconds) {
// One permit belongs to another holder; the waiter can acquire two
// permits immediately, but must wait for the third.
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 1)
lateinit var waiter: kotlinx.coroutines.Job
val cancellation = launch(Dispatchers.Unconfined) {
semaphore.permitsStateFlow.first { it == 1 }
waiter.cancel()
}
waiter = launch(Dispatchers.Unconfined) {
semaphore.acquire(3)
}
assertEquals(0, semaphore.freePermits)
assertFalse(waiter.isCompleted)
// Publishing this release resumes the observer while the internal
// mutex is held. The cancelled acquire must wait to roll back safely.
semaphore.release()
cancellation.join()
waiter.join()
assertTrue(waiter.isCancelled)
assertEquals(3, semaphore.freePermits, "Cancellation must return both partially acquired permits")
semaphore.withAcquire(3) {
assertEquals(0, semaphore.freePermits)
}
assertEquals(3, semaphore.freePermits)
}
@Test
fun cancelledAcquireWithoutPermitsDoesNotReleaseAnotherHoldersPermit() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 1, acquiredPermits = 1)
val waiter = launch(Dispatchers.Unconfined) {
semaphore.acquire()
}
assertFalse(waiter.isCompleted)
waiter.cancelAndJoin()
assertEquals(0, semaphore.freePermits, "A cancelled waiter that acquired nothing must release nothing")
assertTrue(semaphore.release())
assertEquals(1, semaphore.freePermits)
}
@Test
fun tryAcquireUsesAvailablePermits() = runTest {
val semaphore = SmartSemaphore.Mutable(permits = 3)
assertTrue(semaphore.tryAcquire(2))
assertEquals(1, semaphore.freePermits)
assertTrue(semaphore.tryAcquire())
assertEquals(0, semaphore.freePermits)
assertTrue(semaphore.release(3))
assertEquals(3, semaphore.freePermits)
}
@Test
fun tryAcquireWithInsufficientPermitsLeavesStateUnchanged() = runTest {
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 2)
assertFalse(semaphore.tryAcquire(2))
assertEquals(1, semaphore.freePermits)
semaphore.acquire()
assertFalse(semaphore.tryAcquire())
assertEquals(0, semaphore.freePermits)
}
}

View File

@@ -18,5 +18,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.30.2 version=0.30.0
android_code_version=316 android_code_version=315

View File

@@ -6,7 +6,7 @@ kt-coroutines = "1.11.0"
kotlinx-browser = "0.5.0" kotlinx-browser = "0.5.0"
kslog = "1.7.0" kslog = "1.6.1"
jb-compose = "1.11.1" jb-compose = "1.11.1"
jb-compose-material3 = "1.11.0-alpha07" jb-compose-material3 = "1.11.0-alpha07"
@@ -14,18 +14,18 @@ jb-compose-icons = "1.7.8"
jb-exposed = "1.3.0" jb-exposed = "1.3.0"
jb-dokka = "2.2.0" jb-dokka = "2.2.0"
sqlite = "3.53.2.1" sqlite = "3.53.2.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.5.2" ktor = "3.5.1"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.2.2" koin = "4.2.2"
okio = "3.18.1" okio = "3.17.0"
ksp = "2.3.9" ksp = "2.3.9"
kotlin-poet = "2.3.0" kotlin-poet = "2.3.0"
@@ -33,7 +33,7 @@ kotlin-poet = "2.3.0"
versions = "0.54.0" versions = "0.54.0"
nmcp = "1.5.0" nmcp = "1.5.0"
android-gradle = "8.12.+" android-gradle = "8.13.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.19.0" android-coreKtx = "1.19.0"

View File

@@ -53,7 +53,7 @@ class Processor(
val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses( val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(), searchIn = resolver.getAllFiles(),
allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
).distinct() ).distinct()
val subClassesNames = subClasses.filter { val subClassesNames = subClasses.filter {
when (it.classKind) { when (it.classKind) {
@@ -165,7 +165,15 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = withNoSuchElementWorkaround(null) { (it.getGenerateSealedWorkaroundAnnotation) ?.prefix } ?.takeIf { val prefix = runCatching {
(it.getGenerateSealedWorkaroundAnnotation) ?.prefix
}.getOrElse {
if (it is NoSuchElementException) {
""
} else {
throw it
}
} ?.takeIf {
it.isNotEmpty() it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {