Skip to content

test(bigtable): add accelerator shared test harness and concurrency system tests - #18787

Open
mutianf wants to merge 26 commits into
accel11from
accel12
Open

mutianf wants to merge 26 commits into
accel11from
accel12

Conversation

@mutianf

@mutianf mutianf commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Add the shared Cloud Bigtable accelerator system-test harness (_harness.py, _base_async.py, _base_autogen.py, conftest.py, pytest.ini) and the concurrent/multi-process workload system tests (test_concurrency_async.py, test_concurrency_autogen.py).

Part of Stack #18691 (stacked on #18782).

…ule-level import

Change-Id: I533b2c7e1bb78ff725a74db5b00bb701a082b1df
Change-Id: I33e67c9868b186353eff3857406b62ffbfa97eb2
…se for explicit-mode

Change-Id: I030357650201f8681a31d3135cbbff726a4aa650
…rror

Change-Id: I8d4bf5acf04b72af47e6d65829b41d7d8d800165
…User-Agent prefixing

Pass --caller-user-agent python-v<version> when spawning the daemon so it
can construct the full User-Agent prefix (e.g. python-v3.0.0a0-go-accelerator-v3.x.x)
for accurate audit logging and traffic attribution.

Change-Id: I621217335ad54377a84d44ab4b9af88770dd2958
- Remove _AcceleratorUnverified alias (no longer needed)
- Remove duplicate User-Agent flags block from _init_accelerator_config; keep only the early block so the flag is forwarded even when the daemon is blocked
- Move _AcceleratorIdentityError handling into _start_accelerator: close daemon, warn 'could not verify daemon identity', return without re-raising
- Remove test_binary_path_param_bypasses_lookup (binary_path is a test-only knob already exercised implicitly)
- Add test_close_clears_all_state to verify _proc/_tempdir/_uds_path/_log_path/_auth_secret are all None after close()
- Add --app-profile to TestCliFlags.test_cli_flags_appended_to_argv
- Add assert not t._accelerator_breaker.bypass() to test_non_grpc_exception_reraises in both TestMutateRowViaAccelerator and TestReadRowViaAccelerator

Change-Id: Iff4489acc63a6e71211939c23db04ff1a5df10ba

review(bigtable): address PR #18686 reviewer comments

- Remove _AcceleratorUnverified alias (no longer needed)
- Remove duplicate User-Agent flags block from _init_accelerator_config; keep only the early block so the flag is forwarded even when the daemon is blocked
- Add local import google.cloud.bigtable inside _init_accelerator_config to avoid potential circular import at module level
- Move _AcceleratorIdentityError handling into _start_accelerator: close daemon, warn 'could not verify daemon identity', return without re-raising
- Remove test_binary_path_param_bypasses_lookup (binary_path is a test-only knob already exercised implicitly)
- Add --app-profile to TestCliFlags.test_cli_flags_appended_to_argv
- Add assert not t._accelerator_breaker.bypass() to test_non_grpc_exception_reraises in both TestMutateRowViaAccelerator and TestReadRowViaAccelerator

Change-Id: I43b354ad6517a2c0b9f6a2294cdd2d1639d7fe8c

test(bigtable): add TestCleanupTempdir to verify _cleanup_tempdir clears all state

Change-Id: I6d6ec51cc039c78d884451502b368862fe8d4ae3
…e auth session

Change-Id: I78f226004fdffe98d83f7d24b04557a24e9fc7a7
…o prevent orphaned daemon on BaseException

Change-Id: I6f4c18f5b03da26efa9b70ed8c6b4d20f9d4a8df
Change-Id: Iad44827bd86e405524b5aad787e881e2cbf30d14
…rtup timeout

Change-Id: I44684facdfe1f9a1b8807b78bbb4166f8c9ff26b
…ard and Windows skipif

Change-Id: Iba3a175c070fb5f12db54136096f2792bb7f3b7f
…t grace window regardless of sweeper's env

Change-Id: Idf216c34aa7924fd61f258c997c71c426a2cbe34
…ransient _socket_is_live false negatives

Change-Id: If3a0174967e750f04c26b49ccf3e0a4faf4764e0
…sient OSErrors instead of double-probing

Change-Id: Ie6e858f577904cda5a06684afdb9d3aff21dd5ae
…f socket probe

Change-Id: Ifa69e49da685ec3e3e4cd51ec344fb78d5c8f417
… sources and tests

Change-Id: I978a0c1e846f3b95aa2fe5d15dabafec4f0576f9
… checks

fix(bigtable): lock all reads and writes in AcceleratorBreaker

Change-Id: I9d1daffbe086788481d6802cec035a3d4d284729
…, clear stale auth_secret on teardown

Change-Id: I02c7ecb2d9ac5aa082bec0dc3325938b621bd210
Change-Id: I6665fec71ca24efe8ec47f95a71de6dda59abe5e
…), and breaker lock

Change-Id: I21cf617dbea7aa85dc7b0d093e15c1ea22c7f95c
Change-Id: I7a17dd9919dff6e9d38b0cdfb3c600ffc363900c
Change-Id: I6adb2982640fe2d6736ea63ed165c9ffd6feacb7
@mutianf
mutianf requested a review from a team as a code owner October 20, 2026 20:01
@mutianf
mutianf added this pull request to stack #18691 October 20, 2026 20:02

@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 introduces integration and concurrency tests for the Google Cloud Bigtable accelerator, including pytest configurations, base test classes, a state-tracking test harness, and multi-process/multi-threaded concurrency tests. The review feedback suggests several key improvements: cleaning up empty row dictionaries from the state model in _harness.py to prevent memory retention, switching the multiprocessing start method from fork to spawn in test_concurrency_async.py to avoid gRPC-related deadlocks, and adopting a shared monotonic deadline pattern when joining background processes during cleanup to prevent excessive blocking.

Comment on lines +77 to +78
if not col:
self._rows[row_key].pop((family, qualifier), None)

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

When a column qualifier is completely deleted and the column dictionary becomes empty, it is popped from the row. However, if the row itself becomes empty (i.e., has no more column families/qualifiers), the row key is still left in self._rows as an empty dictionary. This causes keys() to return empty rows and leads to unnecessary memory retention. We should clean up the row key if the row dictionary becomes empty.

Suggested change
if not col:
self._rows[row_key].pop((family, qualifier), None)
if not col:
self._rows[row_key].pop((family, qualifier), None)
if not self._rows[row_key]:
self._rows.pop(row_key, None)

Comment on lines +81 to +86
def delete_from_family(self, row_key: bytes, family: str) -> DeleteAllFromFamily:
row = self._rows.get(row_key)
if row is not None:
for key in [k for k in row if k[0] == family]:
del row[key]
return DeleteAllFromFamily(family)

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

Similarly to delete_range_from_column, if all families/qualifiers are deleted from a row during delete_from_family, the row key should be completely removed from self._rows to prevent empty dictionaries from lingering in the state.

Suggested change
def delete_from_family(self, row_key: bytes, family: str) -> DeleteAllFromFamily:
row = self._rows.get(row_key)
if row is not None:
for key in [k for k in row if k[0] == family]:
del row[key]
return DeleteAllFromFamily(family)
def delete_from_family(self, row_key: bytes, family: str) -> DeleteAllFromFamily:
row = self._rows.get(row_key)
if row is not None:
for key in [k for k in row if k[0] == family]:
del row[key]
if not row:
self._rows.pop(row_key, None)
return DeleteAllFromFamily(family)

self, instance_id, table_id, janitor
):
project = os.getenv("GOOGLE_CLOUD_PROJECT") or None
ctx = multiprocessing.get_context("fork")

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

Using the fork start method in Python programs that use gRPC or other multi-threaded C-extensions can lead to deadlocks, crashes, or undefined behavior in child processes because background threads are not inherited. Since the child process workload is fully self-contained and all arguments are pickleable, using spawn is much safer and more robust across different platforms (including macOS and Windows).

Suggested change
ctx = multiprocessing.get_context("fork")
ctx = multiprocessing.get_context("spawn")

Comment on lines +170 to +174
for proc in procs:
proc.join(timeout=30)
if proc.is_alive():
proc.kill()
proc.join()

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

When joining multiple background processes during cleanup, using a fixed timeout per process can cause the cleanup block to block for an excessively long time if multiple processes hang (e.g., up to 120 seconds for 4 processes). Using a shared timeout/deadline pattern based on a monotonic clock ensures that the entire cleanup block respects the overall timeout limit.

Suggested change
for proc in procs:
proc.join(timeout=30)
if proc.is_alive():
proc.kill()
proc.join()
import time
deadline = time.monotonic() + 30.0
for proc in procs:
timeout = max(0.0, deadline - time.monotonic())
proc.join(timeout=timeout)
if proc.is_alive():
proc.kill()
proc.join()
References
  1. When joining multiple background threads during cleanup, use a shared timeout/deadline pattern (e.g., calculating remaining time based on a monotonic clock) instead of a fixed timeout per thread, to prevent the cleanup block from blocking for an excessively long time if multiple threads hang.

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