This is an automated email from the ASF dual-hosted git repository.

Croway 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 dddd56f1dd77 CAMEL-24690: camel-python - evaluate under the 
interpreter lock, the globals are shared (#26311)
dddd56f1dd77 is described below

commit dddd56f1dd77f00fe85abb1420bc1f32c76188fc
Author: Federico Mariani <[email protected]>
AuthorDate: Mon Sep 14 12:16:57 2026 +0200

    CAMEL-24690: camel-python - evaluate under the interpreter lock, the 
globals are shared (#26311)
    
    * CAMEL-24690: camel-python - evaluate a python expression under a lock; 
the interpreter globals are shared
    
    PythonExpression keeps one PythonInterpreter per expression and evaluate() 
sets exchange, body,
    headers and properties into its globals, runs the compiled code and calls 
cleanup(), with no
    synchronization. Concurrent exchanges interleave those steps, so one 
exchange can be evaluated
    with another one's body and cleanup() runs while another thread is 
mid-eval. Binding, running and
    cleaning up now happen under the interpreter's monitor, in PythonExpression 
and in the
    ScriptingLanguage entry point of PythonLanguage. A new test evaluates one 
expression from 8 threads
    and checks every result.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    
    * CAMEL-24690: camel-python - compile under the same lock, keep the monitor 
private
    
    PythonInterpreter.compile() reads and writes the interpreter's shared 
CompilerFlags and installs
    its PySystemState as the thread's current one, so compiling outside the 
lock raced the
    evaluation of another thread; the cache miss now runs inside it. Both 
classes synchronize on a
    private lock object rather than the interpreter itself.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../camel/language/python/PythonExpression.java    |  9 +++
 .../camel/language/python/PythonLanguage.java      | 44 +++++++------
 .../python/PythonExpressionConcurrentTest.java     | 76 ++++++++++++++++++++++
 3 files changed, 109 insertions(+), 20 deletions(-)

diff --git 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
index 7202f12b1594..3de078780711 100644
--- 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
+++ 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonExpression.java
@@ -29,6 +29,7 @@ public class PythonExpression extends ExpressionSupport {
     private final Class<?> type;
     private final PythonInterpreter compiler;
     private final PyCode compiledExpression;
+    private final Object lock = new Object();
 
     public PythonExpression(String expressionString, Class<?> type) {
         this.expressionString = expressionString;
@@ -47,6 +48,14 @@ public class PythonExpression extends ExpressionSupport {
 
     @Override
     public <T> T evaluate(Exchange exchange, Class<T> type) {
+        // the interpreter's globals are shared by every evaluation of this 
expression: bind, run and clean up
+        // under one lock so concurrent exchanges cannot see each other's 
bindings
+        synchronized (lock) {
+            return doEvaluate(exchange, type);
+        }
+    }
+
+    private <T> T doEvaluate(Exchange exchange, Class<T> type) {
         try {
             compiler.set("exchange", exchange);
             compiler.set("context", exchange.getContext());
diff --git 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonLanguage.java
 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonLanguage.java
index fbad8206d2b5..e0aa4a8c6d7c 100644
--- 
a/components/camel-python/src/main/java/org/apache/camel/language/python/PythonLanguage.java
+++ 
b/components/camel-python/src/main/java/org/apache/camel/language/python/PythonLanguage.java
@@ -38,6 +38,8 @@ public class PythonLanguage extends TypedLanguageSupport 
implements ScriptingLan
 
     private final PythonInterpreter compiler = new PythonInterpreter();
 
+    private final Object lock = new Object();
+
     private PythonLanguage(Map<String, PyCode> compiledScriptsCache) {
         this.compiledScriptsCache = compiledScriptsCache;
     }
@@ -64,31 +66,33 @@ public class PythonLanguage extends TypedLanguageSupport 
implements ScriptingLan
     public <T> T evaluate(String script, Map<String, Object> bindings, 
Class<T> resultType) {
         script = loadResource(script);
 
-        PyCode code = getCompiledScriptFromCache(script);
-
-        if (code == null) {
+        // compile, bind, run and clean up under one lock: the interpreter's 
compiler flags, system state and
+        // globals are all shared by every caller of this method
+        synchronized (lock) {
+            PyCode code = getCompiledScriptFromCache(script);
+            if (code == null) {
+                try {
+                    code = compiler.compile(script);
+                    addCompiledScriptToCache(script, code);
+                } catch (Exception e) {
+                    throw new ExpressionIllegalSyntaxException(script, e);
+                }
+            }
             try {
-                code = compiler.compile(script);
-                addCompiledScriptToCache(script, code);
+                if (bindings != null) {
+                    bindings.forEach(compiler::set);
+                }
+                PyObject out = compiler.eval(code);
+                if (out != null) {
+                    String value = out.toString();
+                    return 
getCamelContext().getTypeConverter().convertTo(resultType, value);
+                }
             } catch (Exception e) {
                 throw new ExpressionIllegalSyntaxException(script, e);
+            } finally {
+                compiler.cleanup();
             }
         }
-
-        try {
-            if (bindings != null) {
-                bindings.forEach(compiler::set);
-            }
-            PyObject out = compiler.eval(code);
-            if (out != null) {
-                String value = out.toString();
-                return 
getCamelContext().getTypeConverter().convertTo(resultType, value);
-            }
-        } catch (Exception e) {
-            throw new ExpressionIllegalSyntaxException(script, e);
-        } finally {
-            compiler.cleanup();
-        }
         return null;
     }
 
diff --git 
a/components/camel-python/src/test/java/org/apache/camel/language/python/PythonExpressionConcurrentTest.java
 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonExpressionConcurrentTest.java
new file mode 100644
index 000000000000..584222a993d8
--- /dev/null
+++ 
b/components/camel-python/src/test/java/org/apache/camel/language/python/PythonExpressionConcurrentTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.language.python;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.support.DefaultExchange;
+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.assertTrue;
+
+/**
+ * Concurrent evaluations of one python expression must not see each other's 
bindings.
+ */
+public class PythonExpressionConcurrentTest extends CamelTestSupport {
+
+    @Test
+    public void testConcurrentEvaluationsDoNotShareBindings() throws Exception 
{
+        Expression expression = 
context.resolveLanguage("python").createExpression("body + '-' + 
str(headers['n'])");
+        int threads = 8;
+        int rounds = 200;
+        ExecutorService pool = Executors.newFixedThreadPool(threads);
+        CountDownLatch start = new CountDownLatch(1);
+        List<String> failures = new CopyOnWriteArrayList<>();
+        List<Runnable> tasks = new ArrayList<>();
+        for (int t = 0; t < threads; t++) {
+            final int id = t;
+            tasks.add(() -> {
+                try {
+                    start.await();
+                    for (int i = 0; i < rounds; i++) {
+                        Exchange exchange = new DefaultExchange(context);
+                        exchange.getMessage().setBody("t" + id);
+                        exchange.getMessage().setHeader("n", i);
+                        String expected = "t" + id + "-" + i;
+                        String actual = expression.evaluate(exchange, 
String.class);
+                        if (!expected.equals(actual)) {
+                            failures.add(expected + " != " + actual);
+                        }
+                    }
+                } catch (Exception e) {
+                    failures.add(e.toString());
+                }
+            });
+        }
+        tasks.forEach(pool::submit);
+        start.countDown();
+        pool.shutdown();
+        assertTrue(pool.awaitTermination(2, TimeUnit.MINUTES));
+        assertEquals(List.of(), failures);
+    }
+}

Reply via email to