Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2026 Google LLC
#
# Licensed 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.

import time

import pytest

from google.cloud.bigtable.data._accelerator._daemon import AcceleratorDaemon

from . import _harness

_FLAGS = ["--project", "p", "--instance", "i", "--app-profile", "ap"]


def test_nonexecutable_binary_raises_on_start(tmp_bin_dir, monkeypatch):
monkeypatch.setenv("BIGTABLE_ACCELERATOR_STARTUP_TIMEOUT", "2.0")
path = _harness.nonexecutable_binary(tmp_bin_dir)
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
with pytest.raises(RuntimeError, match="spawn"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
_harness.force_kill_daemon(daemon)
Comment on lines +29 to +32

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

If an unexpected exception is raised or if the assertion fails, _harness.force_kill_daemon(daemon) will not be executed, potentially leaking the daemon process. Wrapping the startup and assertion in a try...finally block ensures that the daemon is always cleaned up properly, preventing resource leaks in the test environment.

Suggested change
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
with pytest.raises(RuntimeError, match="spawn"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
_harness.force_kill_daemon(daemon)
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
try:
with pytest.raises(RuntimeError, match="spawn"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
finally:
_harness.force_kill_daemon(daemon)



def test_exit_during_startup_raises_promptly(tmp_bin_dir, monkeypatch):
monkeypatch.setenv("BIGTABLE_ACCELERATOR_STARTUP_TIMEOUT", "5.0")
path = _harness.immediately_exiting_binary(tmp_bin_dir, exit_code=3)
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
started = time.monotonic()
with pytest.raises(RuntimeError, match="exited during startup"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
elapsed = time.monotonic() - started
assert elapsed < 3.0, f"exit-during-startup took {elapsed:.2f}s; not prompt"
_harness.force_kill_daemon(daemon)
Comment on lines +38 to +44

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

Similar to the previous test, if any assertion fails or an unexpected exception occurs, the daemon process will not be killed. Wrapping this block in a try...finally ensures reliable cleanup of the daemon process.

Suggested change
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
started = time.monotonic()
with pytest.raises(RuntimeError, match="exited during startup"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
elapsed = time.monotonic() - started
assert elapsed < 3.0, f"exit-during-startup took {elapsed:.2f}s; not prompt"
_harness.force_kill_daemon(daemon)
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
started = time.monotonic()
try:
with pytest.raises(RuntimeError, match="exited during startup"):
_harness.call_with_timeout(daemon.start, timeout=5.0)
elapsed = time.monotonic() - started
assert elapsed < 3.0, f"exit-during-startup took {elapsed:.2f}s; not prompt"
finally:
_harness.force_kill_daemon(daemon)



def test_never_binds_honors_startup_timeout(tmp_bin_dir, monkeypatch):
monkeypatch.setenv("BIGTABLE_ACCELERATOR_STARTUP_TIMEOUT", "1.0")
path = _harness.never_binds_binary(tmp_bin_dir)
daemon = AcceleratorDaemon(_FLAGS, binary_path=path)
started = time.monotonic()
try:
with pytest.raises(RuntimeError, match="did not become ready"):
_harness.call_with_timeout(daemon.start, timeout=4.0)
elapsed = time.monotonic() - started
assert elapsed < 3.0, f"never-binds took {elapsed:.2f}s; not honoring timeout"
finally:
_harness.force_kill_daemon(daemon)
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright 2026 Google LLC
#
# Licensed 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.

import uuid

import pytest
from google.api_core import exceptions as core_exceptions

from google.cloud.bigtable.data._accelerator import _daemon
from google.cloud.bigtable.data._cross_sync import CrossSync
from google.cloud.bigtable.data.mutations import DeleteAllFromRow, SetCell

from . import _harness

if CrossSync.is_async:
from ._base_async import AcceleratorTestBaseAsync as AcceleratorTestBase
else:
from ._base_autogen import AcceleratorTestBase

__CROSS_SYNC_OUTPUT__ = "tests.system.data.accelerator.test_error_handling_autogen"


@CrossSync.convert_class(sync_name="TestErrorHandling")
class TestErrorHandlingAsync(AcceleratorTestBase):
@CrossSync.convert
async def _assert_round_trip(self, table):
key = f"errtest-{uuid.uuid4().hex}".encode()
ts = 1000 * _harness.MS
await table.mutate_row(
key, SetCell(_harness.TEST_FAMILY, b"q", b"ok", timestamp_micros=ts)
)
row = await table.read_row(key)
assert row is not None and row.cells[0].value == b"ok"
return key

@CrossSync.pytest
async def test_start_failure_falls_back_to_native(
self, tmp_bin_dir, instance_id, table_id, monkeypatch
):
path = _harness.immediately_exiting_binary(tmp_bin_dir, monkeypatch=monkeypatch)
monkeypatch.setenv(_harness.BIN_ENV_VAR, path)
monkeypatch.setattr(_daemon, "_resolve_binary_path", lambda: path)

async with self._make_client(use_accelerator=True) as explicit_client:
with pytest.raises(RuntimeError, match="exited during startup"):
explicit_client.get_table(instance_id, table_id)

async with self._make_client() as client:
with pytest.warns(RuntimeWarning, match="Failed to start"):
table = client.get_table(instance_id, table_id)
async with table:
assert table._accelerator_client is None
assert table._accelerator_daemon is None
key = await self._assert_round_trip(table)
await table.mutate_row(key, DeleteAllFromRow())

@CrossSync.pytest
async def test_daemon_killed_midflight_falls_back_to_native(
self, instance_id, table_id, native_table
):
async with self._make_client(use_accelerator=True) as client:
async with client.get_table(instance_id, table_id) as table:
self.assert_accelerator_active(table)
key = await self._assert_round_trip(table)
assert not table._accelerator_breaker.bypass()

pid = _harness.daemon_pid(table)
assert pid is not None and _harness.pid_alive(pid)
_harness.force_kill_daemon(table._accelerator_daemon)
assert not table._accelerator_daemon.is_running

row = await table.read_row(key, operation_timeout=30)
assert row is not None and row.cells[0].value == b"ok"

assert table._accelerator_breaker.bypass()
assert table._accelerator_client is not None
assert table._use_accelerator("read_row") is False
assert table._use_accelerator("mutate_row") is False

after_ts = 2000 * _harness.MS
cell = SetCell(_harness.TEST_FAMILY, b"q", b"after", after_ts)
await table.mutate_row(key, cell, operation_timeout=30)
roundtrip = await table.read_row(key)
assert any(c.value == b"after" for c in roundtrip.cells)

assert (await native_table.read_row(key)) is not None
await native_table.mutate_row(key, DeleteAllFromRow())

@CrossSync.pytest
async def test_operation_timeout_enforced_as_deadline(
self, accel_table, native_table
):
key = f"errtest-{uuid.uuid4().hex}".encode()
with pytest.raises(core_exceptions.DeadlineExceeded):
await accel_table.read_row(key, operation_timeout=0.001)
with pytest.raises(core_exceptions.DeadlineExceeded):
await native_table.read_row(key, operation_timeout=0.001)
assert not accel_table._accelerator_breaker.bypass()
assert accel_table._accelerator_client is not None

@CrossSync.pytest
async def test_backend_error_type_matches_native(self, accel_table, native_table):
key = f"errtest-{uuid.uuid4().hex}".encode()
bad = SetCell("no-such-family", b"q", b"v", timestamp_micros=1000 * _harness.MS)
with pytest.raises(Exception) as accel_ei:
await accel_table.mutate_row(key, bad)
with pytest.raises(Exception) as native_ei:
await native_table.mutate_row(key, bad)
assert type(accel_ei.value) is type(native_ei.value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2026 Google LLC
#
# Licensed 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.


# This file is automatically generated by CrossSync. Do not edit manually.

import uuid
import pytest
from google.api_core import exceptions as core_exceptions
from google.cloud.bigtable.data._accelerator import _daemon
from google.cloud.bigtable.data._cross_sync import CrossSync
from google.cloud.bigtable.data.mutations import DeleteAllFromRow, SetCell
from . import _harness
from ._base_autogen import AcceleratorTestBase

class TestErrorHandling(AcceleratorTestBase):

def _assert_round_trip(self, table):
key = f'errtest-{uuid.uuid4().hex}'.encode()
ts = 1000 * _harness.MS
table.mutate_row(key, SetCell(_harness.TEST_FAMILY, b'q', b'ok', timestamp_micros=ts))
row = table.read_row(key)
assert row is not None and row.cells[0].value == b'ok'
return key

def test_start_failure_falls_back_to_native(self, tmp_bin_dir, instance_id, table_id, monkeypatch):
path = _harness.immediately_exiting_binary(tmp_bin_dir, monkeypatch=monkeypatch)
monkeypatch.setenv(_harness.BIN_ENV_VAR, path)
monkeypatch.setattr(_daemon, '_resolve_binary_path', lambda: path)
with self._make_client(use_accelerator=True) as explicit_client:
with pytest.raises(RuntimeError, match='exited during startup'):
explicit_client.get_table(instance_id, table_id)
with self._make_client() as client:
with pytest.warns(RuntimeWarning, match='Failed to start'):
table = client.get_table(instance_id, table_id)
with table:
assert table._accelerator_client is None
assert table._accelerator_daemon is None
key = self._assert_round_trip(table)
table.mutate_row(key, DeleteAllFromRow())

def test_daemon_killed_midflight_falls_back_to_native(self, instance_id, table_id, native_table):
with self._make_client(use_accelerator=True) as client:
with client.get_table(instance_id, table_id) as table:
self.assert_accelerator_active(table)
key = self._assert_round_trip(table)
assert not table._accelerator_breaker.bypass()
pid = _harness.daemon_pid(table)
assert pid is not None and _harness.pid_alive(pid)
_harness.force_kill_daemon(table._accelerator_daemon)
assert not table._accelerator_daemon.is_running
row = table.read_row(key, operation_timeout=30)
assert row is not None and row.cells[0].value == b'ok'
assert table._accelerator_breaker.bypass()
assert table._accelerator_client is not None
assert table._use_accelerator('read_row') is False
assert table._use_accelerator('mutate_row') is False
after_ts = 2000 * _harness.MS
cell = SetCell(_harness.TEST_FAMILY, b'q', b'after', after_ts)
table.mutate_row(key, cell, operation_timeout=30)
roundtrip = table.read_row(key)
assert any((c.value == b'after' for c in roundtrip.cells))
assert native_table.read_row(key) is not None
native_table.mutate_row(key, DeleteAllFromRow())

def test_operation_timeout_enforced_as_deadline(self, accel_table, native_table):
key = f'errtest-{uuid.uuid4().hex}'.encode()
with pytest.raises(core_exceptions.DeadlineExceeded):
accel_table.read_row(key, operation_timeout=0.001)
with pytest.raises(core_exceptions.DeadlineExceeded):
native_table.read_row(key, operation_timeout=0.001)
assert not accel_table._accelerator_breaker.bypass()
assert accel_table._accelerator_client is not None

def test_backend_error_type_matches_native(self, accel_table, native_table):
key = f'errtest-{uuid.uuid4().hex}'.encode()
bad = SetCell('no-such-family', b'q', b'v', timestamp_micros=1000 * _harness.MS)
with pytest.raises(Exception) as accel_ei:
accel_table.mutate_row(key, bad)
with pytest.raises(Exception) as native_ei:
native_table.mutate_row(key, bad)
assert type(accel_ei.value) is type(native_ei.value)
Loading