Skip to content

Commit 0f4ed10

Browse files
committed
fix(model-api): node resolution inside coroutines
Moved the class ContextValue to a new library and removed the duplication. Then properly implemented the integration between coroutines and threads, also for JS.
1 parent 45f6358 commit 0f4ed10

File tree

31 files changed

+530
-88
lines changed

31 files changed

+530
-88
lines changed

kotlin-utils/build.gradle.kts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
plugins {
2+
`maven-publish`
3+
id("org.jetbrains.kotlin.multiplatform")
4+
}
5+
6+
kotlin {
7+
jvm()
8+
js(IR) {
9+
browser {}
10+
nodejs {
11+
testTask(
12+
Action {
13+
useMocha {
14+
timeout = "30s"
15+
}
16+
},
17+
)
18+
}
19+
useCommonJs()
20+
}
21+
sourceSets {
22+
val commonMain by getting {
23+
dependencies {
24+
implementation(libs.kotlin.coroutines.core)
25+
}
26+
}
27+
val commonTest by getting {
28+
dependencies {
29+
implementation(kotlin("test"))
30+
implementation(libs.kotlin.coroutines.test)
31+
}
32+
}
33+
val jvmMain by getting {
34+
dependencies {
35+
}
36+
}
37+
val jvmTest by getting {
38+
dependencies {
39+
}
40+
}
41+
val jsMain by getting {
42+
dependencies {
43+
}
44+
}
45+
val jsTest by getting {
46+
dependencies {
47+
}
48+
}
49+
}
50+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.kotlin.utils
15+
16+
expect class ContextValue<E> {
17+
18+
constructor()
19+
constructor(defaultValue: E)
20+
21+
fun getValue(): E
22+
fun getValueOrNull(): E?
23+
fun getAllValues(): List<E>
24+
fun <T> computeWith(newValue: E, body: () -> T): T
25+
26+
suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T
27+
}
28+
29+
fun <E, T> ContextValue<E>.offer(value: E, body: () -> T): T {
30+
return if (getAllValues().isEmpty()) {
31+
computeWith(value, body)
32+
} else {
33+
body()
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.kotlin.utils
18+
19+
expect inline fun <R> runSynchronized(lock: Any, block: () -> R): R
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.kotlin.utils
18+
19+
import kotlinx.coroutines.coroutineScope
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.launch
22+
import kotlinx.coroutines.test.runTest
23+
import kotlin.test.Test
24+
import kotlin.test.assertEquals
25+
import kotlin.time.Duration.Companion.milliseconds
26+
27+
class ContextValueTests {
28+
29+
@Test
30+
fun multipleCoroutines() = runTest {
31+
val contextValue = ContextValue<String>("a")
32+
assertEquals("a", contextValue.getValueOrNull())
33+
coroutineScope {
34+
launch {
35+
for (i in 1..10) {
36+
contextValue.runInCoroutine("b1") {
37+
contextValue.computeWith("b11") {
38+
assertEquals("b11", contextValue.getValueOrNull())
39+
}
40+
assertEquals("b1", contextValue.getValueOrNull())
41+
contextValue.computeWith("b12") {
42+
assertEquals("b12", contextValue.getValueOrNull())
43+
}
44+
delay(1.milliseconds)
45+
}
46+
}
47+
}
48+
launch {
49+
for (i in 1..10) {
50+
contextValue.runInCoroutine("b2") {
51+
assertEquals("b2", contextValue.getValueOrNull())
52+
delay(1.milliseconds)
53+
}
54+
}
55+
}
56+
for (i in 1..5) {
57+
contextValue.runInCoroutine("c") {
58+
assertEquals("c", contextValue.getValueOrNull())
59+
delay(1.milliseconds)
60+
}
61+
}
62+
}
63+
contextValue.computeWith("d") {
64+
assertEquals("d", contextValue.getValueOrNull())
65+
}
66+
assertEquals("a", contextValue.getValueOrNull())
67+
}
68+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.kotlin.utils
15+
16+
import kotlinx.coroutines.currentCoroutineContext
17+
import kotlinx.coroutines.withContext
18+
import kotlin.coroutines.AbstractCoroutineContextElement
19+
import kotlin.coroutines.Continuation
20+
import kotlin.coroutines.ContinuationInterceptor
21+
import kotlin.coroutines.CoroutineContext
22+
23+
actual class ContextValue<E> {
24+
private val initialStack: List<E>
25+
private var synchronousValueStack: List<E>? = null
26+
private var stackFromCoroutine: List<E>? = null
27+
private val contextElementKey = object : CoroutineContext.Key<ContextValueElement<E>> {}
28+
private var isInSynchronousBlock = false
29+
30+
actual constructor() {
31+
initialStack = emptyList()
32+
}
33+
34+
actual constructor(defaultValue: E) {
35+
initialStack = listOf(defaultValue)
36+
}
37+
38+
actual fun getValue(): E {
39+
return getAllValues().last()
40+
}
41+
42+
actual fun getValueOrNull(): E? {
43+
return getAllValues().lastOrNull()
44+
}
45+
46+
actual fun <T> computeWith(newValue: E, body: () -> T): T {
47+
val oldStack = synchronousValueStack
48+
val newStack = getAllValues() + newValue
49+
val wasInSynchronousBlock = isInSynchronousBlock
50+
try {
51+
isInSynchronousBlock = true
52+
synchronousValueStack = newStack
53+
return body()
54+
} finally {
55+
synchronousValueStack = oldStack
56+
isInSynchronousBlock = wasInSynchronousBlock
57+
}
58+
}
59+
60+
actual fun getAllValues(): List<E> {
61+
return (if (isInSynchronousBlock) synchronousValueStack else stackFromCoroutine) ?: initialStack
62+
}
63+
64+
private suspend fun getAllValuesFromCoroutine(): List<E>? {
65+
return currentCoroutineContext()[contextElementKey]?.stack
66+
}
67+
68+
actual suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T {
69+
return withContext(ContextValueElement((getAllValuesFromCoroutine() ?: initialStack) + newValue)) {
70+
val parentInterceptor = checkNotNull(currentCoroutineContext()[ContinuationInterceptor]) {
71+
"No ContinuationInterceptor found in the context"
72+
}
73+
withContext(Interceptor(parentInterceptor)) {
74+
body()
75+
}
76+
}
77+
}
78+
79+
private inner class Interceptor(
80+
private val dispatcher: ContinuationInterceptor,
81+
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
82+
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
83+
return dispatcher.interceptContinuation(object : Continuation<T> {
84+
override val context get() = continuation.context
85+
86+
override fun resumeWith(result: Result<T>) {
87+
stackFromCoroutine = context[contextElementKey]?.stack ?: emptyList()
88+
continuation.resumeWith(result)
89+
}
90+
})
91+
}
92+
93+
override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
94+
super.releaseInterceptedContinuation(continuation)
95+
stackFromCoroutine = null
96+
}
97+
}
98+
99+
inner class ContextValueElement<E>(val stack: List<E>) : AbstractCoroutineContextElement(contextElementKey)
100+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.kotlin.utils
18+
19+
actual inline fun <R> runSynchronized(lock: Any, block: () -> R): R {
20+
return block()
21+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.kotlin.utils
15+
16+
import kotlinx.coroutines.asContextElement
17+
import kotlinx.coroutines.withContext
18+
19+
actual class ContextValue<E>(private val initialStack: List<E>) {
20+
21+
private val valueStack = ThreadLocal.withInitial { initialStack }
22+
23+
actual constructor() : this(emptyList())
24+
25+
actual constructor(defaultValue: E) : this(listOf(defaultValue))
26+
27+
actual fun <T> computeWith(newValue: E, body: () -> T): T {
28+
val oldStack: List<E> = valueStack.get()
29+
return try {
30+
valueStack.set(oldStack + newValue)
31+
body()
32+
} finally {
33+
valueStack.set(oldStack)
34+
}
35+
}
36+
37+
actual suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T {
38+
return withContext(valueStack.asContextElement(getAllValues() + newValue)) {
39+
body()
40+
}
41+
}
42+
43+
actual fun getValue(): E {
44+
return valueStack.get().last()
45+
}
46+
47+
actual fun getValueOrNull(): E? {
48+
return valueStack.get().lastOrNull()
49+
}
50+
51+
actual fun getAllValues(): List<E> {
52+
return valueStack.get()
53+
}
54+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2023.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.kotlin.utils
18+
19+
actual inline fun <R> runSynchronized(lock: Any, block: () -> R): R {
20+
return synchronized(lock, block)
21+
}

model-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ kotlin {
2929
sourceSets {
3030
val commonMain by getting {
3131
dependencies {
32+
api(project(":kotlin-utils"))
3233
implementation(kotlin("stdlib-common"))
3334
implementation(libs.kotlin.logging)
3435
implementation(libs.kotlin.serialization.json)

model-api/src/commonMain/kotlin/org/modelix/model/api/ContextValue.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package org.modelix.model.api
1515

16+
@Deprecated("use org.modelix.kotlin.utils.ContextValue from org.modelix:kotlin-utils")
1617
expect class ContextValue<E> {
1718

1819
constructor()

0 commit comments

Comments
 (0)