Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The calling context impacts whether a mutex or a semaphore are locked on Kotlin/JS #3754

Closed
CLOVIS-AI opened this issue May 14, 2023 · 2 comments
Labels

Comments

@CLOVIS-AI
Copy link
Contributor

My code never interacts with a mutex in any other way than through withLock. My understanding is that it is not possible for the code to fail with "Mutex is locked by <UNLOCKED>". Yet, on Kotlin/JS, it does, while the same code runs fine on Kotlin/JVM.

Describe the bug

Code that only ever uses withLock fails with "Mutex is locked by <UNLOCKED>" on Kotlin/JS, but runs with no issues on Kotlin/JVM.

To reproduce, use:

  • Kotlin 1.8.20
  • KotlinX.Coroutines 1.6.4 or 1.7.0
  • Arrow Core 1.2.0-RC

Add the imports:

import arrow.core.raise.nullable
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.test.runTest
import kotlin.test.assertEquals

1. Mutex

The following code crashes on Kotlin/JS but runs on Kotlin/JVM.

runTest {
	val lock = Mutex()
	val data = HashMap<Int, Unit>()

	assertEquals(
		null,
		nullable {
			val counter = lock.withLock("user") { data[2] }

			if (counter != null)
				Unit
			else
				raise(null)
		},
	)
}
Stacktrace for 1.6.4

IllegalStateException: Mutex is locked by <UNLOCKED> but expected user
	at protoOf.unlock_uksyr8(/tmp/_karma_webpack_490441/commons.js:62756)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_490441/commons.js:3846)
	at protoOf.invoke_qflhgo(/tmp/_karma_webpack_490441/commons.js:3801)
	at slambda.$testBody(/tmp/_karma_webpack_490441/commons.js:3935)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_490441/commons.js:64987)
	at protoOf.invoke_tr8wvu(/tmp/_karma_webpack_490441/commons.js:64973)
	at <global>.l(/tmp/_karma_webpack_490441/commons.js:65020)
	at <global>.startCoroutineUndispatched(/tmp/_karma_webpack_490441/commons.js:61694)
	at protoOf.invoke_gvylz3(/tmp/_karma_webpack_490441/commons.js:52674)
	at protoOf.start_eui3tp(/tmp/_karma_webpack_490441/commons.js:51202)

Stacktrace for 1.7.0

IllegalStateException: This mutex is not locked
	at protoOf.unlock_uksyr8(/tmp/_karma_webpack_931909/commons.js:67235)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_931909/commons.js:3862)
	at protoOf.invoke_qflhgo(/tmp/_karma_webpack_931909/commons.js:3817)
	at slambda.$testBody(/tmp/_karma_webpack_931909/commons.js:3951)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_931909/commons.js:69451)
	at protoOf.resumeWith_7onugl(/tmp/_karma_webpack_931909/commons.js:50388)
	at protoOf.resumeWith_s3a3yh(/tmp/_karma_webpack_931909/commons.js:50435)
	at protoOf.run_mw4iiu(/tmp/_karma_webpack_931909/commons.js:64142)
	at protoOf.processEvent_iukd42(/tmp/_karma_webpack_931909/commons.js:70339)
	at protoOf.tryRunNextTaskUnless_d4q0a1(/tmp/_karma_webpack_931909/commons.js:69987)

2. Semaphore

The same code using a semaphore with a single permit crashes with "IllegalStateException: The number of released permits cannot be greater than 1":

runTest {
	val lock = Semaphore(1)
	val data = HashMap<Int, Unit>()

	assertEquals(
		null,
		nullable {
			val counter = lock.withPermit { data[2] }

			if (counter != null)
				Unit
			else
				raise(null)
		},
	)
}
Stacktrace for 1.6.4

IllegalStateException: The number of released permits cannot be greater than 1
	at protoOf.release_wtm6d2(/tmp/_karma_webpack_490441/commons.js:63351)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_490441/commons.js:4231)
	at protoOf.invoke_qflhgo(/tmp/_karma_webpack_490441/commons.js:4186)
	at slambda.$testBody(/tmp/_karma_webpack_490441/commons.js:4320)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_490441/commons.js:64987)
	at protoOf.invoke_tr8wvu(/tmp/_karma_webpack_490441/commons.js:64973)
	at <global>.l(/tmp/_karma_webpack_490441/commons.js:65020)
	at <global>.startCoroutineUndispatched(/tmp/_karma_webpack_490441/commons.js:61694)
	at protoOf.invoke_gvylz3(/tmp/_karma_webpack_490441/commons.js:52674)
	at protoOf.start_eui3tp(/tmp/_karma_webpack_490441/commons.js:51202)

Stacktrace for 1.7.0

IllegalStateException: The number of released permits cannot be greater than 1
	at protoOf.release_wtm6d2(/tmp/_karma_webpack_931909/commons.js:67834)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_931909/commons.js:4247)
	at protoOf.invoke_qflhgo(/tmp/_karma_webpack_931909/commons.js:4202)
	at slambda.$testBody(/tmp/_karma_webpack_931909/commons.js:4336)
	at protoOf.doResume_5yljmg(/tmp/_karma_webpack_931909/commons.js:69451)
	at protoOf.resumeWith_7onugl(/tmp/_karma_webpack_931909/commons.js:50388)
	at protoOf.resumeWith_s3a3yh(/tmp/_karma_webpack_931909/commons.js:50435)
	at protoOf.run_mw4iiu(/tmp/_karma_webpack_931909/commons.js:64142)
	at protoOf.processEvent_iukd42(/tmp/_karma_webpack_931909/commons.js:70339)
	at protoOf.tryRunNextTaskUnless_d4q0a1(/tmp/_karma_webpack_931909/commons.js:69987)

3. Removing the condition after the call to withLock

If I remove the useless if after the withLock call, the bug disappears, and the behavior on Kotlin/JS and Kotlin/JVM becomes the same (the test is successful).

To me, this means that the error is not caused by Arrow, since I didn't change anything in the way Arrow is used and the bug disappeared.

runTest {
	val lock = Mutex()
	val data = HashMap<Int, Unit>()

	assertEquals(
		null,
		nullable {
			lock.withLock("user") { data[2] }
		},
	)
}

4. Removing the nullable call altogether

However, removing the nullable call altogether also makes the test pass.

runTest {
	val lock = Mutex()
	val data = HashMap<Int, Unit>()

	assertEquals(
		null,
		lock.withLock("user") { data[2] },
	)
}

Other means of reproduction

If a complete project with Gradle setup etc is more convenient for reproduction, the bug is visible in the Pedestal project. To reproduce it:

  • git clone https://gitlab.com/opensavvy/pedestal.git
  • cd pedestal
  • git switch --detach 0cc0c4b9 (explicit for future-proofing; it's currently the latest commit on main)
  • ./gradlew state:jvmTest ← passes
  • ./gradlew state:jsBrowserTest ← fails

The version numbers are stored in the versions.properties at the root of the project, if you want to experiment with other library versions. All the above samples can be copy-pasted as-is in the tests of the state project to reproduce them.

Projects that import Pedestal as a library also exhibit the bug (e.g. it is currently visible in formulaide!158).

@dkhalanskyjb
Copy link
Collaborator

I tweaked your reproducer so it doesn't use our mutex or the Arrow plugin. I filed it at https://youtrack.jetbrains.com/issue/KT-58685/Incorrect-behavior-on-Kotlin-JS

@CLOVIS-AI
Copy link
Contributor Author

CLOVIS-AI commented May 15, 2023

Thanks, I wasn't sure which library was responsible of it. I'll follow the other ticket (as soon as YouTrack is back up).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants