Skip to content

Avoid having shared mutable state between JsonSchema invocations #226

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

Merged
merged 2 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.optimumcode.json.schema
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.schema.OutputCollector.DelegateOutputCollector
import io.github.optimumcode.json.schema.internal.DefaultAssertionContext
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolver
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolverProvider
import io.github.optimumcode.json.schema.internal.IsolatedLoader
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.wrapper.wrap
Expand All @@ -18,7 +18,7 @@ import kotlin.jvm.JvmStatic
*/
public class JsonSchema internal constructor(
private val assertion: JsonSchemaAssertion,
private val referenceResolver: DefaultReferenceResolver,
private val referenceResolverProvider: DefaultReferenceResolverProvider,
) {
/**
* Validates [value] against this [JsonSchema].
Expand Down Expand Up @@ -56,7 +56,7 @@ public class JsonSchema internal constructor(
value: AbstractElement,
errorCollector: ErrorCollector,
): Boolean {
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolverProvider.createResolver())
return DelegateOutputCollector(errorCollector).use {
assertion.validate(value, context, this)
}
Expand All @@ -74,7 +74,7 @@ public class JsonSchema internal constructor(
value: AbstractElement,
outputCollectorProvider: OutputCollector.Provider<T>,
): T {
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolverProvider.createResolver())
val collector = outputCollectorProvider.get()
collector.use {
assertion.validate(value, context, this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ internal class ReferenceHolder(
operator fun component3(): Uri = scopeId
}

internal class DefaultReferenceResolverProvider(
private val references: Map<RefId, AssertionWithPath>,
) {
fun createResolver(): DefaultReferenceResolver = DefaultReferenceResolver(references)
}

internal class DefaultReferenceResolver(
private val references: Map<RefId, AssertionWithPath>,
private val schemaPathsStack: ArrayDeque<Pair<JsonPointer, Uri>> = ArrayDeque(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ private fun createSchema(result: LoadResult): JsonSchema {
.asSequence()
.filter { it.key in result.usedRefs || it.key in dynamicRefs }
.associate { it.key to it.value }
return JsonSchema(result.assertion, DefaultReferenceResolver(usedReferencesWithPath))
return JsonSchema(result.assertion, DefaultReferenceResolverProvider(usedReferencesWithPath))
}

private class LoadResult(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.github.optimumcode.json.schema.cuncurrency

import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.OutputCollector
import io.github.optimumcode.json.schema.SchemaType
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlin.time.Duration.Companion.milliseconds

class ConcurrentExecutionTest : FunSpec() {
init {
val schema =
JsonSchema.Companion.fromDefinition(
"""
{
"properties": {
"inner": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
}
}
}
""".trimIndent(),
defaultType = SchemaType.DRAFT_2020_12,
)

test("BUG #224: JsonSchema can be used concurrently").config(coroutineTestScope = false) {
val target =
buildJsonObject {
put(
"inner",
buildJsonObject {
put("value", JsonPrimitive("test"))
},
)
}
shouldNotThrowAny {
coroutineScope {
withContext(Dispatchers.Default) {
repeat(1000) {
launch {
// delay is added to force suspension and increase changes of catching a concurrent issue within JsonSchema
delay(1.milliseconds)
val result = schema.validate(target, OutputCollector.Companion.flag())
result.valid.shouldBeTrue()
}
}
}
}
}
}
}
}
Loading