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

Commit b81a9be

Browse files
committed
added crosssync, moved close logic back to client
1 parent e71b1d5 commit b81a9be

2 files changed

Lines changed: 65 additions & 47 deletions

File tree

google/cloud/bigtable/data/_async/replaceable_channel.py renamed to google/cloud/bigtable/data/_async/_replaceable_channel.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,80 +16,98 @@
1616

1717
from typing import Callable
1818

19-
import grpc # type: ignore
20-
from grpc.experimental import aio # type: ignore
21-
2219
from google.cloud.bigtable.data._cross_sync import CrossSync
2320

21+
from grpc import ChannelConnectivity
22+
23+
if CrossSync.is_async:
24+
from grpc.aio import Call
25+
from grpc.aio import Channel
26+
from grpc.aio import UnaryUnaryMultiCallable
27+
from grpc.aio import UnaryStreamMultiCallable
28+
from grpc.aio import StreamUnaryMultiCallable
29+
from grpc.aio import StreamStreamMultiCallable
30+
else:
31+
from grpc import Call
32+
from grpc import Channel
33+
from grpc import UnaryUnaryMultiCallable
34+
from grpc import UnaryStreamMultiCallable
35+
from grpc import StreamUnaryMultiCallable
36+
from grpc import StreamStreamMultiCallable
37+
38+
__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen._replaceable_channel"
39+
40+
@CrossSync.convert_class
2441
class _WrappedMultiCallable:
2542
"""
2643
Wrapper class that implements the grpc MultiCallable interface.
2744
Allows generic functions that return calls to pass checks for
2845
MultiCallable objects.
2946
"""
3047

31-
def __init__(self, call_factory: Callable[..., aio.Call]):
48+
def __init__(self, call_factory: Callable[..., Call]):
3249
self._call_factory = call_factory
3350

34-
def __call__(self, *args, **kwargs) -> aio.Call:
51+
def __call__(self, *args, **kwargs) -> Call:
3552
return self._call_factory(*args, **kwargs)
3653

3754

3855
class WrappedUnaryUnaryMultiCallable(
39-
_WrappedMultiCallable, aio.UnaryUnaryMultiCallable
56+
_WrappedMultiCallable, UnaryUnaryMultiCallable
4057
):
4158
pass
4259

4360

4461
class WrappedUnaryStreamMultiCallable(
45-
_WrappedMultiCallable, aio.UnaryStreamMultiCallable
62+
_WrappedMultiCallable, UnaryStreamMultiCallable
4663
):
4764
pass
4865

4966

5067
class WrappedStreamUnaryMultiCallable(
51-
_WrappedMultiCallable, aio.StreamUnaryMultiCallable
68+
_WrappedMultiCallable, StreamUnaryMultiCallable
5269
):
5370
pass
5471

5572

5673
class WrappedStreamStreamMultiCallable(
57-
_WrappedMultiCallable, aio.StreamStreamMultiCallable
74+
_WrappedMultiCallable, StreamStreamMultiCallable
5875
):
5976
pass
6077

6178

62-
class _AsyncWrappedChannel(aio.Channel):
79+
@CrossSync.convert_class(sync_name="_WrappedChannel", rm_aio=True)
80+
class _AsyncWrappedChannel(Channel):
6381
"""
6482
A wrapper around a gRPC channel. All methods are passed
6583
through to the underlying channel.
6684
"""
6785

68-
def __init__(self, channel: aio.Channel):
86+
def __init__(self, channel: Channel):
6987
self._channel = channel
7088

71-
def unary_unary(self, *args, **kwargs) -> grpc.aio.UnaryUnaryMultiCallable:
89+
def unary_unary(self, *args, **kwargs) -> UnaryUnaryMultiCallable:
7290
return WrappedUnaryUnaryMultiCallable(
7391
lambda *call_args, **call_kwargs: self._channel.unary_unary(
7492
*args, **kwargs
7593
)(*call_args, **call_kwargs)
7694
)
7795

78-
def unary_stream(self, *args, **kwargs) -> grpc.aio.UnaryStreamMultiCallable:
96+
def unary_stream(self, *args, **kwargs) -> UnaryStreamMultiCallable:
7997
return WrappedUnaryStreamMultiCallable(
8098
lambda *call_args, **call_kwargs: self._channel.unary_stream(
8199
*args, **kwargs
82100
)(*call_args, **call_kwargs)
83101
)
84102

85-
def stream_unary(self, *args, **kwargs) -> grpc.aio.StreamUnaryMultiCallable:
103+
def stream_unary(self, *args, **kwargs) -> StreamUnaryMultiCallable:
86104
return WrappedStreamUnaryMultiCallable(
87105
lambda *call_args, **call_kwargs: self._channel.stream_unary(
88106
*args, **kwargs
89107
)(*call_args, **call_kwargs)
90108
)
91109

92-
def stream_stream(self, *args, **kwargs) -> grpc.aio.StreamStreamMultiCallable:
110+
def stream_stream(self, *args, **kwargs) -> StreamStreamMultiCallable:
93111
return WrappedStreamStreamMultiCallable(
94112
lambda *call_args, **call_kwargs: self._channel.stream_stream(
95113
*args, **kwargs
@@ -102,14 +120,16 @@ async def close(self, grace=None):
102120
async def channel_ready(self):
103121
return await self._channel.channel_ready()
104122

123+
@CrossSync.convert(sync_name="__enter__", replace_symbols={"__aenter__": "__enter__"})
105124
async def __aenter__(self):
106125
await self._channel.__aenter__()
107126
return self
108127

128+
@CrossSync.convert(sync_name="__exit__", replace_symbols={"__aexit__": "__exit__"})
109129
async def __aexit__(self, exc_type, exc_val, exc_tb):
110130
return await self._channel.__aexit__(exc_type, exc_val, exc_tb)
111131

112-
def get_state(self, try_to_connect: bool = False) -> grpc.ChannelConnectivity:
132+
def get_state(self, try_to_connect: bool = False) -> ChannelConnectivity:
113133
return self._channel.get_state(try_to_connect=try_to_connect)
114134

115135
async def wait_for_state_change(self, last_observed_state):
@@ -118,32 +138,26 @@ async def wait_for_state_change(self, last_observed_state):
118138
def __getattr__(self, name):
119139
return getattr(self._channel, name)
120140

121-
141+
@CrossSync.convert_class(sync_name="_ReplaceableChannel", replace_symbols={"_AsyncWrappedChannel": "_WrappedChannel"})
122142
class _AsyncReplaceableChannel(_AsyncWrappedChannel):
123143

124-
def __init__(self, channel_fn: Callable[[], aio.Channel]):
144+
def __init__(self, channel_fn: Callable[[], Channel]):
125145
self._channel_fn = channel_fn
126146
self._channel = channel_fn()
127147

128-
def create_channel(self) -> aio.Channel:
129-
return self._channel_fn()
130-
131-
async def replace_wrapped_channel(self, new_channel: aio.Channel, grace_period: float | None, copy_async_interceptors: bool=True) -> aio.Channel:
132-
old_channel = self._channel
133-
if CrossSync.is_async and copy_async_interceptors:
148+
def create_channel(self) -> Channel:
149+
new_channel = self._channel_fn()
150+
if CrossSync.is_async:
134151
# copy over interceptors
135152
# this is needed because of how gapic attaches the LoggingClientAIOInterceptor
136153
# sync channels add interceptors by wrapping, so this step isn't needed
137-
new_channel._unary_unary_interceptors = old_channel._unary_unary_interceptors
138-
new_channel._unary_stream_interceptors = old_channel._unary_stream_interceptors
139-
new_channel._stream_unary_interceptors = old_channel._stream_unary_interceptors
140-
new_channel._stream_stream_interceptors = old_channel._stream_stream_interceptors
154+
new_channel._unary_unary_interceptors = self._channel._unary_unary_interceptors
155+
new_channel._unary_stream_interceptors = self._channel._unary_stream_interceptors
156+
new_channel._stream_unary_interceptors = self._channel._stream_unary_interceptors
157+
new_channel._stream_stream_interceptors = self._channel._stream_stream_interceptors
158+
return new_channel
159+
160+
def replace_wrapped_channel(self, new_channel: Channel) -> Channel:
161+
old_channel = self._channel
141162
self._channel = new_channel
142-
# give old_channel a chance to complete existing rpcs
143-
if CrossSync.is_async:
144-
await old_channel.close(grace_period)
145-
else:
146-
if grace_period:
147-
self._is_closed.wait(grace_period) # type: ignore
148-
old_channel.close() # type: ignore
149163
return old_channel

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,14 @@
9696
_LoggingClientAIOInterceptor
9797
)
9898
from google.cloud.bigtable.data._async.mutations_batcher import _MB_SIZE
99-
from google.cloud.bigtable.data._async.replaceable_channel import _AsyncReplaceableChannel
99+
from google.cloud.bigtable.data._async._replaceable_channel import _AsyncReplaceableChannel
100100
else:
101101
from typing import Iterable # noqa: F401
102102
from grpc import insecure_channel
103103
from grpc import intercept_channel
104104
from google.cloud.bigtable_v2.services.bigtable.transports import BigtableGrpcTransport as TransportType # type: ignore
105105
from google.cloud.bigtable.data._sync_autogen.mutations_batcher import _MB_SIZE
106+
from google.cloud.bigtable.data._async._replaceable_channel import _ReplaceableChannel
106107

107108

108109
if TYPE_CHECKING:
@@ -238,17 +239,14 @@ def __init__(
238239
stacklevel=2,
239240
)
240241

242+
@CrossSync.convert(replace_symbols={"_AsyncReplaceableChannel": "_ReplaceableChannel"})
241243
def _build_grpc_channel(self, *args, **kwargs) -> _AsyncReplaceableChannel:
242244
if self._emulator_host is not None:
243245
# emulators use insecure channel
244246
create_channel_fn = partial(insecure_channel, self._emulator_host)
245247
else:
246248
create_channel_fn = partial(TransportType.create_channel, *args, **kwargs)
247-
if CrossSync.is_async:
248-
return _AsyncReplaceableChannel(create_channel_fn)
249-
else:
250-
return TransportType.create_channel(*args, **kwargs)
251-
249+
return _AsyncReplaceableChannel(create_channel_fn)
252250

253251

254252
@staticmethod
@@ -373,18 +371,17 @@ async def _manage_channel(
373371
grace_period: time to allow previous channel to serve existing
374372
requests before closing, in seconds
375373
"""
376-
channel = self.transport.grpc_channel
377374
if not isinstance(self.transport.grpc_channel, _AsyncReplaceableChannel):
378375
warnings.warn("Channel does not support auto-refresh.")
379376
return
380-
channel: _AsyncReplaceableChannel = self.transport.grpc_channel
377+
super_channel: _AsyncReplaceableChannel = self.transport.grpc_channel
381378
first_refresh = self._channel_init_time + random.uniform(
382379
refresh_interval_min, refresh_interval_max
383380
)
384381
next_sleep = max(first_refresh - time.monotonic(), 0)
385382
if next_sleep > 0:
386383
# warm the current channel immediately
387-
await self._ping_and_warm_instances(channel=channel)
384+
await self._ping_and_warm_instances(channel=super_channel)
388385
# continuously refresh the channel every `refresh_interval` seconds
389386
while not self._is_closed.is_set():
390387
await CrossSync.event_wait(
@@ -397,10 +394,17 @@ async def _manage_channel(
397394
break
398395
start_timestamp = time.monotonic()
399396
# prepare new channel for use
400-
new_sub_channel = channel.create_channel()
401-
await self._ping_and_warm_instances(channel=new_sub_channel)
397+
new_channel = super_channel.create_channel()
398+
await self._ping_and_warm_instances(channel=new_channel)
402399
# cycle channel out of use, with long grace window before closure
403-
await channel.replace_wrapped_channel(new_sub_channel, grace_period)
400+
old_channel = super_channel.replace_wrapped_channel(new_channel, grace_period)
401+
# give old_channel a chance to complete existing rpcs
402+
if CrossSync.is_async:
403+
await old_channel.close(grace_period)
404+
else:
405+
if grace_period:
406+
self._is_closed.wait(grace_period) # type: ignore
407+
old_channel.close() # type: ignore
404408
# subtract the time spent waiting for the channel to be replaced
405409
next_refresh = random.uniform(refresh_interval_min, refresh_interval_max)
406410
next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0)

0 commit comments

Comments
 (0)