This is an automated email from the ASF dual-hosted git repository.
jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 4550ac8804b [Python] Preserve data channel failure causes (#40775)
4550ac8804b is described below
commit 4550ac8804bf18d925efa05a823f3db1718a5cfc
Author: Biao Geng <[email protected]>
AuthorDate: Tue Oct 27 00:50:17 2026 +0800
[Python] Preserve data channel failure causes (#40775)
* [Python] Preserve data channel failure causes
Chain premature-close errors from recorded reader failures so bundle
errors retain the original cause and omit unrelated queue timeouts.
Fixes #40774.
Generated-by: OpenAI Codex (GPT-6)
AI-Model: gpt-6
AI-Contributed/Feature: 3/3
AI-Contributed/UT: 50/50
* [Python] Avoid polling delays in data channel regression tests
Inject queue.Empty in the three timeout-driven tests instead of
waiting for each one-second queue poll. Keep the real reader and
consumer paths and their diagnostic assertions intact.
Generated-by: OpenAI Codex (GPT-6)
AI-Model: gpt-6
AI-Contributed/Feature: 0/0
AI-Contributed/UT: 11/11
* [Python] Simplify data channel failure test cleanup
Register the expected close failure with addCleanup and retain the
consumer cause assertion, removing redundant reader and close checks.
Generated-by: OpenAI Codex (gpt-6-astra)
AI-Model: gpt-6-astra
AI-Contributed/Feature: 0/0
AI-Contributed/UT: 27/27
---
.../apache_beam/runners/worker/data_plane.py | 3 +-
.../apache_beam/runners/worker/data_plane_test.py | 56 ++++++++++++++++++++++
2 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/sdks/python/apache_beam/runners/worker/data_plane.py
b/sdks/python/apache_beam/runners/worker/data_plane.py
index 2a626b163a6..582ed6f0daf 100644
--- a/sdks/python/apache_beam/runners/worker/data_plane.py
+++ b/sdks/python/apache_beam/runners/worker/data_plane.py
@@ -591,7 +591,8 @@ class _GrpcDataChannel(DataChannel):
element = received.get(timeout=1)
except queue.Empty:
if self._closed:
- raise RuntimeError('Channel closed prematurely.')
+ raise RuntimeError(
+ 'Channel closed prematurely.') from self._exception
if abort_callback():
return
if self._exception:
diff --git a/sdks/python/apache_beam/runners/worker/data_plane_test.py
b/sdks/python/apache_beam/runners/worker/data_plane_test.py
index 16648f23f5f..07c1af9fe80 100644
--- a/sdks/python/apache_beam/runners/worker/data_plane_test.py
+++ b/sdks/python/apache_beam/runners/worker/data_plane_test.py
@@ -21,8 +21,10 @@
import itertools
import logging
+import queue
import time
import unittest
+from unittest import mock
import grpc
@@ -107,6 +109,60 @@ class DataChannelTest(unittest.TestCase):
finally:
server.stop(0)
+ def test_grpc_data_channel_preserves_input_failure(self):
+ channel = data_plane._GrpcDataChannel()
+ input_error = ValueError('injected data stream failure')
+ self.addCleanup(self.assertRaises, ValueError, channel.close)
+
+ def failed_inputs():
+ raise input_error
+ yield
+
+ with self.assertLogs(data_plane._LOGGER, level=logging.ERROR):
+ with self.assertRaises(ValueError):
+ channel._read_inputs(failed_inputs())
+ channel._received['instruction'] = mock.Mock(
+ spec=data_plane.ByteLimitedQueue,
+ get=mock.Mock(side_effect=queue.Empty))
+ with self.assertRaisesRegex(RuntimeError,
+ 'Channel closed prematurely') as
consumer_error:
+ list(channel.input_elements('instruction', ['input']))
+ self.assertIs(consumer_error.exception.__cause__, input_error)
+
+ def test_grpc_data_channel_premature_eof(self):
+ channel = data_plane._GrpcDataChannel()
+ self.addCleanup(channel.close)
+ channel._read_inputs(iter([]))
+ channel._received['instruction'] = mock.Mock(
+ spec=data_plane.ByteLimitedQueue,
+ get=mock.Mock(side_effect=queue.Empty))
+ with self.assertRaisesRegex(RuntimeError,
+ 'Channel closed prematurely') as
consumer_error:
+ list(channel.input_elements('instruction', ['input']))
+ self.assertIsNone(consumer_error.exception.__cause__)
+ self.assertTrue(consumer_error.exception.__suppress_context__)
+
+ def test_grpc_data_channel_completed_input_before_eof(self):
+ channel = data_plane._GrpcDataChannel()
+ self.addCleanup(channel.close)
+ data = beam_fn_api_pb2.Elements.Data(
+ instruction_id='instruction', transform_id='input', data=b'abc')
+ end = beam_fn_api_pb2.Elements.Data(
+ instruction_id='instruction', transform_id='input', is_last=True)
+ channel._read_inputs(iter([beam_fn_api_pb2.Elements(data=[data, end])]))
+ self.assertEqual(
+ list(channel.input_elements('instruction', ['input'])), [data])
+
+ def test_grpc_data_channel_input_aborted(self):
+ channel = data_plane._GrpcDataChannel()
+ self.addCleanup(channel.close)
+ channel._received['instruction'] = mock.Mock(
+ spec=data_plane.ByteLimitedQueue,
+ get=mock.Mock(side_effect=queue.Empty))
+ self.assertEqual(
+ list(channel.input_elements('instruction', ['input'], lambda: True)),
+ [])
+
def test_in_memory_data_channel(self):
channel = data_plane.InMemoryDataChannel()
self._data_channel_test(channel, channel.inverse())