9292 from google .cloud .bigtable_v2 .services .bigtable .transports import (
9393 BigtableGrpcAsyncIOTransport as TransportType ,
9494 )
95+ from google .cloud .bigtable_v2 .services .bigtable import (
96+ BigtableAsyncClient as GapicClient ,
97+ )
9598 from google .cloud .bigtable .data ._async .mutations_batcher import _MB_SIZE
99+ from google .cloud .bigtable .data ._async ._swappable_channel import (
100+ AsyncSwappableChannel ,
101+ )
96102else :
97103 from typing import Iterable # noqa: F401
98104 from grpc import insecure_channel
99- from grpc import intercept_channel
100105 from google .cloud .bigtable_v2 .services .bigtable .transports import BigtableGrpcTransport as TransportType # type: ignore
106+ from google .cloud .bigtable_v2 .services .bigtable import BigtableClient as GapicClient # type: ignore
101107 from google .cloud .bigtable .data ._sync_autogen .mutations_batcher import _MB_SIZE
108+ from google .cloud .bigtable .data ._sync_autogen ._swappable_channel import ( # noqa: F401
109+ SwappableChannel ,
110+ )
102111
103112
104113if TYPE_CHECKING :
@@ -182,7 +191,6 @@ def __init__(
182191 client_options = cast (
183192 Optional [client_options_lib .ClientOptions ], client_options
184193 )
185- custom_channel = None
186194 self ._emulator_host = os .getenv (BIGTABLE_EMULATOR )
187195 if self ._emulator_host is not None :
188196 warnings .warn (
@@ -191,24 +199,24 @@ def __init__(
191199 stacklevel = 2 ,
192200 )
193201 # use insecure channel if emulator is set
194- custom_channel = insecure_channel (self ._emulator_host )
195202 if credentials is None :
196203 credentials = google .auth .credentials .AnonymousCredentials ()
197204 if project is None :
198205 project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT
206+
199207 # initialize client
200208 ClientWithProject .__init__ (
201209 self ,
202210 credentials = credentials ,
203211 project = project ,
204212 client_options = client_options ,
205213 )
206- self ._gapic_client = CrossSync . GapicClient (
214+ self ._gapic_client = GapicClient (
207215 credentials = credentials ,
208216 client_options = client_options ,
209217 client_info = self .client_info ,
210218 transport = lambda * args , ** kwargs : TransportType (
211- * args , ** kwargs , channel = custom_channel
219+ * args , ** kwargs , channel = self . _build_grpc_channel
212220 ),
213221 )
214222 if (
@@ -234,7 +242,7 @@ def __init__(
234242 self ._instance_owners : dict [_WarmedInstanceKey , Set [int ]] = {}
235243 self ._channel_init_time = time .monotonic ()
236244 self ._channel_refresh_task : CrossSync .Task [None ] | None = None
237- self ._executor = (
245+ self ._executor : concurrent . futures . ThreadPoolExecutor | None = (
238246 concurrent .futures .ThreadPoolExecutor () if not CrossSync .is_async else None
239247 )
240248 if self ._emulator_host is None :
@@ -249,6 +257,29 @@ def __init__(
249257 stacklevel = 2 ,
250258 )
251259
260+ @CrossSync .convert (replace_symbols = {"AsyncSwappableChannel" : "SwappableChannel" })
261+ def _build_grpc_channel (self , * args , ** kwargs ) -> AsyncSwappableChannel :
262+ """
263+ This method is called by the gapic transport to create a grpc channel.
264+
265+ The init arguments passed down are captured in a partial used by AsyncSwappableChannel
266+ to create new channel instances in the future, as part of the channel refresh logic
267+
268+ Emulators always use an inseucre channel
269+
270+ Args:
271+ - *args: positional arguments passed by the gapic layer to create a new channel with
272+ - **kwargs: keyword arguments passed by the gapic layer to create a new channel with
273+ Returns:
274+ a custom wrapped swappable channel
275+ """
276+ if self ._emulator_host is not None :
277+ # emulators use insecure channel
278+ create_channel_fn = partial (insecure_channel , self ._emulator_host )
279+ else :
280+ create_channel_fn = partial (TransportType .create_channel , * args , ** kwargs )
281+ return AsyncSwappableChannel (create_channel_fn )
282+
252283 @property
253284 def universe_domain (self ) -> str :
254285 """Return the universe domain used by the client instance.
@@ -364,7 +395,12 @@ async def _ping_and_warm_instances(
364395 )
365396 return [r or None for r in result_list ]
366397
367- @CrossSync .convert
398+ def _invalidate_channel_stubs (self ):
399+ """Helper to reset the cached stubs. Needed when changing out the grpc channel"""
400+ self .transport ._stubs = {}
401+ self .transport ._prep_wrapped_messages (self .client_info )
402+
403+ @CrossSync .convert (replace_symbols = {"AsyncSwappableChannel" : "SwappableChannel" })
368404 async def _manage_channel (
369405 self ,
370406 refresh_interval_min : float = 60 * 35 ,
@@ -389,13 +425,17 @@ async def _manage_channel(
389425 grace_period: time to allow previous channel to serve existing
390426 requests before closing, in seconds
391427 """
428+ if not isinstance (self .transport .grpc_channel , AsyncSwappableChannel ):
429+ warnings .warn ("Channel does not support auto-refresh." )
430+ return
431+ super_channel : AsyncSwappableChannel = self .transport .grpc_channel
392432 first_refresh = self ._channel_init_time + random .uniform (
393433 refresh_interval_min , refresh_interval_max
394434 )
395435 next_sleep = max (first_refresh - time .monotonic (), 0 )
396436 if next_sleep > 0 :
397437 # warm the current channel immediately
398- await self ._ping_and_warm_instances (channel = self . transport . grpc_channel )
438+ await self ._ping_and_warm_instances (channel = super_channel )
399439 # continuously refresh the channel every `refresh_interval` seconds
400440 while not self ._is_closed .is_set ():
401441 await CrossSync .event_wait (
@@ -408,32 +448,19 @@ async def _manage_channel(
408448 break
409449 start_timestamp = time .monotonic ()
410450 # prepare new channel for use
411- # TODO: refactor to avoid using internal references: https://github.com/googleapis/python-bigtable/issues/1094
412- old_channel = self .transport .grpc_channel
413- new_channel = self .transport .create_channel ()
414- if CrossSync .is_async :
415- new_channel ._unary_unary_interceptors .append (
416- self .transport ._interceptor
417- )
418- else :
419- new_channel = intercept_channel (
420- new_channel , self .transport ._interceptor
421- )
451+ new_channel = super_channel .create_channel ()
422452 await self ._ping_and_warm_instances (channel = new_channel )
423453 # cycle channel out of use, with long grace window before closure
424- self .transport ._grpc_channel = new_channel
425- self .transport ._logged_channel = new_channel
426- # invalidate caches
427- self .transport ._stubs = {}
428- self .transport ._prep_wrapped_messages (self .client_info )
454+ old_channel = super_channel .swap_channel (new_channel )
455+ self ._invalidate_channel_stubs ()
429456 # give old_channel a chance to complete existing rpcs
430457 if CrossSync .is_async :
431458 await old_channel .close (grace_period )
432459 else :
433460 if grace_period :
434461 self ._is_closed .wait (grace_period ) # type: ignore
435462 old_channel .close () # type: ignore
436- # subtract thed time spent waiting for the channel to be replaced
463+ # subtract the time spent waiting for the channel to be replaced
437464 next_refresh = random .uniform (refresh_interval_min , refresh_interval_max )
438465 next_sleep = max (next_refresh - (time .monotonic () - start_timestamp ), 0 )
439466
@@ -895,24 +922,32 @@ def __init__(
895922 self .table_name = self .client ._gapic_client .table_path (
896923 self .client .project , instance_id , table_id
897924 )
898- self .app_profile_id = app_profile_id
925+ self .app_profile_id : str | None = app_profile_id
899926
900- self .default_operation_timeout = default_operation_timeout
901- self .default_attempt_timeout = default_attempt_timeout
902- self .default_read_rows_operation_timeout = default_read_rows_operation_timeout
903- self .default_read_rows_attempt_timeout = default_read_rows_attempt_timeout
904- self .default_mutate_rows_operation_timeout = (
927+ self .default_operation_timeout : float = default_operation_timeout
928+ self .default_attempt_timeout : float | None = default_attempt_timeout
929+ self .default_read_rows_operation_timeout : float = (
930+ default_read_rows_operation_timeout
931+ )
932+ self .default_read_rows_attempt_timeout : float | None = (
933+ default_read_rows_attempt_timeout
934+ )
935+ self .default_mutate_rows_operation_timeout : float = (
905936 default_mutate_rows_operation_timeout
906937 )
907- self .default_mutate_rows_attempt_timeout = default_mutate_rows_attempt_timeout
938+ self .default_mutate_rows_attempt_timeout : float | None = (
939+ default_mutate_rows_attempt_timeout
940+ )
908941
909- self .default_read_rows_retryable_errors = (
942+ self .default_read_rows_retryable_errors : Sequence [ type [ Exception ]] = (
910943 default_read_rows_retryable_errors or ()
911944 )
912- self .default_mutate_rows_retryable_errors = (
945+ self .default_mutate_rows_retryable_errors : Sequence [ type [ Exception ]] = (
913946 default_mutate_rows_retryable_errors or ()
914947 )
915- self .default_retryable_errors = default_retryable_errors or ()
948+ self .default_retryable_errors : Sequence [type [Exception ]] = (
949+ default_retryable_errors or ()
950+ )
916951
917952 try :
918953 self ._register_instance_future = CrossSync .create_task (
0 commit comments