Repository navigation
fix(bigtable): close accelerator daemon and unregister instance on Table GC #19023
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: bigtable-accel
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 | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1420,6 +1420,15 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"{self.__class__.__name__} must be created within an async event loop context." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._instance_finalizer = weakref.finalize( | ||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._finalize_instance_registration, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.client, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.instance_id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.app_profile_id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| id(self), | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._register_instance_future, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Optional in-process accelerator daemon, scoped to this data target's | ||||||||||||||||||||||||||||||||||||||||||||||||
| # (project, instance_id, app_profile_id) tuple. Enabled by default; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1527,6 +1536,7 @@ def _start_accelerator(self) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||
| server.uds_path, server.auth_secret | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon = server | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._daemon_finalizer = weakref.finalize(self, server.close) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except BaseException: | ||||||||||||||||||||||||||||||||||||||||||||||||
| server.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2493,18 +2503,36 @@ async def close(self): | |||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_client = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| if self._accelerator_daemon is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| daemon_finalizer = getattr(self, "_daemon_finalizer", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if daemon_finalizer is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| daemon_finalizer.detach() | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.warning("Failed to close accelerator daemon: %s", exc) | ||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_finalizer = getattr(self, "_instance_finalizer", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if instance_finalizer is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_finalizer.detach() | ||||||||||||||||||||||||||||||||||||||||||||||||
| if self._register_instance_future: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._register_instance_future.cancel() | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.client._remove_instance_registration( | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.instance_id, self.app_profile_id, id(self) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||
| def _finalize_instance_registration( | ||||||||||||||||||||||||||||||||||||||||||||||||
| client: Any, | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||
| app_profile_id: str | None, | ||||||||||||||||||||||||||||||||||||||||||||||||
| owner_id: int, | ||||||||||||||||||||||||||||||||||||||||||||||||
| register_future: Any, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if register_future: | ||||||||||||||||||||||||||||||||||||||||||||||||
| register_future.cancel() | ||||||||||||||||||||||||||||||||||||||||||||||||
| client._remove_instance_registration(instance_id, app_profile_id, owner_id) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2524
to
+2534
Contributor
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. Update
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @CrossSync.convert(sync_name="__enter__") | ||||||||||||||||||||||||||||||||||||||||||||||||
| async def __aenter__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -261,12 +261,11 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| project is None | ||||||||||||||||||||||||||||||||||||||||||||||||
| and credentials is None | ||||||||||||||||||||||||||||||||||||||||||||||||
| and client_options is not None | ||||||||||||||||||||||||||||||||||||||||||||||||
| and (client_options is not None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| and client_options.credentials_file | ||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||
| project = os.getenv( | ||||||||||||||||||||||||||||||||||||||||||||||||
| environment_vars.PROJECT, | ||||||||||||||||||||||||||||||||||||||||||||||||
| os.getenv(environment_vars.LEGACY_PROJECT), | ||||||||||||||||||||||||||||||||||||||||||||||||
| environment_vars.PROJECT, os.getenv(environment_vars.LEGACY_PROJECT) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if project is None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _, project = google.auth.load_credentials_from_file( | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1122,6 +1121,15 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||||||||||||||||||||||||||
| f"{self.__class__.__name__} must be created within an async event loop context." | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) from e | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._instance_finalizer = weakref.finalize( | ||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._finalize_instance_registration, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.client, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.instance_id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.app_profile_id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| id(self), | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._register_instance_future, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1124
to
+1132
Contributor
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. Passing Why this happens:
Solution:Pass a weak reference to the future (
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon: AcceleratorDaemon | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_client: AcceleratorClientType | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_health: AcceleratorHealthMonitorType | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1194,6 +1202,7 @@ def _start_accelerator(self) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||
| server.uds_path, server.auth_secret | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon = server | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._daemon_finalizer = weakref.finalize(self, server.close) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except BaseException: | ||||||||||||||||||||||||||||||||||||||||||||||||
| server.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2046,18 +2055,36 @@ def close(self): | |||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_client = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| if self._accelerator_daemon is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| daemon_finalizer = getattr(self, "_daemon_finalizer", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if daemon_finalizer is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| daemon_finalizer.detach() | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||
| _LOGGER.warning("Failed to close accelerator daemon: %s", exc) | ||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._accelerator_daemon = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_finalizer = getattr(self, "_instance_finalizer", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if instance_finalizer is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_finalizer.detach() | ||||||||||||||||||||||||||||||||||||||||||||||||
| if self._register_instance_future: | ||||||||||||||||||||||||||||||||||||||||||||||||
| self._register_instance_future.cancel() | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.client._remove_instance_registration( | ||||||||||||||||||||||||||||||||||||||||||||||||
| self.instance_id, self.app_profile_id, id(self) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||
| def _finalize_instance_registration( | ||||||||||||||||||||||||||||||||||||||||||||||||
| client: Any, | ||||||||||||||||||||||||||||||||||||||||||||||||
| instance_id: str, | ||||||||||||||||||||||||||||||||||||||||||||||||
| app_profile_id: str | None, | ||||||||||||||||||||||||||||||||||||||||||||||||
| owner_id: int, | ||||||||||||||||||||||||||||||||||||||||||||||||
| register_future: Any, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if register_future: | ||||||||||||||||||||||||||||||||||||||||||||||||
| register_future.cancel() | ||||||||||||||||||||||||||||||||||||||||||||||||
| client._remove_instance_registration(instance_id, app_profile_id, owner_id) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2076
to
+2086
Contributor
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. Update
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def __enter__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||
| """Implement async context manager protocol | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -987,6 +987,79 @@ def capture_daemon(cli_flags=(), **kwargs): | |
|
|
||
| assert "--app-profile" not in captured["cli_flags"] | ||
|
|
||
| def test_gc_and_explicit_close_finalizers(self): | ||
| """Dropping an unclosed Table/TableAsync stops health, closes the daemon, | ||
| and unregisters the instance; explicit close() detaches the finalizers.""" | ||
| import gc | ||
|
|
||
| from google.cloud.bigtable.data._accelerator._daemon import AcceleratorDaemon | ||
| from google.cloud.bigtable.data._async.client import TableAsync | ||
| from google.cloud.bigtable.data._sync_autogen.client import Table | ||
|
|
||
| for table_cls, is_async, call_close in ( | ||
| (TableAsync, True, False), | ||
| (Table, False, False), | ||
| (TableAsync, True, True), | ||
| (Table, False, True), | ||
| ): | ||
| fake_daemon = mock.Mock( | ||
| spec=AcceleratorDaemon, uds_path="/tmp/s", auth_secret="sec" | ||
| ) | ||
| fake_health = mock.Mock(close=mock.AsyncMock() if is_async else mock.Mock()) | ||
| fake_client = mock.Mock(close=mock.AsyncMock() if is_async else mock.Mock()) | ||
| fake_future = mock.Mock() | ||
|
Contributor
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. Since class WeakableMock(mock.Mock):
__slots__ = ("__weakref__",)
fake_future = WeakableMock() |
||
| remove_reg = mock.Mock() | ||
| client = SimpleNamespace( | ||
| project="p", | ||
| _emulator_host=None, | ||
| _accelerator_blocked_reason=None, | ||
| _accelerator_flags=[], | ||
| _accelerator_env={}, | ||
| _use_accelerator=True, | ||
| _executor=None, | ||
| _gapic_client=mock.Mock( | ||
| instance_path=lambda p, i: f"projects/{p}/instances/{i}" | ||
| ), | ||
| _register_instance=mock.Mock(), | ||
| _remove_instance_registration=remove_reg, | ||
| ) | ||
| mod = ( | ||
| "google.cloud.bigtable.data._async.client" | ||
| if is_async | ||
| else "google.cloud.bigtable.data._sync_autogen.client" | ||
| ) | ||
| task_fn = ( | ||
| "CrossSync.create_task" | ||
| if is_async | ||
| else "CrossSync._Sync_Impl.create_task" | ||
| ) | ||
| with ( | ||
| mock.patch(f"{mod}.{task_fn}", return_value=fake_future), | ||
| mock.patch(f"{mod}.AcceleratorDaemon", return_value=fake_daemon), | ||
| mock.patch(f"{mod}.AcceleratorClientType", return_value=fake_client), | ||
| mock.patch( | ||
| f"{mod}.AcceleratorHealthMonitorType", return_value=fake_health | ||
| ), | ||
| mock.patch.object(table_cls, "_verify_daemon_identity"), | ||
| ): | ||
| table = table_cls(client, "inst-1", "tbl-1", app_profile_id="prof-1") | ||
| table_id = id(table) | ||
|
|
||
| if call_close: | ||
| if is_async: | ||
| asyncio.get_event_loop().run_until_complete(table.close()) | ||
| else: | ||
| table.close() | ||
|
|
||
| del table | ||
| gc.collect() | ||
|
|
||
| if not call_close: | ||
| fake_health.request_stop.assert_called_once() | ||
| fake_daemon.close.assert_called_once() | ||
| fake_future.cancel.assert_called_once() | ||
| remove_reg.assert_called_once_with("inst-1", "prof-1", table_id) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Event loop fixture so run_until_complete works | ||
|
|
||
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.
Passing
self._register_instance_futuredirectly toweakref.finalizecreates a reference cycle that prevents theTableAsyncinstance from ever being garbage collected.Why this happens:
weakref.finalizeobjects are stored in a global registry (weakref.finalize._registry) to keep them alive.global registry->finalize object->args->self._register_instance_future.self._register_instance_futureis a Task/Future wrapping a coroutine ofself, it holds a strong reference toself.selfis reachable from a global root, meaning its reference count will never drop to 0, and the cyclic garbage collector will never collect it.Solution:
Pass a weak reference to the future (
weakref.ref(self._register_instance_future)) instead of the future itself. This breaks the reference cycle and allows the instance to be garbage collected as expected.