Skip to content

fix(bigtable): close accelerator daemon and unregister instance on Table GC - #19023

Open
mutianf wants to merge 1 commit into
bigtable-accelfrom
fix-accelerator-gc-daemon-leak
Open

mutianf wants to merge 1 commit into
bigtable-accelfrom
fix-accelerator-gc-daemon-leak

Conversation

@mutianf

@mutianf mutianf commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

When an unclosed Table, TableAsync, or AcceleratorDaemon is garbage-collected, subprocess.Popen.__del__ runs while proc.stdin is still open and the Go daemon's monitorStdin watchdog is blocked waiting for EOF. Popen.__del__ sees returncode is None and appends proc to subprocess._active, resurrecting proc and its open stdin pipe so the Go daemon subprocess, /tmp/bt-accel-* directory, and client._instance_owners entry are leaked for the lifetime of the Python process.

Register weakref.finalize callbacks on AcceleratorDaemon and _DataApiTarget/_DataApiTargetAsync (detached on explicit close()) so garbage collection closes stdin, reaps the daemon subprocess before Popen.__del__ runs, removes the UDS tempdir, and unregisters the instance from the client.

…ble GC

Register weakref.finalize callbacks on AcceleratorDaemon and _DataApiTarget/_DataApiTargetAsync so garbage-collected tables and daemons close stdin, reap the Go daemon subprocess before Popen.__del__ runs, remove the UDS tempdir, and unregister the instance from the client.
@mutianf
mutianf requested a review from a team as a code owner November 9, 2026 15:54

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements automatic resource cleanup on garbage collection using weakref.finalize for AcceleratorDaemon, TableAsync, and Table instances, ensuring that subprocesses are reaped and registrations are cleaned up even if close() is not explicitly called. The review feedback highlights a critical reference cycle issue: passing self._register_instance_future directly to weakref.finalize prevents the table instances from being garbage-collected. To resolve this, the reviewer suggests passing a weak reference to the future, updating the finalizer to resolve it, and using a custom mock subclass in tests to avoid TypeError since standard mocks do not support weak references.

Comment on lines +1423 to +1431
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Passing self._register_instance_future directly to weakref.finalize creates a reference cycle that prevents the TableAsync instance from ever being garbage collected.

Why this happens:

  1. weakref.finalize objects are stored in a global registry (weakref.finalize._registry) to keep them alive.
  2. This creates a strong reference path: global registry -> finalize object -> args -> self._register_instance_future.
  3. Since self._register_instance_future is a Task/Future wrapping a coroutine of self, it holds a strong reference to self.
  4. Therefore, self is 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.

Suggested change
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,
)
self._instance_finalizer = weakref.finalize(
self,
self._finalize_instance_registration,
self.client,
self.instance_id,
self.app_profile_id,
id(self),
weakref.ref(self._register_instance_future),
)

Comment on lines +2524 to +2534
@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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Update _finalize_instance_registration to resolve the weak reference to the registration future before attempting to cancel it.

Suggested change
@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)
@staticmethod
def _finalize_instance_registration(
client: Any,
instance_id: str,
app_profile_id: str | None,
owner_id: int,
register_future_ref: Any,
) -> None:
register_future = register_future_ref() if register_future_ref else None
if register_future:
register_future.cancel()
client._remove_instance_registration(instance_id, app_profile_id, owner_id)

Comment on lines +1124 to +1132
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Passing self._register_instance_future directly to weakref.finalize creates a reference cycle that prevents the Table instance from ever being garbage collected.

Why this happens:

  1. weakref.finalize objects are stored in a global registry (weakref.finalize._registry) to keep them alive.
  2. This creates a strong reference path: global registry -> finalize object -> args -> self._register_instance_future.
  3. Since self._register_instance_future is a Task/Future wrapping a coroutine of self, it holds a strong reference to self.
  4. Therefore, self is 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.

Suggested change
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,
)
self._instance_finalizer = weakref.finalize(
self,
self._finalize_instance_registration,
self.client,
self.instance_id,
self.app_profile_id,
id(self),
weakref.ref(self._register_instance_future),
)

Comment on lines +2076 to +2086
@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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Update _finalize_instance_registration to resolve the weak reference to the registration future before attempting to cancel it.

Suggested change
@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)
@staticmethod
def _finalize_instance_registration(
client: Any,
instance_id: str,
app_profile_id: str | None,
owner_id: int,
register_future_ref: Any,
) -> None:
register_future = register_future_ref() if register_future_ref else None
if register_future:
register_future.cancel()
client._remove_instance_registration(instance_id, app_profile_id, owner_id)

)
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since mock.Mock objects do not support weak references by default, passing a standard mock to weakref.ref will raise a TypeError. Use a custom subclass of mock.Mock that defines __slots__ with __weakref__ to allow weak referencing in tests.

            class WeakableMock(mock.Mock):
                __slots__ = ("__weakref__",)
            fake_future = WeakableMock()

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant