Skip to content

Commit d1fbcfb

Browse files
authored
Avoid having shared mutable state between JsonSchema invocations (#226)
Instead of having a shared reference resolver we now create a new one for each `validate` method invocation. This removes the shared mutable state when JsonSchema is invoked from multiple threads. This change should not affect performance much as the reference resolver is created only once per `validate` method invocation. Resolves #224
1 parent 6a5c52f commit d1fbcfb

File tree

4 files changed

+76
-5
lines changed

4 files changed

+76
-5
lines changed

json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.github.optimumcode.json.schema
33
import io.github.optimumcode.json.pointer.JsonPointer
44
import io.github.optimumcode.json.schema.OutputCollector.DelegateOutputCollector
55
import io.github.optimumcode.json.schema.internal.DefaultAssertionContext
6-
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolver
6+
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolverProvider
77
import io.github.optimumcode.json.schema.internal.IsolatedLoader
88
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
99
import io.github.optimumcode.json.schema.internal.wrapper.wrap
@@ -18,7 +18,7 @@ import kotlin.jvm.JvmStatic
1818
*/
1919
public class JsonSchema internal constructor(
2020
private val assertion: JsonSchemaAssertion,
21-
private val referenceResolver: DefaultReferenceResolver,
21+
private val referenceResolverProvider: DefaultReferenceResolverProvider,
2222
) {
2323
/**
2424
* Validates [value] against this [JsonSchema].
@@ -56,7 +56,7 @@ public class JsonSchema internal constructor(
5656
value: AbstractElement,
5757
errorCollector: ErrorCollector,
5858
): Boolean {
59-
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
59+
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolverProvider.createResolver())
6060
return DelegateOutputCollector(errorCollector).use {
6161
assertion.validate(value, context, this)
6262
}
@@ -74,7 +74,7 @@ public class JsonSchema internal constructor(
7474
value: AbstractElement,
7575
outputCollectorProvider: OutputCollector.Provider<T>,
7676
): T {
77-
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
77+
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolverProvider.createResolver())
7878
val collector = outputCollectorProvider.get()
7979
collector.use {
8080
assertion.validate(value, context, this)

json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ internal class ReferenceHolder(
2222
operator fun component3(): Uri = scopeId
2323
}
2424

25+
internal class DefaultReferenceResolverProvider(
26+
private val references: Map<RefId, AssertionWithPath>,
27+
) {
28+
fun createResolver(): DefaultReferenceResolver = DefaultReferenceResolver(references)
29+
}
30+
2531
internal class DefaultReferenceResolver(
2632
private val references: Map<RefId, AssertionWithPath>,
2733
private val schemaPathsStack: ArrayDeque<Pair<JsonPointer, Uri>> = ArrayDeque(),

json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ private fun createSchema(result: LoadResult): JsonSchema {
289289
.asSequence()
290290
.filter { it.key in result.usedRefs || it.key in dynamicRefs }
291291
.associate { it.key to it.value }
292-
return JsonSchema(result.assertion, DefaultReferenceResolver(usedReferencesWithPath))
292+
return JsonSchema(result.assertion, DefaultReferenceResolverProvider(usedReferencesWithPath))
293293
}
294294

295295
private class LoadResult(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.github.optimumcode.json.schema.cuncurrency
2+
3+
import io.github.optimumcode.json.schema.JsonSchema
4+
import io.github.optimumcode.json.schema.OutputCollector
5+
import io.github.optimumcode.json.schema.SchemaType
6+
import io.kotest.assertions.throwables.shouldNotThrowAny
7+
import io.kotest.core.spec.style.FunSpec
8+
import io.kotest.matchers.booleans.shouldBeTrue
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.coroutineScope
11+
import kotlinx.coroutines.delay
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.withContext
14+
import kotlinx.serialization.json.JsonPrimitive
15+
import kotlinx.serialization.json.buildJsonObject
16+
import kotlin.time.Duration.Companion.milliseconds
17+
18+
class ConcurrentExecutionTest : FunSpec() {
19+
init {
20+
val schema =
21+
JsonSchema.Companion.fromDefinition(
22+
"""
23+
{
24+
"properties": {
25+
"inner": {
26+
"type": "object",
27+
"properties": {
28+
"value": {
29+
"type": "string"
30+
}
31+
}
32+
}
33+
}
34+
}
35+
""".trimIndent(),
36+
defaultType = SchemaType.DRAFT_2020_12,
37+
)
38+
39+
test("BUG #224: JsonSchema can be used concurrently").config(coroutineTestScope = false) {
40+
val target =
41+
buildJsonObject {
42+
put(
43+
"inner",
44+
buildJsonObject {
45+
put("value", JsonPrimitive("test"))
46+
},
47+
)
48+
}
49+
shouldNotThrowAny {
50+
coroutineScope {
51+
withContext(Dispatchers.Default) {
52+
repeat(1000) {
53+
launch {
54+
// delay is added to force suspension and increase changes of catching a concurrent issue within JsonSchema
55+
delay(1.milliseconds)
56+
val result = schema.validate(target, OutputCollector.Companion.flag())
57+
result.valid.shouldBeTrue()
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)