Skip to content

Commit

Permalink
fix(model-api): node resolution inside coroutines
Browse files Browse the repository at this point in the history
Moved the class ContextValue to a new library and removed the
duplication. Then properly implemented the integration between
coroutines and threads, also for JS.
  • Loading branch information
slisson committed Sep 5, 2023
1 parent 45f6358 commit 0f4ed10
Show file tree
Hide file tree
Showing 31 changed files with 530 additions and 88 deletions.
50 changes: 50 additions & 0 deletions kotlin-utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
plugins {
`maven-publish`
id("org.jetbrains.kotlin.multiplatform")
}

kotlin {
jvm()
js(IR) {
browser {}
nodejs {
testTask(
Action {
useMocha {
timeout = "30s"
}
},
)
}
useCommonJs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlin.coroutines.core)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlin.coroutines.test)
}
}
val jvmMain by getting {
dependencies {
}
}
val jvmTest by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
}
}
val jsTest by getting {
dependencies {
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modelix.kotlin.utils

expect class ContextValue<E> {

constructor()
constructor(defaultValue: E)

fun getValue(): E
fun getValueOrNull(): E?
fun getAllValues(): List<E>
fun <T> computeWith(newValue: E, body: () -> T): T

suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T
}

fun <E, T> ContextValue<E>.offer(value: E, body: () -> T): T {
return if (getAllValues().isEmpty()) {
computeWith(value, body)
} else {
body()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.kotlin.utils

expect inline fun <R> runSynchronized(lock: Any, block: () -> R): R
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.kotlin.utils

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds

class ContextValueTests {

@Test
fun multipleCoroutines() = runTest {
val contextValue = ContextValue<String>("a")
assertEquals("a", contextValue.getValueOrNull())
coroutineScope {
launch {
for (i in 1..10) {
contextValue.runInCoroutine("b1") {
contextValue.computeWith("b11") {
assertEquals("b11", contextValue.getValueOrNull())
}
assertEquals("b1", contextValue.getValueOrNull())
contextValue.computeWith("b12") {
assertEquals("b12", contextValue.getValueOrNull())
}
delay(1.milliseconds)
}
}
}
launch {
for (i in 1..10) {
contextValue.runInCoroutine("b2") {
assertEquals("b2", contextValue.getValueOrNull())
delay(1.milliseconds)
}
}
}
for (i in 1..5) {
contextValue.runInCoroutine("c") {
assertEquals("c", contextValue.getValueOrNull())
delay(1.milliseconds)
}
}
}
contextValue.computeWith("d") {
assertEquals("d", contextValue.getValueOrNull())
}
assertEquals("a", contextValue.getValueOrNull())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modelix.kotlin.utils

import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.withContext
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.Continuation
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext

actual class ContextValue<E> {
private val initialStack: List<E>
private var synchronousValueStack: List<E>? = null
private var stackFromCoroutine: List<E>? = null
private val contextElementKey = object : CoroutineContext.Key<ContextValueElement<E>> {}
private var isInSynchronousBlock = false

actual constructor() {
initialStack = emptyList()
}

actual constructor(defaultValue: E) {
initialStack = listOf(defaultValue)
}

actual fun getValue(): E {
return getAllValues().last()
}

actual fun getValueOrNull(): E? {
return getAllValues().lastOrNull()
}

actual fun <T> computeWith(newValue: E, body: () -> T): T {
val oldStack = synchronousValueStack
val newStack = getAllValues() + newValue
val wasInSynchronousBlock = isInSynchronousBlock
try {
isInSynchronousBlock = true
synchronousValueStack = newStack
return body()
} finally {
synchronousValueStack = oldStack
isInSynchronousBlock = wasInSynchronousBlock
}
}

actual fun getAllValues(): List<E> {
return (if (isInSynchronousBlock) synchronousValueStack else stackFromCoroutine) ?: initialStack
}

private suspend fun getAllValuesFromCoroutine(): List<E>? {
return currentCoroutineContext()[contextElementKey]?.stack
}

actual suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T {
return withContext(ContextValueElement((getAllValuesFromCoroutine() ?: initialStack) + newValue)) {
val parentInterceptor = checkNotNull(currentCoroutineContext()[ContinuationInterceptor]) {
"No ContinuationInterceptor found in the context"
}
withContext(Interceptor(parentInterceptor)) {
body()
}
}
}

private inner class Interceptor(
private val dispatcher: ContinuationInterceptor,
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return dispatcher.interceptContinuation(object : Continuation<T> {
override val context get() = continuation.context

override fun resumeWith(result: Result<T>) {
stackFromCoroutine = context[contextElementKey]?.stack ?: emptyList()
continuation.resumeWith(result)
}
})
}

override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
super.releaseInterceptedContinuation(continuation)
stackFromCoroutine = null
}
}

inner class ContextValueElement<E>(val stack: List<E>) : AbstractCoroutineContextElement(contextElementKey)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.kotlin.utils

actual inline fun <R> runSynchronized(lock: Any, block: () -> R): R {
return block()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modelix.kotlin.utils

import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.withContext

actual class ContextValue<E>(private val initialStack: List<E>) {

private val valueStack = ThreadLocal.withInitial { initialStack }

actual constructor() : this(emptyList())

actual constructor(defaultValue: E) : this(listOf(defaultValue))

actual fun <T> computeWith(newValue: E, body: () -> T): T {
val oldStack: List<E> = valueStack.get()
return try {
valueStack.set(oldStack + newValue)
body()
} finally {
valueStack.set(oldStack)
}
}

actual suspend fun <T> runInCoroutine(newValue: E, body: suspend () -> T): T {
return withContext(valueStack.asContextElement(getAllValues() + newValue)) {
body()
}
}

actual fun getValue(): E {
return valueStack.get().last()
}

actual fun getValueOrNull(): E? {
return valueStack.get().lastOrNull()
}

actual fun getAllValues(): List<E> {
return valueStack.get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.modelix.kotlin.utils

actual inline fun <R> runSynchronized(lock: Any, block: () -> R): R {
return synchronized(lock, block)
}
1 change: 1 addition & 0 deletions model-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(project(":kotlin-utils"))
implementation(kotlin("stdlib-common"))
implementation(libs.kotlin.logging)
implementation(libs.kotlin.serialization.json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package org.modelix.model.api

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

constructor()
Expand Down
Loading

0 comments on commit 0f4ed10

Please sign in to comment.