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

yuxiqian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new 405d91495 [hotfix][runtime] Fix occasional Janino initialization race 
error (#4558)
405d91495 is described below

commit 405d914957c30885a0496d0de44d8f6c53b6d673
Author: yuxiqian <[email protected]>
AuthorDate: Wed Sep 23 19:00:24 2026 +0800

    [hotfix][runtime] Fix occasional Janino initialization race error (#4558)
---
 .../transform/TransformExpressionCompiler.java     |   3 +
 .../transform/TransformExpressionCompilerTest.java | 129 +++++++++++++++++++++
 2 files changed, 132 insertions(+)

diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
index 33a70a9ce..211f5f669 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
@@ -91,6 +91,9 @@ public class TransformExpressionCompiler {
                         try {
                             // Compile
                             expressionEvaluator.cook(key.getFullExpression());
+                            // Initialize Janino's lazily populated Method 
cache before publishing
+                            // the evaluator to concurrent cache readers.
+                            expressionEvaluator.getMethod();
                         } catch (CompileException e) {
                             throw new InvalidProgramException(
                                     String.format(
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompilerTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompilerTest.java
new file mode 100644
index 000000000..0abb55420
--- /dev/null
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompilerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.flink.cdc.runtime.operators.transform;
+
+import org.codehaus.janino.ExpressionEvaluator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+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 static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link TransformExpressionCompiler}. */
+class TransformExpressionCompilerTest {
+
+    private static final int CONCURRENT_WORKERS = 32;
+    private static final int CONCURRENT_EVALUATION_ROUNDS = 512;
+
+    @AfterEach
+    void cleanUp() {
+        TransformExpressionCompiler.cleanUp();
+    }
+
+    @Test
+    void testCachedEvaluatorSupportsConcurrentFirstEvaluation() throws 
Exception {
+        ExecutorService executor = 
Executors.newFixedThreadPool(CONCURRENT_WORKERS);
+
+        try {
+            for (int round = 0; round < CONCURRENT_EVALUATION_ROUNDS; round++) 
{
+                String compiledExpression = "extras.equals(\"test data\") && " 
+ round + " >= 0";
+                TransformExpressionKey expressionKey =
+                        TransformExpressionKey.of(
+                                "extras = 'test data' AND round = " + round,
+                                compiledExpression,
+                                Collections.singletonList("extras"),
+                                Collections.singletonList(String.class),
+                                Boolean.class,
+                                Collections.singletonMap("extras", "extras"));
+                CountDownLatch evaluatorsReady = new 
CountDownLatch(CONCURRENT_WORKERS);
+                CountDownLatch startEvaluation = new CountDownLatch(1);
+                List<Future<Object>> results = new ArrayList<>();
+                for (int i = 0; i < CONCURRENT_WORKERS; i++) {
+                    results.add(
+                            executor.submit(
+                                    () -> {
+                                        ExpressionEvaluator evaluator;
+                                        try {
+                                            evaluator =
+                                                    
TransformExpressionCompiler.compileExpression(
+                                                            expressionKey, 
Collections.emptyList());
+                                        } finally {
+                                            evaluatorsReady.countDown();
+                                        }
+                                        startEvaluation.await();
+                                        return evaluator.evaluate(
+                                                new Object[] {"test data", 
Collections.emptyMap()});
+                                    }));
+                }
+
+                assertThat(evaluatorsReady.await(30, 
TimeUnit.SECONDS)).isTrue();
+                startEvaluation.countDown();
+                for (Future<Object> result : results) {
+                    assertThat(result.get(30, 
TimeUnit.SECONDS)).isEqualTo(true);
+                }
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    @Test
+    void testCompiledEvaluatorInitializesJaninoMethodCacheBeforePublication() 
throws Exception {
+        TransformExpressionKey expressionKey =
+                TransformExpressionKey.of(
+                        "extras = 'test data'",
+                        "extras.equals(\"test data\")",
+                        Collections.singletonList("extras"),
+                        Collections.singletonList(String.class),
+                        Boolean.class,
+                        Collections.singletonMap("extras", "extras"));
+
+        ExpressionEvaluator evaluator =
+                TransformExpressionCompiler.compileExpression(
+                        expressionKey, Collections.emptyList());
+
+        // Inspect Janino 3.1.10's private cache without calling getMethod(), 
which would lazily
+        // initialize the cache and hide the unsafe-publication regression 
this test guards against.
+        assertThat(getJaninoMethodCache(evaluator))
+                .as(
+                        "The Janino Method cache must be complete before the 
evaluator is "
+                                + "published to concurrent cache readers")
+                .hasSize(1)
+                .doesNotContainNull();
+    }
+
+    private static Method[] getJaninoMethodCache(ExpressionEvaluator 
evaluator) throws Exception {
+        Field scriptEvaluatorField = 
ExpressionEvaluator.class.getDeclaredField("se");
+        scriptEvaluatorField.setAccessible(true);
+        Object scriptEvaluator = scriptEvaluatorField.get(evaluator);
+
+        Field methodCacheField = 
scriptEvaluator.getClass().getDeclaredField("getMethodsCache");
+        methodCacheField.setAccessible(true);
+        return (Method[]) methodCacheField.get(scriptEvaluator);
+    }
+}

Reply via email to