This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new fdd9a7ba2b29 CAMEL-24152: Fix concurrent marshal/unmarshal data
corruption in camel-snakeyaml (#24814)
fdd9a7ba2b29 is described below
commit fdd9a7ba2b29fede18929dc427c75486f9d90f93
Author: Federico Mariani <[email protected]>
AuthorDate: Fri Jul 17 14:47:35 2026 +0200
CAMEL-24152: Fix concurrent marshal/unmarshal data corruption in
camel-snakeyaml (#24814)
Since CAMEL-22354 (4.15.0), all per-thread Yaml instances shared single
BaseConstructor and Representer singletons. These SnakeYAML classes are
heavily stateful and mutated on every load/dump, causing concurrent
crashes and silent cross-thread data bleed.
Create fresh BaseConstructor, Representer, DumperOptions, and Resolver
per cached Yaml in getYaml(), restoring per-thread construction.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../component/snakeyaml/SnakeYAMLDataFormat.java | 18 +---
.../snakeyaml/SnakeYAMLConcurrentTest.java | 104 +++++++++++++++++++++
2 files changed, 109 insertions(+), 13 deletions(-)
diff --git
a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
index a8a3937f3e3d..21074bba843d 100644
---
a/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
+++
b/components/camel-snakeyaml/src/main/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormat.java
@@ -134,18 +134,6 @@ public final class SnakeYAMLDataFormat extends
ServiceSupport implements DataFor
if (allowAnyType) {
typeFilters = "*";
}
- if (this.constructor == null) {
- this.constructor = defaultConstructor(camelContext);
- }
- if (this.representer == null) {
- this.representer = defaultRepresenter();
- }
- if (this.dumperOptions == null) {
- this.dumperOptions = defaultDumperOptions();
- }
- if (this.resolver == null) {
- this.resolver = defaultResolver();
- }
}
@Override
@@ -167,7 +155,11 @@ public final class SnakeYAMLDataFormat extends
ServiceSupport implements DataFor
options.setTagInspector(new TrustedTagInspector());
options.setAllowRecursiveKeys(allowRecursiveKeys);
options.setMaxAliasesForCollections(maxAliasesForCollections);
- yaml = new Yaml(constructor, representer, dumperOptions, options,
resolver);
+ BaseConstructor c = constructor != null ? constructor :
defaultConstructor(camelContext);
+ Representer r = representer != null ? representer :
defaultRepresenter();
+ DumperOptions d = dumperOptions != null ? dumperOptions :
defaultDumperOptions();
+ Resolver res = resolver != null ? resolver : defaultResolver();
+ yaml = new Yaml(c, r, d, options, res);
yamlCache.set(new WeakReference<>(yaml));
}
diff --git
a/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
new file mode 100644
index 000000000000..f526dba74cca
--- /dev/null
+++
b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLConcurrentTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.component.snakeyaml;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.snakeyaml.model.TestPojo;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SnakeYAMLConcurrentTest extends CamelTestSupport {
+
+ private static final int THREAD_COUNT = 8;
+ private static final int ITERATIONS = 500;
+
+ @Test
+ public void testConcurrentMarshalUnmarshal() throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
+ CountDownLatch startLatch = new CountDownLatch(1);
+ List<Future<Void>> futures = new ArrayList<>();
+
+ for (int t = 0; t < THREAD_COUNT; t++) {
+ final int threadId = t;
+ futures.add(executor.submit(() -> {
+ startLatch.await();
+ for (int i = 0; i < ITERATIONS; i++) {
+ String name = "Thread" + threadId + "-Iter" + i;
+ TestPojo original = new TestPojo(name);
+
+ String yaml = template.requestBody("direct:marshal",
original, String.class);
+ TestPojo result = template.requestBody("direct:unmarshal",
yaml, TestPojo.class);
+
+ assertEquals(name, result.getName(),
+ "Data corruption detected: expected '" + name + "'
but got '" + result.getName() + "'");
+ }
+ return null;
+ }));
+ }
+
+ startLatch.countDown();
+
+ executor.shutdown();
+ executor.awaitTermination(60, TimeUnit.SECONDS);
+
+ List<Throwable> errors = new ArrayList<>();
+ for (Future<Void> future : futures) {
+ try {
+ future.get();
+ } catch (Exception e) {
+ errors.add(e.getCause() != null ? e.getCause() : e);
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(errors.size()).append(" thread(s) failed:\n");
+ for (Throwable err : errors) {
+ sb.append(" - ").append(err.getMessage()).append("\n");
+ }
+ fail(sb.toString());
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ SnakeYAMLDataFormat format = new SnakeYAMLDataFormat(TestPojo.class);
+ format.setAllowAnyType(true);
+
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:marshal")
+ .marshal(format);
+ from("direct:unmarshal")
+ .unmarshal(format)
+ .convertBodyTo(TestPojo.class);
+ }
+ };
+ }
+}