From a8ed436250c7fa14854241f079cd44d4a5828447 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Wed, 1 Jan 2025 19:37:27 +0400 Subject: [PATCH 1/2] Add a test to reproduce the issue --- .../cuncurrency/ConcurrentExecutionTest.kt | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/cuncurrency/ConcurrentExecutionTest.kt diff --git a/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/cuncurrency/ConcurrentExecutionTest.kt b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/cuncurrency/ConcurrentExecutionTest.kt new file mode 100644 index 00000000..32961af4 --- /dev/null +++ b/json-schema-validator/src/commonTest/kotlin/io/github/optimumcode/json/schema/cuncurrency/ConcurrentExecutionTest.kt @@ -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() + } + } + } + } + } + } + } +} \ No newline at end of file From ae3f0c898d6c681b0ca2147ac3ee6163a69355af Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Wed, 1 Jan 2025 19:43:28 +0400 Subject: [PATCH 2/2] Create reference resolver per validation invocation --- .../io/github/optimumcode/json/schema/JsonSchema.kt | 8 ++++---- .../optimumcode/json/schema/internal/ReferenceResolver.kt | 6 ++++++ .../optimumcode/json/schema/internal/SchemaLoader.kt | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt index a0e3d16c..2adfda4e 100644 --- a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt +++ b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt @@ -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 @@ -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]. @@ -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) } @@ -74,7 +74,7 @@ public class JsonSchema internal constructor( value: AbstractElement, outputCollectorProvider: OutputCollector.Provider, ): 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) diff --git a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt index 545c74c9..808b5ea5 100644 --- a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt +++ b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt @@ -22,6 +22,12 @@ internal class ReferenceHolder( operator fun component3(): Uri = scopeId } +internal class DefaultReferenceResolverProvider( + private val references: Map, +) { + fun createResolver(): DefaultReferenceResolver = DefaultReferenceResolver(references) +} + internal class DefaultReferenceResolver( private val references: Map, private val schemaPathsStack: ArrayDeque> = ArrayDeque(), diff --git a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt index 68390a61..13638f19 100644 --- a/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt +++ b/json-schema-validator/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt @@ -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(