This is an automated email from the ASF dual-hosted git repository.
dianfu pushed a commit to branch release-1.20
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/release-1.20 by this push:
new 7a048417bfe [FLINK-40628][python] Avoid gateway launch during embedded
TTL initialization
7a048417bfe is described below
commit 7a048417bfe41e83bb5555dbfd4cee265c7a95e3
Author: bowenli86 <[email protected]>
AuthorDate: Thu Sep 10 10:08:07 2026 -0700
[FLINK-40628][python] Avoid gateway launch during embedded TTL
initialization
This closes #29155.
Generated-by: Codex (GPT-6)
---
flink-python/pyflink/datastream/state.py | 8 +-
.../pyflink/datastream/tests/test_data_stream.py | 92 ++++++++++++++++++++++
.../datastream/tests/test_state_ttl_config.py | 70 ++++++++++++++++
.../fn_execution/tests/test_java_gateway.py | 78 ++++++++++++++++++
flink-python/pyflink/java_gateway.py | 13 +++
5 files changed, 258 insertions(+), 3 deletions(-)
diff --git a/flink-python/pyflink/datastream/state.py
b/flink-python/pyflink/datastream/state.py
index 4216561299c..5397cca95b7 100644
--- a/flink-python/pyflink/datastream/state.py
+++ b/flink-python/pyflink/datastream/state.py
@@ -834,7 +834,7 @@ class StateTtlConfig(object):
StateTtlConfig.CleanupStrategies.Strategies.ROCKSDB_COMPACTION_FILTER] = \
StateTtlConfig.CleanupStrategies.RocksdbCompactFilterCleanupStrategy(
query_time_after_num_entries,
- periodic_compaction_time if periodic_compaction_time else
Duration.of_days(30))
+ periodic_compaction_time)
return self
def disable_cleanup_in_background(self) -> 'StateTtlConfig.Builder':
@@ -928,13 +928,15 @@ class StateTtlConfig(object):
query_time_after_num_entries: int,
periodic_compaction_time=None):
self._query_time_after_num_entries =
query_time_after_num_entries
- self._periodic_compaction_time = periodic_compaction_time \
- if periodic_compaction_time else Duration.of_days(30)
+ # Creating the default Duration here would start a Py4J
gateway in embedded workers.
+ self._periodic_compaction_time = periodic_compaction_time or
None
def get_query_time_after_num_entries(self) -> int:
return self._query_time_after_num_entries
def get_periodic_compaction_time(self) -> Duration:
+ if self._periodic_compaction_time is None:
+ return Duration.of_days(30)
return self._periodic_compaction_time
EMPTY_STRATEGY = EmptyCleanupStrategy()
diff --git a/flink-python/pyflink/datastream/tests/test_data_stream.py
b/flink-python/pyflink/datastream/tests/test_data_stream.py
index 8d8de717639..4b41ee6323b 100644
--- a/flink-python/pyflink/datastream/tests/test_data_stream.py
+++ b/flink-python/pyflink/datastream/tests/test_data_stream.py
@@ -1255,6 +1255,98 @@ class
EmbeddedDataStreamStreamTests(DataStreamStreamingTests, PyFlinkStreamingTe
config =
get_j_env_configuration(self.env._j_stream_execution_environment)
config.setString("python.execution-mode", "thread")
+ def test_gateway_access_is_rejected(self):
+ self.addCleanup(self.env.set_parallelism, self.env.get_parallelism())
+ self.env.set_parallelism(1)
+
+ class GatewayAccessFunction(MapFunction):
+ def open(self, runtime_context: RuntimeContext):
+ from pemja import findClass
+
+ self.j_integer = findClass('java.lang.Integer')
+
+ def map(self, value):
+ from unittest import TestCase
+ from unittest.mock import patch
+
+ from pyflink.java_gateway import get_gateway, launch_gateway
+
+ with
patch('pyflink.java_gateway.launch_gateway_server_process',
+ side_effect=AssertionError('Gateway subprocess
launched')):
+ for gateway_function in [get_gateway, launch_gateway]:
+ with TestCase().assertRaisesRegex(RuntimeError,
r'pemja\.findClass'):
+ gateway_function()
+
+ return self.j_integer.parseInt(value)
+
+ (self.env.from_collection(['1', '2'], type_info=Types.STRING())
+ .map(GatewayAccessFunction(), output_type=Types.INT())
+ .add_sink(self.test_sink))
+ self.env.execute('test_gateway_access_is_rejected')
+ self.assert_equals_sorted(['1', '2'], self.test_sink.get_results())
+
+ def test_state_ttl_without_gateway(self):
+ self.addCleanup(self.env.set_parallelism, self.env.get_parallelism())
+ self.env.set_parallelism(1)
+ config =
get_j_env_configuration(self.env._j_stream_execution_environment)
+ if config.containsKey("state.backend.type"):
+ self.addCleanup(config.setString, "state.backend.type",
+ config.getString("state.backend.type", None))
+ else:
+ self.addCleanup(config.removeKey, "state.backend.type")
+ config.setString("state.backend.type", "rocksdb")
+
+ class TtlStateFunction(KeyedProcessFunction):
+ def open(self, runtime_context: RuntimeContext):
+ from unittest.mock import patch
+
+ from pyflink.fn_execution.embedded.java_utils import
to_java_state_ttl_config
+
+ self.states = []
+ cases = [('default', None, True),
+ ('explicit_default', 17, True),
+ ('disabled', None, False),
+ ('explicit_disabled', 23, False)]
+ # Guard the actual embedded worker, after the client gateway
already exists.
+ with patch('pyflink.common.time.get_gateway',
side_effect=AssertionError(
+ 'TTL state initialization must not request a Py4J
gateway')):
+ for name, queries, background in cases:
+ builder = StateTtlConfig.new_builder(Time.days(1))
+ if queries is not None:
+ builder.cleanup_in_rocksdb_compact_filter(queries)
+ if not background:
+ builder.disable_cleanup_in_background()
+ ttl_config = builder.build()
+ descriptor = ValueStateDescriptor('ttl_' + name,
Types.INT())
+ descriptor.enable_time_to_live(ttl_config)
+
self.states.append(runtime_context.get_state(descriptor))
+
+ j_ttl_config = to_java_state_ttl_config(ttl_config)
+ assert j_ttl_config.getTimeToLive().toMillis() ==
86400000
+ cleanup = j_ttl_config.getCleanupStrategies()
+ assert cleanup.isCleanupInBackground() == background
+ rocksdb_cleanup =
cleanup.getRocksdbCompactFilterCleanupStrategy()
+ if background or queries is not None:
+ assert
rocksdb_cleanup.getQueryTimeAfterNumEntries() == (
+ 1000 if queries is None else queries)
+ assert
rocksdb_cleanup.getPeriodicCompactionTime().toMillis() == (
+ 30 * 24 * 60 * 60 * 1000)
+ else:
+ assert rocksdb_cleanup is None
+
+ def process_element(self, value, ctx):
+ for index, state in enumerate(self.states):
+ state.update((state.value() or 0) + value)
+ yield index, state.value()
+
+ (self.env.from_collection([1, 2], type_info=Types.INT())
+ .key_by(lambda value: 0)
+ .process(TtlStateFunction(), output_type=Types.TUPLE([Types.INT(),
Types.INT()]))
+ .add_sink(self.test_sink))
+ self.env.execute('test_state_ttl_without_gateway')
+ expected = ['(%s,%s)' % (index, total) for index in range(4) for total
in [1, 3]]
+ self.assert_equals_sorted(expected, self.test_sink.get_results())
+
def test_metrics(self):
ds = self.env.from_collection(
[('ab', 'a', decimal.Decimal(1)),
diff --git a/flink-python/pyflink/datastream/tests/test_state_ttl_config.py
b/flink-python/pyflink/datastream/tests/test_state_ttl_config.py
new file mode 100644
index 00000000000..3b556d5518b
--- /dev/null
+++ b/flink-python/pyflink/datastream/tests/test_state_ttl_config.py
@@ -0,0 +1,70 @@
+################################################################################
+# 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.
+################################################################################
+import unittest
+from unittest.mock import patch
+
+from pyflink.common.time import Duration, Time
+from pyflink.datastream.state import StateTtlConfig
+
+
+class StateTtlConfigTests(unittest.TestCase):
+
+ def test_default_cleanup_without_gateway(self):
+ with patch('pyflink.common.time.get_gateway',
+ side_effect=AssertionError('Gateway requested')):
+ config = StateTtlConfig.new_builder(Time.days(1)).build()
+ strategy =
config.get_cleanup_strategies().get_rocksdb_compact_filter_cleanup_strategy()
+ self.assertEqual(strategy.get_query_time_after_num_entries(), 1000)
+
+ def test_explicit_cleanup_without_gateway(self):
+ for arguments in [(), (None,), (0,), (False,), ('',)]:
+ with self.subTest(arguments=arguments):
+ with patch('pyflink.common.time.get_gateway',
+ side_effect=AssertionError('Gateway requested')):
+ config = (StateTtlConfig.new_builder(Time.days(1))
+ .cleanup_in_rocksdb_compact_filter(17,
*arguments)
+ .build())
+ strategy = (config.get_cleanup_strategies()
+ .get_rocksdb_compact_filter_cleanup_strategy())
+
self.assertEqual(strategy.get_query_time_after_num_entries(), 17)
+
+ def test_default_duration_is_created_by_accessor(self):
+ with patch('pyflink.common.time.get_gateway') as gateway:
+ strategy =
StateTtlConfig.CleanupStrategies.RocksdbCompactFilterCleanupStrategy(17)
+ gateway.assert_not_called()
+
+ duration = strategy.get_periodic_compaction_time()
+
+ gateway.assert_called_once_with()
+ java_duration = gateway.return_value.jvm.java.time.Duration
+ java_duration.ofDays.assert_called_once_with(30)
+ self.assertIs(duration._j_duration,
java_duration.ofDays.return_value)
+
+ def test_explicit_duration_is_preserved(self):
+ for milliseconds in [0, 3600000]:
+ with self.subTest(milliseconds=milliseconds):
+ with patch('pyflink.common.time.get_gateway'):
+ duration = Duration.of_millis(milliseconds)
+ with patch('pyflink.common.time.get_gateway',
+ side_effect=AssertionError('Gateway requested')):
+ config = (StateTtlConfig.new_builder(Time.days(1))
+ .cleanup_in_rocksdb_compact_filter(17, duration)
+ .build())
+ strategy = (config.get_cleanup_strategies()
+ .get_rocksdb_compact_filter_cleanup_strategy())
+ self.assertIs(strategy.get_periodic_compaction_time(),
duration)
diff --git a/flink-python/pyflink/fn_execution/tests/test_java_gateway.py
b/flink-python/pyflink/fn_execution/tests/test_java_gateway.py
new file mode 100644
index 00000000000..c5f7a5f6ac0
--- /dev/null
+++ b/flink-python/pyflink/fn_execution/tests/test_java_gateway.py
@@ -0,0 +1,78 @@
+################################################################################
+# 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.
+################################################################################
+import os
+import sys
+import unittest
+from unittest.mock import patch
+
+from pyflink import java_gateway
+
+
+class JavaGatewayTests(unittest.TestCase):
+
+ def test_embedded_gateway_access_is_rejected_before_locking(self):
+ for cached_gateway in [None, object()]:
+ for gateway_port in [None, '12345']:
+ with self.subTest(cached=cached_gateway is not None,
port=gateway_port):
+ with patch.dict(sys.modules, {'_pemja': object()}), \
+ patch.dict(os.environ), \
+ patch.object(java_gateway, '_gateway',
cached_gateway), \
+ patch.object(java_gateway, '_lock') as lock:
+ os.environ.pop('PYFLINK_GATEWAY_PORT', None)
+ if gateway_port is not None:
+ os.environ['PYFLINK_GATEWAY_PORT'] = gateway_port
+ lock.__enter__.side_effect = AssertionError('Gateway
lock acquired')
+
+ with self.assertRaises(RuntimeError) as error:
+ java_gateway.get_gateway()
+
+ message = str(error.exception)
+ self.assertIn('pemja.findClass', message)
+ self.assertIn('multithreaded TaskManager can
deadlock', message)
+ lock.__enter__.assert_not_called()
+
+ def test_embedded_gateway_launch_is_rejected(self):
+ with patch.dict(sys.modules, {'_pemja': object()}), \
+ patch.object(java_gateway, 'is_launch_gateway_disabled',
return_value=False), \
+ patch.object(java_gateway, '_find_flink_home'), \
+ patch.object(java_gateway, 'launch_gateway_server_process') as
launch:
+ launch.side_effect = AssertionError('Gateway subprocess launched')
+
+ with self.assertRaisesRegex(RuntimeError, r'pemja\.findClass'):
+ java_gateway.launch_gateway()
+
+ launch.assert_not_called()
+
+ def test_client_gateway_is_available_when_pemja_package_is_imported(self):
+ cached_gateway = object()
+ with patch.dict(sys.modules, {'pemja': object()}), \
+ patch.object(java_gateway, '_gateway', cached_gateway):
+ sys.modules.pop('_pemja', None)
+
+ self.assertIs(java_gateway.get_gateway(), cached_gateway)
+
+ def test_process_worker_gateway_launch_is_still_disabled(self):
+ with patch.dict(sys.modules), \
+ patch.dict(os.environ, {'PYFLINK_GATEWAY_DISABLED': 'true'}), \
+ patch.object(java_gateway, 'launch_gateway_server_process') as
launch:
+ sys.modules.pop('_pemja', None)
+
+ with self.assertRaisesRegex(Exception, 'during Python UDF
execution'):
+ java_gateway.launch_gateway()
+
+ launch.assert_not_called()
diff --git a/flink-python/pyflink/java_gateway.py
b/flink-python/pyflink/java_gateway.py
index a7be4d24f91..284040641cf 100644
--- a/flink-python/pyflink/java_gateway.py
+++ b/flink-python/pyflink/java_gateway.py
@@ -20,6 +20,7 @@ import os
import shlex
import shutil
import struct
+import sys
import tempfile
import time
from logging import WARN
@@ -44,10 +45,21 @@ def is_launch_gateway_disabled():
return False
+def _check_gateway_access():
+ # Pemja registers its native module before loading code in an embedded
interpreter.
+ if '_pemja' in sys.modules:
+ raise RuntimeError(
+ 'Accessing Java through Py4J is not supported in Python thread
mode. '
+ 'Use pemja.findClass to access Java classes instead. Launching a
Py4J gateway '
+ 'subprocess from the multithreaded TaskManager can deadlock. See
FLINK-40628 for '
+ 'details: https://issues.apache.org/jira/browse/FLINK-40628.')
+
+
def get_gateway():
# type: () -> JavaGateway
global _gateway
global _lock
+ _check_gateway_access()
with _lock:
if _gateway is None:
# Set the level to WARN to mute the noisy INFO level logs
@@ -84,6 +96,7 @@ def launch_gateway():
"""
launch jvm gateway
"""
+ _check_gateway_access()
if is_launch_gateway_disabled():
raise Exception("It's launching the PythonGatewayServer during Python
UDF execution "
"which is unexpected. It usually happens when the job
codes are "