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 b54be00c3 [hotfix][runtime/python] Fix Python signature parsing
concurrency issue (#4552)
b54be00c3 is described below
commit b54be00c3c593287f17985b914ea9ae02e467429
Author: yuxiqian <[email protected]>
AuthorDate: Mon Sep 21 17:13:46 2026 +0800
[hotfix][runtime/python] Fix Python signature parsing concurrency issue
(#4552)
---
.../pipeline/tests/stage2/PythonUdfE2eITCase.java | 93 ++++++++++++++++++++++
.../flink/cdc/python/utils/PythonUdfSignature.java | 12 ++-
.../cdc/python/utils/PythonUdfSignatureTest.java | 54 +++++++++++++
3 files changed, 156 insertions(+), 3 deletions(-)
diff --git
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/PythonUdfE2eITCase.java
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/PythonUdfE2eITCase.java
index 873d518cc..e1853f034 100644
---
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/PythonUdfE2eITCase.java
+++
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/PythonUdfE2eITCase.java
@@ -37,6 +37,7 @@ import javax.annotation.Nullable;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -106,6 +107,98 @@ class PythonUdfE2eITCase extends PipelineTestEnvironment {
"DataChangeEvent{tableId=%s.USERS, before=[], after=[4,
[email protected], 84], op=INSERT, meta=()}");
}
+ @Test
+ void testConcurrentLargeSourceSignatureParsing() throws Exception {
+ submitPipelineJob(buildSignaturePipelineJob());
+ waitUntilJobRunning(Duration.ofMinutes(5));
+
+ String tableId = pythonUdfTestDatabase.getDatabaseName() + ".USERS";
+ waitUntilEventOrTaskManagerStops(
+ "CreateTableEvent{tableId="
+ + tableId
+ + ", schema=columns={`ID` INT NOT NULL,`RESULT_ONE`
STRING,"
+ + "`RESULT_TWO` STRING,`RESULT_THREE` STRING},
primaryKeys=ID, options=()}");
+ waitUntilEventOrTaskManagerStops(
+ "DataChangeEvent{tableId="
+ + tableId
+ + ", before=[], after=[1, [email protected],
[email protected], "
+ + "[email protected]], op=INSERT, meta=()}");
+ }
+
+ private void waitUntilEventOrTaskManagerStops(String event) throws
Exception {
+ long deadline = System.currentTimeMillis() +
EVENT_WAITING_TIMEOUT.toMillis();
+ while (System.currentTimeMillis() < deadline) {
+ String taskManagerLog = taskManagerConsumer.toUtf8String();
+ if (taskManagerLog.contains(event)) {
+ return;
+ }
+ if (!taskManager.isRunning()) {
+ throw new AssertionError(
+ "TaskManager exited while waiting for event: "
+ + event
+ + "\nTaskManager log:\n"
+ + taskManagerLog);
+ }
+ Thread.sleep(1000);
+ }
+ throw new TimeoutException("Timed out waiting for event: " + event);
+ }
+
+ private String buildSignaturePipelineJob() {
+ StringBuilder padding = new StringBuilder();
+ for (int i = 0; i < 8192; i++) {
+ padding.append(" None\n");
+ }
+ return String.format(
+ "source:\n"
+ + " type: mysql\n"
+ + " hostname: %s\n"
+ + " port: 3306\n"
+ + " username: %s\n"
+ + " password: %s\n"
+ + " scan.startup.mode: earliest-offset\n"
+ + " tables: %s.USERS\n"
+ + " server-id: 5600-5610\n"
+ + " server-time-zone: UTC\n"
+ + "\n"
+ + "sink:\n"
+ + " type: values\n"
+ + "\n"
+ + "transform:\n"
+ + " - source-table: %s.USERS\n"
+ + " projection: ID, first_identity(EMAIL) AS
RESULT_ONE, "
+ + "second_identity(EMAIL) AS RESULT_TWO, "
+ + "third_identity(EMAIL) AS RESULT_THREE\n"
+ + " filter: ID = 1\n"
+ + "\n"
+ + "pipeline:\n"
+ + " parallelism: 4\n"
+ + " transform.async-execution.enabled: false\n"
+ + " user-defined-function:\n"
+ + "%s"
+ + "%s"
+ + "%s",
+ INTER_CONTAINER_MYSQL_ALIAS,
+ MYSQL_TEST_USER,
+ MYSQL_TEST_PASSWORD,
+ pythonUdfTestDatabase.getDatabaseName(),
+ pythonUdfTestDatabase.getDatabaseName(),
+ signatureUdf("first_identity", padding.toString()),
+ signatureUdf("second_identity", padding.toString()),
+ signatureUdf("third_identity", padding.toString()));
+ }
+
+ private static String signatureUdf(String name, String padding) {
+ return String.format(
+ " - name: %s\n"
+ + " python-code: |\n"
+ + "%s"
+ + " def eval(value: str) -> str:\n"
+ + " return value\n"
+ + " python-executable: %s\n",
+ name, padding, CONTAINER_PYTHON_EXECUTABLE);
+ }
+
private String buildJobYaml(
String projection, @Nullable String filter, List<UdfEntry>
userDefinedFunctions) {
StringBuilder transform =
diff --git
a/flink-cdc-python/src/main/java/org/apache/flink/cdc/python/utils/PythonUdfSignature.java
b/flink-cdc-python/src/main/java/org/apache/flink/cdc/python/utils/PythonUdfSignature.java
index a9a3976aa..ce5de219c 100644
---
a/flink-cdc-python/src/main/java/org/apache/flink/cdc/python/utils/PythonUdfSignature.java
+++
b/flink-cdc-python/src/main/java/org/apache/flink/cdc/python/utils/PythonUdfSignature.java
@@ -61,14 +61,20 @@ public final class PythonUdfSignature {
}
private static String parseReturnAnnotation(String source, String
pythonExec) {
- PythonInterpreterConfig pemjaConfig =
-
PythonInterpreterConfig.newBuilder().setPythonExec(pythonExec).build();
- try (PythonInterpreter parser = new PythonInterpreter(pemjaConfig)) {
+ try (PythonInterpreter parser =
+ new PythonInterpreter(createInterpreterConfig(pythonExec))) {
parser.exec(loadSignatureScript());
return (String) parser.invoke("eval_return_type", source);
}
}
+ static PythonInterpreterConfig createInterpreterConfig(String pythonExec) {
+ return PythonInterpreterConfig.newBuilder()
+ .setPythonExec(pythonExec)
+ .setExcType(PythonInterpreterConfig.ExecType.SUB_INTERPRETER)
+ .build();
+ }
+
private static String loadSignatureScript() {
try (InputStream in =
PythonUdfSignature.class.getResourceAsStream(SIGNATURE_PARSER_RESOURCE)) {
diff --git
a/flink-cdc-python/src/test/java/org/apache/flink/cdc/python/utils/PythonUdfSignatureTest.java
b/flink-cdc-python/src/test/java/org/apache/flink/cdc/python/utils/PythonUdfSignatureTest.java
index 090f8e2ac..bec97c7db 100644
---
a/flink-cdc-python/src/test/java/org/apache/flink/cdc/python/utils/PythonUdfSignatureTest.java
+++
b/flink-cdc-python/src/test/java/org/apache/flink/cdc/python/utils/PythonUdfSignatureTest.java
@@ -21,6 +21,15 @@ import org.apache.flink.cdc.common.types.DataTypes;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import pemja.core.PythonInterpreterConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CyclicBarrier;
+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;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -33,6 +42,51 @@ class PythonUdfSignatureTest {
PemjaTestSupport.requirePemja();
}
+ @Test
+ void usesSubInterpreterForSignatureParsing() {
+ assertThat(
+
PythonUdfSignature.createInterpreterConfig(PemjaTestSupport.PYTHON_EXEC)
+ .getExecType())
+ .isEqualTo(PythonInterpreterConfig.ExecType.SUB_INTERPRETER);
+ }
+
+ @Test
+ void resolvesLargeSourcesConcurrently() throws Exception {
+ int threadCount = 2;
+ int rounds = 8;
+ String source =
+ "PADDING = '"
+ + "x".repeat(256 * 1024)
+ + "'\n"
+ + "def eval(value: bytes) -> bytes:\n"
+ + " return value\n";
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CyclicBarrier barrier = new CyclicBarrier(threadCount);
+ List<Future<?>> futures = new ArrayList<>();
+ try {
+ for (int thread = 0; thread < threadCount; thread++) {
+ futures.add(
+ executor.submit(
+ () -> {
+ barrier.await();
+ for (int round = 0; round < rounds;
round++) {
+ assertThat(
+
PythonUdfSignature.parseReturnType(
+ source,
+
PemjaTestSupport.PYTHON_EXEC))
+ .isEqualTo(DataTypes.BYTES());
+ }
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
@Test
void resolvesEvenWhenOtherFunctionsArePresent() {
String source =