Skip to content

Commit 4181f6f

Browse files
authored
Pickle the relative path for code objects when cloudpickle is used to encode special deterministic types. (#36345)
* Add ability for coders to set version tags for update compat checks. * Fix lint. * draft. * Use relative filepaths when pickling special types with cloudpickle in deterministic fallback coder. * Lint. * Comments. * Update comments.
1 parent ec35441 commit 4181f6f

4 files changed

Lines changed: 92 additions & 19 deletions

File tree

sdks/python/apache_beam/coders/coder_impl.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ cdef class FastPrimitivesCoderImpl(StreamCoderImpl):
8282
cdef object requires_deterministic_step_label
8383
cdef bint warn_deterministic_fallback
8484
cdef bint force_use_dill
85+
cdef bint use_relative_filepaths
8586

8687
@cython.locals(dict_value=dict, int_value=libc.stdint.int64_t,
8788
unicode_value=unicode)

sdks/python/apache_beam/coders/coder_impl.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757

5858
from apache_beam.coders import observable
5959
from apache_beam.coders.avro_record import AvroRecord
60+
from apache_beam.internal import cloudpickle
6061
from apache_beam.internal import cloudpickle_pickler
6162
from apache_beam.typehints.schemas import named_tuple_from_schema
6263
from apache_beam.utils import proto_utils
@@ -377,12 +378,14 @@ def __init__(
377378
self,
378379
fallback_coder_impl,
379380
requires_deterministic_step_label=None,
380-
force_use_dill=False):
381+
force_use_dill=False,
382+
use_relative_filepaths=True):
381383
self.fallback_coder_impl = fallback_coder_impl
382384
self.iterable_coder_impl = IterableCoderImpl(self)
383385
self.requires_deterministic_step_label = requires_deterministic_step_label
384386
self.warn_deterministic_fallback = True
385387
self.force_use_dill = force_use_dill
388+
self.use_relative_filepaths = use_relative_filepaths
386389

387390
@staticmethod
388391
def register_iterable_like_type(t):
@@ -560,8 +563,13 @@ def encode_type(self, t, stream):
560563
return self.encode_type_2_67_0(t, stream)
561564

562565
if t not in _pickled_types:
563-
_pickled_types[t] = cloudpickle_pickler.dumps(
564-
t, config=cloudpickle_pickler.NO_DYNAMIC_CLASS_TRACKING_CONFIG)
566+
config = cloudpickle.CloudPickleConfig(
567+
id_generator=None,
568+
skip_reset_dynamic_type_state=True,
569+
filepath_interceptor=cloudpickle.get_relative_path)
570+
if not self.use_relative_filepaths:
571+
config.filepath_interceptor = None
572+
_pickled_types[t] = cloudpickle_pickler.dumps(t, config=config)
565573
stream.write(_pickled_types[t], True)
566574

567575
def decode_type(self, stream):

sdks/python/apache_beam/coders/coders.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -927,16 +927,24 @@ def _create_impl(self):
927927

928928
class DeterministicFastPrimitivesCoderV2(FastCoder):
929929
"""Throws runtime errors when encoding non-deterministic values."""
930-
def __init__(self, coder, step_label):
930+
def __init__(self, coder, step_label, update_compatibility_version=None):
931931
self._underlying_coder = coder
932932
self._step_label = step_label
933+
self._use_relative_filepaths = True
934+
self._version_tag = "v2_69"
935+
from apache_beam.transforms.util import is_v1_prior_to_v2
936+
# Versions prior to 2.69.0 did not use relative filepaths.
937+
if update_compatibility_version and is_v1_prior_to_v2(
938+
v1=update_compatibility_version, v2="2.69.0"):
939+
self._version_tag = ""
940+
self._use_relative_filepaths = False
933941

934942
def _create_impl(self):
935-
936943
return coder_impl.FastPrimitivesCoderImpl(
937944
self._underlying_coder.get_impl(),
938945
requires_deterministic_step_label=self._step_label,
939-
force_use_dill=False)
946+
force_use_dill=False,
947+
use_relative_filepaths=self._use_relative_filepaths)
940948

941949
def is_deterministic(self):
942950
# type: () -> bool
@@ -962,6 +970,9 @@ def to_runner_api_parameter(self, context):
962970
google.protobuf.wrappers_pb2.BytesValue(value=serialize_coder(self)),
963971
())
964972

973+
def version_tag(self):
974+
return self._version_tag
975+
965976

966977
class DeterministicFastPrimitivesCoder(FastCoder):
967978
"""Throws runtime errors when encoding non-deterministic values."""
@@ -993,11 +1004,8 @@ def to_type_hint(self):
9931004
return Any
9941005

9951006

996-
def _should_force_use_dill():
997-
from apache_beam.coders import typecoders
1007+
def _should_force_use_dill(update_compat_version):
9981008
from apache_beam.transforms.util import is_v1_prior_to_v2
999-
update_compat_version = typecoders.registry.update_compatibility_version
1000-
10011009
if not update_compat_version:
10021010
return False
10031011

@@ -1016,9 +1024,22 @@ def _should_force_use_dill():
10161024

10171025

10181026
def _update_compatible_deterministic_fast_primitives_coder(coder, step_label):
1019-
if _should_force_use_dill():
1027+
""" Returns the update compatible version of DeterministicFastPrimitivesCoder
1028+
The differences are in how "special types" e.g. NamedTuples, Dataclasses are
1029+
deterministically encoded.
1030+
1031+
- In SDK version <= 2.67.0 dill is used to encode "special types"
1032+
- In SDK version 2.68.0 cloudpickle is used to encode "special types" with
1033+
absolute filepaths in code objects and dynamic functions.
1034+
- In SDK version 2.69.0 cloudpickle is used to encode "special types" with
1035+
relative filepaths in code objects and dynamic functions.
1036+
"""
1037+
from apache_beam.coders import typecoders
1038+
update_compat_version = typecoders.registry.update_compatibility_version
1039+
if _should_force_use_dill(update_compat_version):
10201040
return DeterministicFastPrimitivesCoder(coder, step_label)
1021-
return DeterministicFastPrimitivesCoderV2(coder, step_label)
1041+
return DeterministicFastPrimitivesCoderV2(
1042+
coder, step_label, update_compat_version)
10221043

10231044

10241045
class FastPrimitivesCoder(FastCoder):

sdks/python/apache_beam/coders/coders_test_common.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import enum
2424
import logging
2525
import math
26+
import os
2627
import pickle
2728
import subprocess
2829
import sys
@@ -248,12 +249,21 @@ def test_memoizing_pickle_coder(self):
248249
@parameterized.expand([
249250
param(compat_version=None),
250251
param(compat_version="2.67.0"),
252+
param(compat_version="2.68.0"),
251253
])
252254
def test_deterministic_coder(self, compat_version):
255+
""" Test in process determinism for all special deterministic types
256+
257+
- In SDK version <= 2.67.0 dill is used to encode "special types"
258+
- In SDK version 2.68.0 cloudpickle is used to encode "special types" with
259+
absolute filepaths in code objects and dynamic functions.
260+
- In SDK version >=2.69.0 cloudpickle is used to encode "special types"
261+
with relative filepaths in code objects and dynamic functions.
262+
"""
253263

254264
typecoders.registry.update_compatibility_version = compat_version
255265
coder = coders.FastPrimitivesCoder()
256-
if not dill and compat_version:
266+
if not dill and compat_version == "2.67.0":
257267
with self.assertRaises(RuntimeError):
258268
coder.as_deterministic_coder(step_label="step")
259269
self.skipTest('Dill not installed')
@@ -283,7 +293,7 @@ def test_deterministic_coder(self, compat_version):
283293
# Skip this test during cloudpickle. Dill monkey patches the __reduce__
284294
# method for anonymous named tuples (MyNamedTuple) which is not pickleable.
285295
# Since the test is parameterized the type gets colbbered.
286-
if compat_version:
296+
if compat_version == "2.67.0":
287297
self.check_coder(
288298
deterministic_coder, [MyNamedTuple(1, 2), MyTypedNamedTuple(1, 'a')])
289299

@@ -324,8 +334,18 @@ def test_deterministic_coder(self, compat_version):
324334
@parameterized.expand([
325335
param(compat_version=None),
326336
param(compat_version="2.67.0"),
337+
param(compat_version="2.68.0"),
327338
])
328339
def test_deterministic_map_coder_is_update_compatible(self, compat_version):
340+
""" Test in process determinism for map coder including when a component
341+
coder uses DeterministicFastPrimitivesCoder for "special types".
342+
343+
- In SDK version <= 2.67.0 dill is used to encode "special types"
344+
- In SDK version 2.68.0 cloudpickle is used to encode "special types" with
345+
absolute filepaths in code objects and dynamic functions.
346+
- In SDK version >=2.69.0 cloudpickle is used to encode "special types"
347+
with relative file.
348+
"""
329349
typecoders.registry.update_compatibility_version = compat_version
330350
values = [{
331351
MyTypedNamedTuple(i, 'a'): MyTypedNamedTuple('a', i)
@@ -335,7 +355,7 @@ def test_deterministic_map_coder_is_update_compatible(self, compat_version):
335355
coder = coders.MapCoder(
336356
coders.FastPrimitivesCoder(), coders.FastPrimitivesCoder())
337357

338-
if not dill and compat_version:
358+
if not dill and compat_version == "2.67.0":
339359
with self.assertRaises(RuntimeError):
340360
coder.as_deterministic_coder(step_label="step")
341361
self.skipTest('Dill not installed')
@@ -344,8 +364,8 @@ def test_deterministic_map_coder_is_update_compatible(self, compat_version):
344364

345365
assert isinstance(
346366
deterministic_coder._key_coder,
347-
coders.DeterministicFastPrimitivesCoderV2
348-
if not compat_version else coders.DeterministicFastPrimitivesCoder)
367+
coders.DeterministicFastPrimitivesCoderV2 if compat_version
368+
in (None, "2.68.0") else coders.DeterministicFastPrimitivesCoder)
349369

350370
self.check_coder(deterministic_coder, *values)
351371

@@ -681,11 +701,20 @@ def test_param_windowed_value_coder(self):
681701
@parameterized.expand([
682702
param(compat_version=None),
683703
param(compat_version="2.67.0"),
704+
param(compat_version="2.68.0"),
684705
])
685706
def test_cross_process_encoding_of_special_types_is_deterministic(
686707
self, compat_version):
687-
"""Test cross-process determinism for all special deterministic types"""
688-
if compat_version:
708+
"""Test cross-process determinism for all special deterministic types
709+
710+
- In SDK version <= 2.67.0 dill is used to encode "special types"
711+
- In SDK version 2.68.0 cloudpickle is used to encode "special types" with
712+
absolute filepaths in code objects and dynamic functions.
713+
- In SDK version 2.69.0 cloudpickle is used to encode "special types" with
714+
relative filepaths in code objects and dynamic functions.
715+
"""
716+
is_using_dill = compat_version == "2.67.0"
717+
if is_using_dill:
689718
pytest.importorskip("dill")
690719

691720
if sys.executable is None:
@@ -785,6 +814,7 @@ def run_subprocess():
785814
deterministic_coder = coder.as_deterministic_coder("step")
786815

787816
for test_name in results1:
817+
788818
data1 = results1[test_name]
789819
data2 = results2[test_name]
790820

@@ -799,6 +829,19 @@ def run_subprocess():
799829
logging.warning("Could not decode %s data due to %s", test_name, e)
800830
continue
801831

832+
if test_name == "named_tuple_simple" and not is_using_dill:
833+
# The absense of a compat_version means we are using the most recent
834+
# implementation of the coder, which uses relative paths.
835+
should_have_relative_path = not compat_version
836+
named_tuple_type = type(decoded1)
837+
self.assertEqual(
838+
os.path.isabs(named_tuple_type._make.__code__.co_filename),
839+
not should_have_relative_path)
840+
self.assertEqual(
841+
os.path.isabs(
842+
named_tuple_type.__getnewargs__.__globals__['__file__']),
843+
not should_have_relative_path)
844+
802845
self.assertEqual(
803846
decoded1, decoded2, f"Cross-process decoding differs for {test_name}")
804847
self.assertIsInstance(

0 commit comments

Comments
 (0)