Repository navigation
Support closures and clean hard-deletion in MultiProcessShared spawn_process (#40673) #40822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import logging | ||
| import multiprocessing.managers | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import threading | ||
| import time | ||
|
|
@@ -38,6 +39,7 @@ | |
|
|
||
| import fasteners | ||
|
|
||
| from apache_beam.internal import pickler | ||
| from apache_beam.utils import retry | ||
|
|
||
| # In some python versions, there is a bug where AutoProxy doesn't handle | ||
|
|
@@ -150,8 +152,22 @@ def release(self, proxy): | |
| def unsafe_hard_delete(self): | ||
| with self.lock: | ||
| if self.initialied: | ||
| obj = self.obj | ||
| del self.obj | ||
| self.initialied = False | ||
| # When hard-deleting in a spawned server process that exits via | ||
| # os._exit(0), an in-flight RPC thread (e.g. a timed-out call) may | ||
| # still hold a stack reference to `obj`, preventing CPython from | ||
| # invoking `obj.__del__()` before `os._exit(0)`. | ||
| if (self._hard_delete_callback is not None and | ||
| sys.getrefcount(obj) > 2 and hasattr(type(obj), '__del__')): | ||
| try: | ||
| type(obj).__del__(obj) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this hang for a model handler with a lock on the model + with_exception_handling(timeout ?
Potentially run the cleanup on a separate thread and wait for it only up to a timeout, then exit regardless, so a cleanup stuck behind the hung call can't block the hard delete?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The model's cleanup potentially runs twice with
A cleanup that isn't safe to repeat would then run twice. Potentially keep the model referenced until exit after running the cleanup (e.g. append it to a module-level list), so |
||
| except Exception: | ||
| logging.warning( | ||
| 'Exception while finalizing object during unsafe_hard_delete', | ||
| exc_info=True) | ||
| del obj | ||
| if self._hard_delete_callback: | ||
| self._hard_delete_callback() | ||
|
|
||
|
|
@@ -232,7 +248,8 @@ def unsafe_hard_delete(self): | |
| self._proxyObject.unsafe_hard_delete() | ||
|
|
||
|
|
||
| def _run_server_process(address_file, tag, constructor, authkey, life_line): | ||
| def _run_server_process( | ||
| address_file, tag, serialized_constructor, authkey, life_line): | ||
| """ | ||
| Runs in a separate process. | ||
| Includes a 'Suicide Pact' monitor: If parent dies, I die. | ||
|
|
@@ -276,6 +293,7 @@ def _monitor_parent(): | |
| logging.getLogger().setLevel(logging.INFO) | ||
| multiprocessing.current_process().authkey = authkey | ||
|
|
||
| constructor = pickler.loads(serialized_constructor) | ||
| serving_manager = _SingletonRegistrar( | ||
| address=('localhost', 0), authkey=authkey) | ||
| _process_level_singleton_manager.set_hard_delete_callback( | ||
|
|
@@ -439,7 +457,19 @@ def unsafe_hard_delete(self): | |
| to this object exist, or (b) you are ok with all existing references to | ||
| this object throwing strange errors when derefrenced. | ||
| """ | ||
| self._get_manager().unsafe_hard_delete_singleton(self._tag) | ||
| address_file = os.path.join(self._path, self._tag) + ".address" | ||
| if (self._manager is None and | ||
| not _process_level_singleton_manager.has_singleton(self._tag) and | ||
| not os.path.exists(address_file)): | ||
| return | ||
| try: | ||
| self._get_manager().unsafe_hard_delete_singleton(self._tag) | ||
| except (EOFError, OSError, BrokenPipeError): | ||
| # Expected when a spawned server process exits via os._exit(0) before | ||
| # sending an RPC reply. | ||
| pass | ||
| finally: | ||
| self._manager = None | ||
|
|
||
| def _create_server(self, address_file): | ||
| if self._spawn_process: | ||
|
|
@@ -459,7 +489,12 @@ def _create_server(self, address_file): | |
| ctx = multiprocessing.get_context('spawn') | ||
| p = ctx.Process( | ||
| target=_run_server_process, | ||
| args=(address_file, self._tag, self._constructor, AUTH_KEY, reader), | ||
| args=( | ||
| address_file, | ||
| self._tag, | ||
| pickler.dumps(self._constructor), | ||
| AUTH_KEY, | ||
| reader), | ||
| daemon=False # Must be False for nested proxies | ||
| ) | ||
| p.start() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium:
sys.getrefcount(obj) > 2relies on CPython incrementing the reference count both for the local variableobjand for passingobjas an argument tosys.getrefcount(). In Python 3.14+, the compiler emitsLOAD_FAST_BORROWfor local variable loads, which does not increment the reference count when pushingobjonto the stack forsys.getrefcount(obj). As a result:obj(total actual references = 2:objlocal + the in-flight frame),sys.getrefcount(obj)returns2on Python 3.14+, so> 2evaluates toFalseand__del__will be skipped beforeos._exit(0).del objfails to drop the refcount to zero (andos._exit(0)is about to terminate the process anyway whenself._hard_delete_callback is not None), we can avoidsys.getrefcountaltogether by checking aweakrefafterdel objor invokingtype(obj).__del__(obj)beforeself._hard_delete_callback()exits the process.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially check for other holders with a weakref instead (drop our reference, then see whether
ref()still returns the object)?