| # | |
| # 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. | |
| # | |
| """Pickler for values, functions, and classes. | |
| For internal use only. No backwards compatibility guarantees. | |
| Uses the cloudpickle library to pickle data, functions, lambdas | |
| and classes. | |
| dump_session and load_session are no-ops. | |
| """ | |
| # pytype: skip-file | |
| import base64 | |
| import bz2 | |
| import io | |
| import logging | |
| import sys | |
| import threading | |
| import zlib | |
| from apache_beam.internal import code_object_pickler | |
| from apache_beam.internal.cloudpickle import cloudpickle | |
| from apache_beam.internal.code_object_pickler import get_normalized_path | |
| DEFAULT_CONFIG = cloudpickle.CloudPickleConfig( | |
| skip_reset_dynamic_type_state=True, | |
| filepath_interceptor=get_normalized_path) | |
| STABLE_CODE_IDENTIFIER_CONFIG = cloudpickle.CloudPickleConfig( | |
| skip_reset_dynamic_type_state=True, | |
| filepath_interceptor=get_normalized_path, | |
| get_code_object_params=cloudpickle.GetCodeObjectParams( | |
| get_code_object_identifier=code_object_pickler. | |
| get_code_object_identifier, | |
| get_code_from_identifier=code_object_pickler.get_code_from_identifier)) | |
| try: | |
| from absl import flags | |
| except (ImportError, ModuleNotFoundError): | |
| pass | |
| def _get_proto_enum_descriptor_class(): | |
| try: | |
| from google.protobuf.internal import api_implementation | |
| except ImportError: | |
| return None | |
| implementation_type = api_implementation.Type() | |
| if implementation_type == 'upb': | |
| try: | |
| from google._upb._message import EnumDescriptor | |
| return EnumDescriptor | |
| except ImportError: | |
| pass | |
| elif implementation_type == 'cpp': | |
| try: | |
| from google.protobuf.pyext._message import EnumDescriptor | |
| return EnumDescriptor | |
| except ImportError: | |
| pass | |
| elif implementation_type == 'python': | |
| try: | |
| from google.protobuf.internal.python_message import EnumDescriptor | |
| return EnumDescriptor | |
| except ImportError: | |
| pass | |
| return None | |
| EnumDescriptor = _get_proto_enum_descriptor_class() | |
| # Pickling, especially unpickling, causes broken module imports on Python 3 | |
| # if executed concurrently, see: BEAM-8651, http://bugs.python.org/issue38884. | |
| _pickle_lock = threading.RLock() | |
| RLOCK_TYPE = type(_pickle_lock) | |
| LOCK_TYPE = type(threading.Lock()) | |
| _LOGGER = logging.getLogger(__name__) | |
| # Helper to return an object directly during unpickling. | |
| def _return_obj(obj): | |
| return obj | |
| # Optional import for Python 3.12 TypeAliasType | |
| try: # pragma: no cover - dependent on Python version | |
| from typing import TypeAliasType as _TypeAliasType # type: ignore[attr-defined] | |
| except Exception: | |
| _TypeAliasType = None | |
| def _typealias_reduce(obj): | |
| # Unwrap typing.TypeAliasType to its underlying value for robust pickling. | |
| underlying = getattr(obj, '__value__', None) | |
| if underlying is None: | |
| # Fallback: return the object itself; lets default behavior handle it. | |
| return _return_obj, (obj, ) | |
| return _return_obj, (underlying, ) | |
| def _reconstruct_enum_descriptor(full_name): | |
| for _, module in list(sys.modules.items()): | |
| if not hasattr(module, 'DESCRIPTOR'): | |
| continue | |
| if hasattr(module.DESCRIPTOR, 'enum_types_by_name'): | |
| for (_, enum_desc) in module.DESCRIPTOR.enum_types_by_name.items(): | |
| if enum_desc.full_name == full_name: | |
| return enum_desc | |
| for _, attr_value in vars(module).items(): | |
| if not hasattr(attr_value, 'DESCRIPTOR'): | |
| continue | |
| if hasattr(attr_value.DESCRIPTOR, 'enum_types_by_name'): | |
| for (_, enum_desc) in attr_value.DESCRIPTOR.enum_types_by_name.items(): | |
| if enum_desc.full_name == full_name: | |
| return enum_desc | |
| raise ImportError(f'Could not find enum descriptor: {full_name}') | |
| def _pickle_enum_descriptor(obj): | |
| full_name = obj.full_name | |
| return _reconstruct_enum_descriptor, (full_name, ) | |
| def dumps( | |
| o, | |
| enable_trace=True, | |
| use_zlib=False, | |
| enable_best_effort_determinism=False, | |
| enable_stable_code_identifier_pickling=False, | |
| config: cloudpickle.CloudPickleConfig = DEFAULT_CONFIG) -> bytes: | |
| """For internal use only; no backwards-compatibility guarantees.""" | |
| s = _dumps( | |
| o, | |
| enable_best_effort_determinism, | |
| enable_stable_code_identifier_pickling, | |
| config) | |
| # Compress as compactly as possible (compresslevel=9) to decrease peak memory | |
| # usage (of multiple in-memory copies) and to avoid hitting protocol buffer | |
| # limits. | |
| # WARNING: Be cautious about compressor change since it can lead to pipeline | |
| # representation change, and can break streaming job update compatibility on | |
| # runners such as Dataflow. | |
| if use_zlib: | |
| c = zlib.compress(s, 9) | |
| else: | |
| c = bz2.compress(s, compresslevel=9) | |
| del s # Free up some possibly large and no-longer-needed memory. | |
| return base64.b64encode(c) | |
| def _dumps( | |
| o, | |
| enable_best_effort_determinism=False, | |
| enable_stable_code_identifier_pickling=False, | |
| config: cloudpickle.CloudPickleConfig = DEFAULT_CONFIG) -> bytes: | |
| if enable_best_effort_determinism: | |
| # TODO: Add support once https://github.com/cloudpipe/cloudpickle/pull/563 | |
| # is merged in. | |
| _LOGGER.warning( | |
| 'Ignoring unsupported option: enable_best_effort_determinism. ' | |
| 'This has only been implemented for dill.') | |
| with _pickle_lock: | |
| with io.BytesIO() as file: | |
| if enable_stable_code_identifier_pickling: | |
| config = STABLE_CODE_IDENTIFIER_CONFIG | |
| pickler = cloudpickle.CloudPickler(file, config=config) | |
| try: | |
| pickler.dispatch_table[type(flags.FLAGS)] = _pickle_absl_flags | |
| except NameError: | |
| pass | |
| # Register Python 3.12 `type` alias reducer to unwrap to underlying value. | |
| if _TypeAliasType is not None: | |
| pickler.dispatch_table[_TypeAliasType] = _typealias_reduce | |
| try: | |
| pickler.dispatch_table[RLOCK_TYPE] = _pickle_rlock | |
| except NameError: | |
| pass | |
| try: | |
| pickler.dispatch_table[LOCK_TYPE] = _lock_reducer | |
| except NameError: | |
| pass | |
| if EnumDescriptor is not None: | |
| pickler.dispatch_table[EnumDescriptor] = _pickle_enum_descriptor | |
| pickler.dump(o) | |
| return file.getvalue() | |
| def loads(encoded, enable_trace=True, use_zlib=False): | |
| """For internal use only; no backwards-compatibility guarantees.""" | |
| c = base64.b64decode(encoded) | |
| if use_zlib: | |
| s = zlib.decompress(c) | |
| else: | |
| s = bz2.decompress(c) | |
| del c # Free up some possibly large and no-longer-needed memory. | |
| return _loads(s) | |
| def _loads(s): | |
| with _pickle_lock: | |
| unpickled = cloudpickle.loads(s) | |
| return unpickled | |
| def roundtrip(o): | |
| """Internal utility for testing round-trip pickle serialization.""" | |
| return _loads(_dumps(o)) | |
| def _pickle_absl_flags(obj): | |
| return _create_absl_flags, tuple([]) | |
| def _create_absl_flags(): | |
| return flags.FLAGS | |
| def _pickle_rlock(obj): | |
| return RLOCK_TYPE, tuple([]) | |
| def _lock_reducer(obj): | |
| return threading.Lock, tuple([]) | |
| def dump_session(file_path): | |
| # Since References are saved (https://s.apache.org/beam-picklers), we only | |
| # dump supported Beam Registries (currently only logical type registry) | |
| from apache_beam.coders import typecoders | |
| from apache_beam.typehints import schemas | |
| from apache_beam.typehints.schema_registry import SCHEMA_REGISTRY | |
| with _pickle_lock, open(file_path, 'wb') as file: | |
| coder_reg = typecoders.registry.get_custom_type_coder_tuples() | |
| logical_type_reg = schemas.LogicalType._known_logical_types.copy_custom() | |
| schema_reg = SCHEMA_REGISTRY.get_registered_typings() | |
| pickler = cloudpickle.CloudPickler(file) | |
| # TODO(https://github.com/apache/beam/issues/18500) add file system registry | |
| # once implemented | |
| pickler.dump({ | |
| "coder": coder_reg, | |
| "logical_type": logical_type_reg, | |
| "schema": schema_reg | |
| }) | |
| def load_session(file_path): | |
| from apache_beam.coders import typecoders | |
| from apache_beam.typehints import schemas | |
| from apache_beam.typehints.schema_registry import SCHEMA_REGISTRY | |
| with _pickle_lock, open(file_path, 'rb') as file: | |
| registries = cloudpickle.load(file) | |
| if type(registries) != dict: | |
| raise ValueError( | |
| "Faled loading session: expected dict, got {}", type(registries)) | |
| if "coder" in registries: | |
| typecoders.registry.load_custom_type_coder_tuples(registries["coder"]) | |
| else: | |
| _LOGGER.warning('No coder registry found in saved session') | |
| if "logical_type" in registries: | |
| schemas.LogicalType._known_logical_types.load(registries["logical_type"]) | |
| else: | |
| _LOGGER.warning('No logical type registry found in saved session') | |
| if "schema" in registries: | |
| SCHEMA_REGISTRY.load_registered_typings(registries["schema"]) | |
| else: | |
| _LOGGER.warning('No schema registry found in saved session') |