Skip to content

Commit dabcf50

Browse files
authored
[YAML] Fix flaky ErrorHandlingTest: don't pickle Scope into user DoFn closures (#40467)
* [YAML] Add test reproducing unpicklable provider state leaking into DoFn closures A user transform's DoFn that captures `self` also captures the closures YAML patches onto it, which hold the Scope and every provider. Any unpicklable provider state (e.g. a protobuf Descriptor) then makes pickling fail. This test injects such state to reproduce the flaky ErrorHandlingTest failures deterministically. * [YAML] Don't pickle Scope (and all providers) into user DoFn closures
1 parent debfa0e commit dabcf50

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

‎sdks/python/apache_beam/yaml/yaml_transform.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ def __init__(
192192
self.input_providers = input_providers
193193
self._all_followers = None
194194

195+
def __reduce__(self):
196+
# Scope is construction-only, but closures attached in create_ptransform
197+
# can pull it (and all providers) into user DoFn pickles. Stub it out.
198+
return str, ('Pickled YAML scope stub.', )
199+
195200
def followers(self, transform_name):
196201
if self._all_followers is None:
197202
self._all_followers = collections.defaultdict(list)

‎sdks/python/apache_beam/yaml/yaml_transform_test.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,47 @@ def test_output_schema_with_main_transform_error_handling_success(self):
834834
''')
835835

836836

837+
class _UnpicklableFactory:
838+
"""A transform factory holding an object that cloudpickle cannot serialize.
839+
840+
Used to simulate provider state (e.g. lazily populated caches in the
841+
process-global standard providers) that should never be pulled into the
842+
pickled closure of an unrelated user transform.
843+
"""
844+
def __init__(self):
845+
from apache_beam.portability.api import beam_runner_api_pb2
846+
self._descriptor = beam_runner_api_pb2.Pipeline.DESCRIPTOR
847+
848+
def __call__(self):
849+
return beam.Map(lambda x: x)
850+
851+
837852
class ErrorHandlingTest(unittest.TestCase):
853+
def test_closure_does_not_capture_unrelated_providers(self):
854+
# SizeLimiter's DoFn closes over `self`. Pickling that DoFn must not drag in
855+
# the YAML Scope and every provider it knows about.
856+
with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions(
857+
pickle_library='cloudpickle')) as p:
858+
result = p | YamlTransform(
859+
'''
860+
type: composite
861+
transforms:
862+
- type: Create
863+
config:
864+
elements: ['a', 'b', 'biiiiig']
865+
- type: SizeLimiter
866+
input: Create
867+
config:
868+
limit: 5
869+
error_handling:
870+
output: errors
871+
output:
872+
good: SizeLimiter
873+
bad: SizeLimiter.errors
874+
''',
875+
providers=dict(TEST_PROVIDERS, Unpicklable=_UnpicklableFactory()))
876+
assert_that(result['good'], equal_to(['a', 'b']), label="CheckGood")
877+
838878
def test_error_handling_outputs(self):
839879
with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions(
840880
pickle_library='cloudpickle')) as p:

0 commit comments

Comments
 (0)