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

Commit d2175f1

Browse files
committed
use replaceable channel wrapper
1 parent 3648d70 commit d2175f1

2 files changed

Lines changed: 113 additions & 27 deletions

File tree

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

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@
9292
from google.cloud.bigtable_v2.services.bigtable.transports import (
9393
BigtableGrpcAsyncIOTransport as TransportType,
9494
)
95+
from google.cloud.bigtable_v2.services.bigtable.transports.grpc_asyncio import (
96+
_LoggingClientAIOInterceptor
97+
)
9598
from google.cloud.bigtable.data._async.mutations_batcher import _MB_SIZE
99+
from google.cloud.bigtable.data._async.replaceable_channel import _AsyncReplaceableChannel
96100
else:
97101
from typing import Iterable # noqa: F401
98102
from grpc import insecure_channel
@@ -182,7 +186,6 @@ def __init__(
182186
client_options = cast(
183187
Optional[client_options_lib.ClientOptions], client_options
184188
)
185-
custom_channel = None
186189
self._emulator_host = os.getenv(BIGTABLE_EMULATOR)
187190
if self._emulator_host is not None:
188191
warnings.warn(
@@ -191,11 +194,11 @@ def __init__(
191194
stacklevel=2,
192195
)
193196
# use insecure channel if emulator is set
194-
custom_channel = insecure_channel(self._emulator_host)
195197
if credentials is None:
196198
credentials = google.auth.credentials.AnonymousCredentials()
197199
if project is None:
198200
project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT
201+
199202
# initialize client
200203
ClientWithProject.__init__(
201204
self,
@@ -208,7 +211,7 @@ def __init__(
208211
client_options=client_options,
209212
client_info=self.client_info,
210213
transport=lambda *args, **kwargs: TransportType(
211-
*args, **kwargs, channel=custom_channel
214+
*args, **kwargs, channel=self._build_grpc_channel
212215
),
213216
)
214217
self._is_closed = CrossSync.Event()
@@ -235,6 +238,23 @@ def __init__(
235238
stacklevel=2,
236239
)
237240

241+
def _build_grpc_channel(self, *args, **kwargs):
242+
if self._emulator_host is not None:
243+
# 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
247+
if CrossSync.is_async:
248+
# for async, add interceptors to the creation function
249+
create_channel_fn = partial(create_channel_fn, interceptors=[_LoggingClientAIOInterceptor()])
250+
return _AsyncReplaceableChannel(create_channel_fn)
251+
else:
252+
# for sync, chain interceptors using grpc.channel.intercept
253+
# LoggingClientInterceptor not needed, since it is chained in the gapic layer
254+
return TransportType.create_channel(*args, **kwargs)
255+
256+
257+
238258
@staticmethod
239259
def _client_version() -> str:
240260
"""
@@ -376,32 +396,11 @@ async def _manage_channel(
376396
break
377397
start_timestamp = time.monotonic()
378398
# prepare new channel for use
379-
# TODO: refactor to avoid using internal references: https://github.com/googleapis/python-bigtable/issues/1094
380-
old_channel = self.transport.grpc_channel
381-
new_channel = self.transport.create_channel()
382-
if CrossSync.is_async:
383-
new_channel._unary_unary_interceptors.append(
384-
self.transport._interceptor
385-
)
386-
else:
387-
new_channel = intercept_channel(
388-
new_channel, self.transport._interceptor
389-
)
399+
new_channel = self.transport.grpc_channel.create_channel()
390400
await self._ping_and_warm_instances(channel=new_channel)
391401
# cycle channel out of use, with long grace window before closure
392-
self.transport._grpc_channel = new_channel
393-
self.transport._logged_channel = new_channel
394-
# invalidate caches
395-
self.transport._stubs = {}
396-
self.transport._prep_wrapped_messages(self.client_info)
397-
# give old_channel a chance to complete existing rpcs
398-
if CrossSync.is_async:
399-
await old_channel.close(grace_period)
400-
else:
401-
if grace_period:
402-
self._is_closed.wait(grace_period) # type: ignore
403-
old_channel.close() # type: ignore
404-
# subtract thed time spent waiting for the channel to be replaced
402+
await self.transport.grpc_channel.replace_channel(new_channel, grace_period)
403+
# subtract the time spent waiting for the channel to be replaced
405404
next_refresh = random.uniform(refresh_interval_min, refresh_interval_max)
406405
next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0)
407406

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License
14+
#
15+
from __future__ import annotations
16+
17+
from typing import Callable
18+
19+
import grpc # type: ignore
20+
from grpc.experimental import aio # type: ignore
21+
22+
from google.cloud.bigtable.data._cross_sync import CrossSync
23+
24+
class _AsyncReplaceableChannel(aio.Channel):
25+
"""
26+
A wrapper around a gRPC channel. All methods are passed
27+
through to the underlying channel.
28+
"""
29+
30+
def __init__(self, channel_fn: Callable[[], aio.Channel]):
31+
self._channel_fn = channel_fn
32+
self._channel = channel_fn()
33+
34+
def unary_unary(self, *args, **kwargs):
35+
return self._channel.unary_unary(*args, **kwargs)
36+
37+
def unary_stream(self, *args, **kwargs):
38+
return self._channel.unary_stream(*args, **kwargs)
39+
40+
def stream_unary(self, *args, **kwargs):
41+
return self._channel.stream_unary(*args, **kwargs)
42+
43+
def stream_stream(self, *args, **kwargs):
44+
return self._channel.stream_stream(*args, **kwargs)
45+
46+
async def close(self, grace=None):
47+
return await self._channel.close(grace=grace)
48+
49+
async def channel_ready(self):
50+
return await self._channel.channel_ready()
51+
52+
async def __aenter__(self):
53+
await self._channel.__aenter__()
54+
return self
55+
56+
async def __aexit__(self, exc_type, exc_val, exc_tb):
57+
return await self._channel.__aexit__(exc_type, exc_val, exc_tb)
58+
59+
def get_state(self, try_to_connect: bool = False) -> grpc.ChannelConnectivity:
60+
return self._channel.get_state(try_to_connect=try_to_connect)
61+
62+
async def wait_for_state_change(self, last_observed_state):
63+
return await self._channel.wait_for_state_change(last_observed_state)
64+
65+
@property
66+
def wrapped_channel(self):
67+
return self._channel
68+
69+
def create_channel(self) -> aio.Channel:
70+
return self._channel_fn()
71+
72+
async def replace_channel(self, new_channel: aio.Channel, grace_period: float | None) -> aio.Channel:
73+
old_channel = self._channel
74+
self._channel = new_channel
75+
# give old_channel a chance to complete existing rpcs
76+
if CrossSync.is_async:
77+
await old_channel.close(grace_period)
78+
else:
79+
if grace_period:
80+
self._is_closed.wait(grace_period) # type: ignore
81+
old_channel.close() # type: ignore
82+
return old_channel
83+
84+
@property
85+
def _unary_unary_interceptors(self):
86+
# return empty list for compatibility with gapic layer
87+
return []

0 commit comments

Comments
 (0)