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

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


The following commit(s) were added to refs/heads/master by this push:
     new 85532401e2d [FLINK-40829][python] Allow assign uid for Timestamp 
operators (#29312)
85532401e2d is described below

commit 85532401e2da6c7b19e67c32b02f8f2791120e52
Author: florianvazelle <[email protected]>
AuthorDate: Wed Oct 21 11:30:52 2026 +0200

    [FLINK-40829][python] Allow assign uid for Timestamp operators (#29312)
---
 flink-python/pyflink/datastream/data_stream.py     | 68 +++++++++++++++++++++-
 .../pyflink/datastream/tests/test_data_stream.py   | 60 +++++++++++++++++++
 2 files changed, 126 insertions(+), 2 deletions(-)

diff --git a/flink-python/pyflink/datastream/data_stream.py 
b/flink-python/pyflink/datastream/data_stream.py
index 8be48e0640f..14d660abd45 100644
--- a/flink-python/pyflink/datastream/data_stream.py
+++ b/flink-python/pyflink/datastream/data_stream.py
@@ -15,6 +15,7 @@
 #  See the License for the specific language governing permissions and
 # limitations under the License.
 
################################################################################
+import hashlib
 import typing
 import uuid
 from enum import Enum
@@ -65,6 +66,19 @@ __all__ = ['CloseableIterator', 'DataStream', 'KeyedStream', 
'ConnectedStreams',
 
 WINDOW_STATE_NAME = 'window-contents'
 
+# Appended to the uid of an assign_timestamps_and_watermarks() stream, to 
build one for each of
+# the operators it hides. They are part of the uid, so they must stay as they 
are.
+UID_SUFFIX_EXTRACT_TIMESTAMP = 'extract-timestamp'
+UID_SUFFIX_TIMESTAMPS_AND_WATERMARKS = 'timestamps-and-watermarks'
+
+
+def _derive_uid_hash(uid_hash: str, suffix: str) -> str:
+    """
+    Derives the hash of an operator the user cannot reach from the hash of one 
they can. A hash
+    must be 32 hex digits, so the suffix cannot be appended to it as it is to 
a uid.
+    """
+    return hashlib.sha256(("%s-%s" % (uid_hash, 
suffix)).encode('utf-8')).hexdigest()[:32]
+
 
 class DataStream(object):
     """
@@ -704,8 +718,13 @@ class DataStream(object):
             # step 3: remove the timestamp field which is added in step 1
             JRemoveTimestampMapFunction = 
gateway.jvm.org.apache.flink.streaming.api.functions \
                 .python.eventtime.RemoveTimestampMapFunction
-            result = DataStream(j_watermarked_data_stream.map(
-                JRemoveTimestampMapFunction(), self._j_data_stream.getType()))
+            result = _TimestampsAndWatermarksDataStream(
+                j_watermarked_data_stream.map(
+                    JRemoveTimestampMapFunction(), 
self._j_data_stream.getType()),
+                [(UID_SUFFIX_EXTRACT_TIMESTAMP,
+                  timestamped_data_stream._j_data_stream.getTransformation()),
+                 (UID_SUFFIX_TIMESTAMPS_AND_WATERMARKS,
+                  j_watermarked_data_stream.getTransformation())])
             result.name("Remove-Timestamp")
             return result
         else:
@@ -963,6 +982,51 @@ class DataStream(object):
             return self
 
 
+class _TimestampsAndWatermarksDataStream(DataStream):
+    """
+    The stream which :func:`DataStream.assign_timestamps_and_watermarks` 
returns for a Python
+    TimestampAssigner.
+
+    Such an assigner needs three operators: one extracts the timestamp, one 
assigns the
+    timestamps and the watermarks, and one removes the extracted timestamp 
again. Only the last
+    one is represented by the returned stream, so a uid or a uid hash set on 
it would leave the
+    two others without one. They then make the job impossible to submit if 
auto generated uids
+    are disabled, see 
:func:`~pyflink.common.ExecutionConfig.disable_auto_generated_uids`. This
+    stream gives them one too, derived from the one it receives.
+    """
+
+    def __init__(self, j_data_stream, hidden_j_transformations):
+        super(_TimestampsAndWatermarksDataStream, self).__init__(j_data_stream)
+        # The (uid suffix, transformation) pairs of the two operators the 
assigner hides.
+        self._hidden_j_transformations = hidden_j_transformations
+
+    def uid(self, uid: str) -> 'DataStream':
+        """
+        Sets an ID for this operator, and one derived from it for each of the 
operators the
+        timestamp assigner hides.
+
+        :param uid: The unique user-specified ID of this transformation.
+        :return: The operator with the specified ID.
+        """
+        super(_TimestampsAndWatermarksDataStream, self).uid(uid)
+        for uid_suffix, j_transformation in self._hidden_j_transformations:
+            j_transformation.setUid("%s-%s" % (uid, uid_suffix))
+        return self
+
+    def set_uid_hash(self, uid_hash: str) -> 'DataStream':
+        """
+        Sets an user provided hash for this operator, and one derived from it 
for each of the
+        operators the timestamp assigner hides.
+
+        :param uid_hash: The user provided hash for this operator.
+        :return: The operator with the user provided hash.
+        """
+        super(_TimestampsAndWatermarksDataStream, self).set_uid_hash(uid_hash)
+        for uid_suffix, j_transformation in self._hidden_j_transformations:
+            j_transformation.setUidHash(_derive_uid_hash(uid_hash, uid_suffix))
+        return self
+
+
 class DataStreamSink(object):
     """
     A Stream Sink. This is used for emitting elements from a streaming 
topology.
diff --git a/flink-python/pyflink/datastream/tests/test_data_stream.py 
b/flink-python/pyflink/datastream/tests/test_data_stream.py
index 272473b8450..da91445914e 100644
--- a/flink-python/pyflink/datastream/tests/test_data_stream.py
+++ b/flink-python/pyflink/datastream/tests/test_data_stream.py
@@ -1537,6 +1537,66 @@ class CommonDataStreamTests(PyFlinkTestCase):
         pre_ship_strategy = shuffle_node['predecessors'][0]['ship_strategy']
         self.assertEqual(pre_ship_strategy, 'SHUFFLE')
 
+    def _assign_timestamps(self):
+        class MyTimestampAssigner(TimestampAssigner):
+
+            def extract_timestamp(self, value, record_timestamp) -> int:
+                return int(value[0])
+
+        self.env.get_config().disable_auto_generated_uids()
+
+        ds = self.env.from_collection([(1603708211000, 'hi'), (1603708224000, 
'hello')],
+                                      type_info=Types.TUPLE([Types.LONG(), 
Types.STRING()]))
+        return ds.uid('source') \
+            .assign_timestamps_and_watermarks(
+                WatermarkStrategy.for_monotonous_timestamps()
+                .with_timestamp_assigner(MyTimestampAssigner()))
+
+    def _uids_and_hashes(self, name):
+        """The uid and the uid hash of each operator with the given name, in 
creation order."""
+        j_transformations = {}
+        for j_sink in 
self.env._j_stream_execution_environment.getTransformations():
+            for j_transformation in j_sink.getTransitivePredecessors():
+                j_transformations[j_transformation.getId()] = j_transformation
+        return [(j_transformation.getUid(), 
j_transformation.getUserProvidedNodeHash())
+                for _, j_transformation in sorted(j_transformations.items())
+                if j_transformation.getName() == name]
+
+    def test_timestamp_assigner_uid(self):
+        """A uid on the stream must cover the operators the timestamp assigner 
hides."""
+        
self._assign_timestamps().uid('watermarks').add_sink(self.test_sink).uid('sink')
+
+        # Without a uid on every operator, this raises an 
IllegalStateException.
+        self.env.get_execution_plan()
+
+        # The uids identify the operators in a savepoint, so they must not 
change.
+        self.assertEqual(self._uids_and_hashes('Extract-Timestamp'),
+                         [('watermarks-extract-timestamp', None)])
+        self.assertEqual(self._uids_and_hashes('Timestamps/Watermarks'),
+                         [('watermarks-timestamps-and-watermarks', None)])
+        self.assertEqual(self._uids_and_hashes('Remove-Timestamp'), 
[('watermarks', None)])
+
+    def test_timestamp_assigner_uid_hash(self):
+        
self._assign_timestamps().set_uid_hash('0123456789abcdef0123456789abcdef') \
+            .add_sink(self.test_sink).uid('sink')
+
+        self.env.get_execution_plan()
+
+        self.assertEqual(self._uids_and_hashes('Extract-Timestamp'),
+                         [(None, '2230997a8b7cde93ec413c77bd3b152a')])
+        self.assertEqual(self._uids_and_hashes('Timestamps/Watermarks'),
+                         [(None, 'f20476c038a8d6395d90768849f9d94b')])
+
+    def test_timestamp_assigner_uid_with_python_operators(self):
+        self._assign_timestamps().uid('watermarks') \
+            .map(lambda x: x, output_type=Types.TUPLE([Types.LONG(), 
Types.STRING()])) \
+            .uid('map') \
+            .key_by(lambda x: x[1], key_type=Types.STRING()) \
+            .reduce(lambda a, b: (a[0] + b[0], a[1])).uid('reduce') \
+            .add_sink(self.test_sink).uid('sink')
+
+        self.env.get_execution_plan()
+
     def test_keyed_stream_partitioning(self):
         ds = self.env.from_collection([('ab', 1), ('bdc', 2), ('cfgs', 3), 
('deeefg', 4)])
         keyed_stream = ds.key_by(lambda x: x[1])

Reply via email to