Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 5e107fc

Browse files
committed
got unit tests working
1 parent d2175f1 commit 5e107fc

5 files changed

Lines changed: 42 additions & 45 deletions

File tree

google/cloud/bigtable/data/_async/client.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -238,19 +238,15 @@ def __init__(
238238
stacklevel=2,
239239
)
240240

241-
def _build_grpc_channel(self, *args, **kwargs):
241+
def _build_grpc_channel(self, *args, **kwargs) -> _AsyncReplaceableChannel:
242242
if self._emulator_host is not None:
243243
# emulators use insecure channel
244-
return insecure_channel(self._emulator_host)
245-
create_channel_fn = partial(TransportType.create_channel, *args, **kwargs)
246-
# interceptors are handled differently between async and sync, because of differences in the grpc and gapic layers
244+
create_channel_fn = partial(insecure_channel, self._emulator_host)
245+
else:
246+
create_channel_fn = partial(TransportType.create_channel, *args, **kwargs)
247247
if CrossSync.is_async:
248-
# for async, add interceptors to the creation function
249-
create_channel_fn = partial(create_channel_fn, interceptors=[_LoggingClientAIOInterceptor()])
250248
return _AsyncReplaceableChannel(create_channel_fn)
251249
else:
252-
# for sync, chain interceptors using grpc.channel.intercept
253-
# LoggingClientInterceptor not needed, since it is chained in the gapic layer
254250
return TransportType.create_channel(*args, **kwargs)
255251

256252

@@ -377,13 +373,18 @@ async def _manage_channel(
377373
grace_period: time to allow previous channel to serve existing
378374
requests before closing, in seconds
379375
"""
376+
channel = self.transport.grpc_channel
377+
if not isinstance(self.transport.grpc_channel, _AsyncReplaceableChannel):
378+
warnings.warn("Channel does not support auto-refresh.")
379+
return
380+
channel: _AsyncReplaceableChannel = self.transport.grpc_channel
380381
first_refresh = self._channel_init_time + random.uniform(
381382
refresh_interval_min, refresh_interval_max
382383
)
383384
next_sleep = max(first_refresh - time.monotonic(), 0)
384385
if next_sleep > 0:
385386
# warm the current channel immediately
386-
await self._ping_and_warm_instances(channel=self.transport.grpc_channel)
387+
await self._ping_and_warm_instances(channel=channel)
387388
# continuously refresh the channel every `refresh_interval` seconds
388389
while not self._is_closed.is_set():
389390
await CrossSync.event_wait(
@@ -396,10 +397,10 @@ async def _manage_channel(
396397
break
397398
start_timestamp = time.monotonic()
398399
# prepare new channel for use
399-
new_channel = self.transport.grpc_channel.create_channel()
400-
await self._ping_and_warm_instances(channel=new_channel)
400+
new_sub_channel = channel.create_channel()
401+
await self._ping_and_warm_instances(channel=new_sub_channel)
401402
# cycle channel out of use, with long grace window before closure
402-
await self.transport.grpc_channel.replace_channel(new_channel, grace_period)
403+
await channel.replace_wrapped_channel(new_sub_channel, grace_period)
403404
# subtract the time spent waiting for the channel to be replaced
404405
next_refresh = random.uniform(refresh_interval_min, refresh_interval_max)
405406
next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0)

google/cloud/bigtable/data/_async/replaceable_channel.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,19 @@ def get_state(self, try_to_connect: bool = False) -> grpc.ChannelConnectivity:
6262
async def wait_for_state_change(self, last_observed_state):
6363
return await self._channel.wait_for_state_change(last_observed_state)
6464

65-
@property
66-
def wrapped_channel(self):
67-
return self._channel
68-
6965
def create_channel(self) -> aio.Channel:
7066
return self._channel_fn()
7167

72-
async def replace_channel(self, new_channel: aio.Channel, grace_period: float | None) -> aio.Channel:
68+
async def replace_wrapped_channel(self, new_channel: aio.Channel, grace_period: float | None, copy_async_interceptors: bool=True) -> aio.Channel:
7369
old_channel = self._channel
70+
if CrossSync.is_async and copy_async_interceptors:
71+
# copy over interceptors
72+
# this is needed because of how gapic attaches the LoggingClientAIOInterceptor
73+
# sync channels add interceptors by wrapping, so this step isn't needed
74+
new_channel._unary_unary_interceptors = old_channel._unary_unary_interceptors
75+
new_channel._unary_stream_interceptors = old_channel._unary_stream_interceptors
76+
new_channel._stream_unary_interceptors = old_channel._stream_unary_interceptors
77+
new_channel._stream_stream_interceptors = old_channel._stream_stream_interceptors
7478
self._channel = new_channel
7579
# give old_channel a chance to complete existing rpcs
7680
if CrossSync.is_async:
@@ -81,7 +85,5 @@ async def replace_channel(self, new_channel: aio.Channel, grace_period: float |
8185
old_channel.close() # type: ignore
8286
return old_channel
8387

84-
@property
85-
def _unary_unary_interceptors(self):
86-
# return empty list for compatibility with gapic layer
87-
return []
88+
def __getattr__(self, name):
89+
return getattr(self._channel, name)

google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ def __init__(
290290
always_use_jwt_access=always_use_jwt_access,
291291
api_audience=api_audience,
292292
)
293-
294293
if not self._grpc_channel:
295294
# initialize with the provided callable or the default channel
296295
channel_init = channel or type(self).create_channel

tests/system/data/test_system_async.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,14 @@ async def test_channel_refresh(self, table_id, instance_id, temp_rows):
252252
await CrossSync.yield_to_event_loop()
253253
async with client.get_table(instance_id, table_id) as table:
254254
rows = await table.read_rows({})
255-
first_channel = client.transport.grpc_channel
255+
channel_wrapper = client.transport.grpc_channel
256+
first_channel = client.transport.grpc_channel._channel
256257
assert len(rows) == 2
257258
await CrossSync.sleep(2)
258259
rows_after_refresh = await table.read_rows({})
259260
assert len(rows_after_refresh) == 2
260-
assert client.transport.grpc_channel is not first_channel
261-
print(table)
261+
assert client.transport.grpc_channel is channel_wrapper
262+
assert client.transport.grpc_channel._channel is not first_channel
262263
finally:
263264
await client.close()
264265

tests/unit/data/_async/test_client.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ async def test__start_background_channel_refresh_task_exists(self):
223223
@CrossSync.pytest
224224
async def test__start_background_channel_refresh(self):
225225
# should create background tasks for each channel
226-
client = self._make_client(project="project-id")
226+
client = self._make_client(project="project-id", use_emulator=False)
227227
with mock.patch.object(
228228
client, "_ping_and_warm_instances", CrossSync.Mock()
229229
) as ping_and_warm:
@@ -366,7 +366,7 @@ async def test__manage_channel_first_sleep(
366366
with mock.patch.object(CrossSync, "event_wait") as sleep:
367367
sleep.side_effect = asyncio.CancelledError
368368
try:
369-
client = self._make_client(project="project-id")
369+
client = self._make_client(project="project-id", use_emulator=False)
370370
client._channel_init_time = -wait_time
371371
await client._manage_channel(refresh_interval, refresh_interval)
372372
except asyncio.CancelledError:
@@ -395,32 +395,29 @@ async def test__manage_channel_ping_and_warm(self):
395395
_LoggingClientInterceptor as Interceptor,
396396
)
397397

398-
client_mock = mock.Mock()
399-
client_mock.transport._interceptor = Interceptor()
400-
client_mock._is_closed.is_set.return_value = False
401-
client_mock._channel_init_time = time.monotonic()
402-
orig_channel = client_mock.transport.grpc_channel
398+
client = self._make_client(project="project-id", use_emulator=True)
399+
orig_channel = client.transport.grpc_channel
403400
# should ping an warm all new channels, and old channels if sleeping
404401
sleep_tuple = (
405402
(asyncio, "sleep") if CrossSync.is_async else (threading.Event, "wait")
406403
)
407-
with mock.patch.object(*sleep_tuple):
408-
# stop process after close is called
409-
orig_channel.close.side_effect = asyncio.CancelledError
410-
ping_and_warm = client_mock._ping_and_warm_instances = CrossSync.Mock()
404+
with mock.patch.object(*sleep_tuple) as sleep_mock:
405+
# stop process after loop
406+
sleep_mock.side_effect = [None, asyncio.CancelledError]
407+
ping_and_warm = client._ping_and_warm_instances = CrossSync.Mock()
411408
# should ping and warm old channel then new if sleep > 0
412409
try:
413-
await self._get_target_class()._manage_channel(client_mock, 10)
410+
await client._manage_channel(10)
414411
except asyncio.CancelledError:
415412
pass
416413
# should have called at loop start, and after replacement
417414
assert ping_and_warm.call_count == 2
418415
# should have replaced channel once
419-
assert client_mock.transport._grpc_channel != orig_channel
416+
assert client.transport.grpc_channel._channel != orig_channel
420417
# make sure new and old channels were warmed
421418
called_with = [call[1]["channel"] for call in ping_and_warm.call_args_list]
422419
assert orig_channel in called_with
423-
assert client_mock.transport.grpc_channel in called_with
420+
assert client.transport.grpc_channel._channel in called_with
424421

425422
@CrossSync.pytest
426423
@pytest.mark.parametrize(
@@ -438,8 +435,6 @@ async def test__manage_channel_sleeps(
438435
import time
439436
import random
440437

441-
channel = mock.Mock()
442-
channel.close = CrossSync.Mock()
443438
with mock.patch.object(random, "uniform") as uniform:
444439
uniform.side_effect = lambda min_, max_: min_
445440
with mock.patch.object(time, "time") as time_mock:
@@ -448,8 +443,7 @@ async def test__manage_channel_sleeps(
448443
sleep.side_effect = [None for i in range(num_cycles - 1)] + [
449444
asyncio.CancelledError
450445
]
451-
client = self._make_client(project="project-id")
452-
client.transport._grpc_channel = channel
446+
client = self._make_client(project="project-id", use_emulator=True)
453447
with mock.patch.object(
454448
client.transport, "create_channel", CrossSync.Mock
455449
):
@@ -478,7 +472,7 @@ async def test__manage_channel_random(self):
478472
uniform.return_value = 0
479473
try:
480474
uniform.side_effect = asyncio.CancelledError
481-
client = self._make_client(project="project-id")
475+
client = self._make_client(project="project-id", use_emulator=False)
482476
except asyncio.CancelledError:
483477
uniform.side_effect = None
484478
uniform.reset_mock()
@@ -512,7 +506,7 @@ async def test__manage_channel_refresh(self, num_cycles):
512506
CrossSync.grpc_helpers, "create_channel"
513507
) as create_channel:
514508
create_channel.return_value = new_channel
515-
client = self._make_client(project="project-id")
509+
client = self._make_client(project="project-id", use_emulator=False)
516510
create_channel.reset_mock()
517511
try:
518512
await client._manage_channel(

0 commit comments

Comments
 (0)