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

Commit 6561cfa

Browse files
feat: add basic interceptor to client (#1206)
1 parent 72dfdc4 commit 6561cfa

9 files changed

Lines changed: 522 additions & 30 deletions

File tree

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

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
cast,
2020
Any,
2121
AsyncIterable,
22+
Callable,
2223
Optional,
2324
Set,
2425
Sequence,
@@ -99,18 +100,24 @@
99100
)
100101
fromgoogle.cloud.bigtable.data._async.mutations_batcherimport_MB_SIZE
101102
fromgoogle.cloud.bigtable.data._async._swappable_channelimport (
102-
AsyncSwappableChannel,
103+
AsyncSwappableChannelasSwappableChannelType,
104+
)
105+
fromgoogle.cloud.bigtable.data._async.metrics_interceptorimport (
106+
AsyncBigtableMetricsInterceptorasMetricsInterceptorType,
103107
)
104108
else:
105109
fromtypingimportIterable# noqa: F401
106110
fromgrpcimportinsecure_channel
111+
fromgrpcimportintercept_channel
107112
fromgoogle.cloud.bigtable_v2.services.bigtable.transportsimportBigtableGrpcTransportasTransportType# type: ignore
108113
fromgoogle.cloud.bigtable_v2.services.bigtableimportBigtableClientasGapicClient# type: ignore
109114
fromgoogle.cloud.bigtable.data._sync_autogen.mutations_batcherimport_MB_SIZE
110115
fromgoogle.cloud.bigtable.data._sync_autogen._swappable_channelimport ( # noqa: F401
111-
SwappableChannel,
116+
SwappableChannelasSwappableChannelType,
117+
)
118+
fromgoogle.cloud.bigtable.data._sync_autogen.metrics_interceptorimport ( # noqa: F401
119+
BigtableMetricsInterceptorasMetricsInterceptorType,
112120
)
113-
114121

115122
ifTYPE_CHECKING:
116123
fromgoogle.cloud.bigtable.data._helpersimportRowKeySamples
@@ -205,7 +212,7 @@ def __init__(
205212
credentials=google.auth.credentials.AnonymousCredentials()
206213
ifprojectisNone:
207214
project=_DEFAULT_BIGTABLE_EMULATOR_CLIENT
208-
215+
self._metrics_interceptor=MetricsInterceptorType()
209216
# initialize client
210217
ClientWithProject.__init__(
211218
self,
@@ -259,12 +266,11 @@ def __init__(
259266
stacklevel=2,
260267
)
261268

262-
@CrossSync.convert(replace_symbols={"AsyncSwappableChannel": "SwappableChannel"})
263-
def_build_grpc_channel(self, *args, **kwargs) ->AsyncSwappableChannel:
269+
def_build_grpc_channel(self, *args, **kwargs) ->SwappableChannelType:
264270
"""
265271
This method is called by the gapic transport to create a grpc channel.
266272
267-
The init arguments passed down are captured in a partial used by AsyncSwappableChannel
273+
The init arguments passed down are captured in a partial used by SwappableChannel
268274
to create new channel instances in the future, as part of the channel refresh logic
269275
270276
Emulators always use an inseucre channel
@@ -275,12 +281,30 @@ def _build_grpc_channel(self, *args, **kwargs) -> AsyncSwappableChannel:
275281
Returns:
276282
a custom wrapped swappable channel
277283
"""
284+
create_channel_fn: Callable[[], Channel]
278285
ifself._emulator_hostisnotNone:
279-
# emulators use insecure channel
286+
# Emulators use insecure channels
280287
create_channel_fn=partial(insecure_channel, self._emulator_host)
281-
else:
288+
elifCrossSync.is_async:
289+
# For async client, use the default create_channel.
282290
create_channel_fn=partial(TransportType.create_channel, *args, **kwargs)
283-
returnAsyncSwappableChannel(create_channel_fn)
291+
else:
292+
# For sync client, wrap create_channel with interceptors.
293+
defsync_create_channel_fn():
294+
returnintercept_channel(
295+
TransportType.create_channel(*args, **kwargs),
296+
self._metrics_interceptor,
297+
)
298+
299+
create_channel_fn=sync_create_channel_fn
300+
301+
# Instantiate SwappableChannelType with the determined creation function.
302+
new_channel=SwappableChannelType(create_channel_fn)
303+
ifCrossSync.is_async:
304+
# Attach async interceptors to the channel instance itself.
305+
new_channel._unary_unary_interceptors.append(self._metrics_interceptor)
306+
new_channel._unary_stream_interceptors.append(self._metrics_interceptor)
307+
returnnew_channel
284308

285309
@property
286310
defuniverse_domain(self) ->str:
@@ -402,7 +426,7 @@ def _invalidate_channel_stubs(self):
402426
self.transport._stubs= {}
403427
self.transport._prep_wrapped_messages(self.client_info)
404428

405-
@CrossSync.convert(replace_symbols={"AsyncSwappableChannel": "SwappableChannel"})
429+
@CrossSync.convert
406430
asyncdef_manage_channel(
407431
self,
408432
refresh_interval_min: float=60*35,
@@ -427,10 +451,10 @@ async def _manage_channel(
427451
grace_period: time to allow previous channel to serve existing
428452
requests before closing, in seconds
429453
"""
430-
ifnotisinstance(self.transport.grpc_channel, AsyncSwappableChannel):
454+
ifnotisinstance(self.transport.grpc_channel, SwappableChannelType):
431455
warnings.warn("Channel does not support auto-refresh.")
432456
return
433-
super_channel: AsyncSwappableChannel=self.transport.grpc_channel
457+
super_channel: SwappableChannelType=self.transport.grpc_channel
434458
first_refresh=self._channel_init_time+random.uniform(
435459
refresh_interval_min, refresh_interval_max
436460
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2025 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+
from __future__ importannotations
15+
16+
fromgoogle.cloud.bigtable.data._cross_syncimportCrossSync
17+
18+
ifCrossSync.is_async:
19+
fromgrpc.aioimportUnaryUnaryClientInterceptor
20+
fromgrpc.aioimportUnaryStreamClientInterceptor
21+
else:
22+
fromgrpcimportUnaryUnaryClientInterceptor
23+
fromgrpcimportUnaryStreamClientInterceptor
24+
25+
26+
__CROSS_SYNC_OUTPUT__="google.cloud.bigtable.data._sync_autogen.metrics_interceptor"
27+
28+
29+
@CrossSync.convert_class(sync_name="BigtableMetricsInterceptor")
30+
classAsyncBigtableMetricsInterceptor(
31+
UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor
32+
):
33+
"""
34+
An async gRPC interceptor to add client metadata and print server metadata.
35+
"""
36+
37+
@CrossSync.convert
38+
asyncdefintercept_unary_unary(self, continuation, client_call_details, request):
39+
"""
40+
Interceptor for unary rpcs:
41+
- MutateRow
42+
- CheckAndMutateRow
43+
- ReadModifyWriteRow
44+
"""
45+
try:
46+
call=awaitcontinuation(client_call_details, request)
47+
returncall
48+
exceptExceptionasrpc_error:
49+
raiserpc_error
50+
51+
@CrossSync.convert
52+
asyncdefintercept_unary_stream(self, continuation, client_call_details, request):
53+
"""
54+
Interceptor for streaming rpcs:
55+
- ReadRows
56+
- MutateRows
57+
- SampleRowKeys
58+
"""
59+
try:
60+
returnself._streaming_generator_wrapper(
61+
awaitcontinuation(client_call_details, request)
62+
)
63+
exceptExceptionasrpc_error:
64+
# handle errors while intializing stream
65+
raiserpc_error
66+
67+
@staticmethod
68+
@CrossSync.convert
69+
asyncdef_streaming_generator_wrapper(call):
70+
"""
71+
Wrapped generator to be returned by intercept_unary_stream.
72+
"""
73+
try:
74+
asyncforresponseincall:
75+
yieldresponse
76+
exceptExceptionase:
77+
# handle errors while processing stream
78+
raisee

‎google/cloud/bigtable/data/_sync_autogen/client.py‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# This file is automatically generated by CrossSync. Do not edit manually.
1818

1919
from __future__ importannotations
20-
fromtypingimportcast, Any, Optional, Set, Sequence, TYPE_CHECKING
20+
fromtypingimportcast, Any, Callable, Optional, Set, Sequence, TYPE_CHECKING
2121
importabc
2222
importtime
2323
importwarnings
@@ -77,12 +77,18 @@
7777
fromgoogle.cloud.bigtable.data._cross_syncimportCrossSync
7878
fromtypingimportIterable
7979
fromgrpcimportinsecure_channel
80+
fromgrpcimportintercept_channel
8081
fromgoogle.cloud.bigtable_v2.services.bigtable.transportsimport (
8182
BigtableGrpcTransportasTransportType,
8283
)
8384
fromgoogle.cloud.bigtable_v2.services.bigtableimportBigtableClientasGapicClient
8485
fromgoogle.cloud.bigtable.data._sync_autogen.mutations_batcherimport_MB_SIZE
85-
fromgoogle.cloud.bigtable.data._sync_autogen._swappable_channelimportSwappableChannel
86+
fromgoogle.cloud.bigtable.data._sync_autogen._swappable_channelimport (
87+
SwappableChannelasSwappableChannelType,
88+
)
89+
fromgoogle.cloud.bigtable.data._sync_autogen.metrics_interceptorimport (
90+
BigtableMetricsInterceptorasMetricsInterceptorType,
91+
)
8692

8793
ifTYPE_CHECKING:
8894
fromgoogle.cloud.bigtable.data._helpersimportRowKeySamples
@@ -145,6 +151,7 @@ def __init__(
145151
credentials=google.auth.credentials.AnonymousCredentials()
146152
ifprojectisNone:
147153
project=_DEFAULT_BIGTABLE_EMULATOR_CLIENT
154+
self._metrics_interceptor=MetricsInterceptorType()
148155
ClientWithProject.__init__(
149156
self,
150157
credentials=credentials,
@@ -188,7 +195,7 @@ def __init__(
188195
stacklevel=2,
189196
)
190197

191-
def_build_grpc_channel(self, *args, **kwargs) ->SwappableChannel:
198+
def_build_grpc_channel(self, *args, **kwargs) ->SwappableChannelType:
192199
"""This method is called by the gapic transport to create a grpc channel.
193200
194201
The init arguments passed down are captured in a partial used by SwappableChannel
@@ -201,11 +208,20 @@ def _build_grpc_channel(self, *args, **kwargs) -> SwappableChannel:
201208
- **kwargs: keyword arguments passed by the gapic layer to create a new channel with
202209
Returns:
203210
a custom wrapped swappable channel"""
211+
create_channel_fn: Callable[[], Channel]
204212
ifself._emulator_hostisnotNone:
205213
create_channel_fn=partial(insecure_channel, self._emulator_host)
206214
else:
207-
create_channel_fn=partial(TransportType.create_channel, *args, **kwargs)
208-
returnSwappableChannel(create_channel_fn)
215+
216+
defsync_create_channel_fn():
217+
returnintercept_channel(
218+
TransportType.create_channel(*args, **kwargs),
219+
self._metrics_interceptor,
220+
)
221+
222+
create_channel_fn=sync_create_channel_fn
223+
new_channel=SwappableChannelType(create_channel_fn)
224+
returnnew_channel
209225

210226
@property
211227
defuniverse_domain(self) ->str:
@@ -326,10 +342,10 @@ def _manage_channel(
326342
between `refresh_interval_min` and `refresh_interval_max`
327343
grace_period: time to allow previous channel to serve existing
328344
requests before closing, in seconds"""
329-
ifnotisinstance(self.transport.grpc_channel, SwappableChannel):
345+
ifnotisinstance(self.transport.grpc_channel, SwappableChannelType):
330346
warnings.warn("Channel does not support auto-refresh.")
331347
return
332-
super_channel: SwappableChannel=self.transport.grpc_channel
348+
super_channel: SwappableChannelType=self.transport.grpc_channel
333349
first_refresh=self._channel_init_time+random.uniform(
334350
refresh_interval_min, refresh_interval_max
335351
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright 2025 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+
# This file is automatically generated by CrossSync. Do not edit manually.
16+
17+
from __future__ importannotations
18+
fromgrpcimportUnaryUnaryClientInterceptor
19+
fromgrpcimportUnaryStreamClientInterceptor
20+
21+
22+
classBigtableMetricsInterceptor(
23+
UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor
24+
):
25+
"""
26+
An async gRPC interceptor to add client metadata and print server metadata.
27+
"""
28+
29+
defintercept_unary_unary(self, continuation, client_call_details, request):
30+
"""Interceptor for unary rpcs:
31+
- MutateRow
32+
- CheckAndMutateRow
33+
- ReadModifyWriteRow"""
34+
try:
35+
call=continuation(client_call_details, request)
36+
returncall
37+
exceptExceptionasrpc_error:
38+
raiserpc_error
39+
40+
defintercept_unary_stream(self, continuation, client_call_details, request):
41+
"""Interceptor for streaming rpcs:
42+
- ReadRows
43+
- MutateRows
44+
- SampleRowKeys"""
45+
try:
46+
returnself._streaming_generator_wrapper(
47+
continuation(client_call_details, request)
48+
)
49+
exceptExceptionasrpc_error:
50+
raiserpc_error
51+
52+
@staticmethod
53+
def_streaming_generator_wrapper(call):
54+
"""Wrapped generator to be returned by intercept_unary_stream."""
55+
try:
56+
forresponseincall:
57+
yieldresponse
58+
exceptExceptionase:
59+
raisee

‎tests/system/data/test_system_async.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,23 +285,28 @@ async def test_channel_refresh(self, table_id, instance_id, temp_rows):
285285
asyncwithclient.get_table(instance_id, table_id) astable:
286286
rows=awaittable.read_rows({})
287287
channel_wrapper=client.transport.grpc_channel
288-
first_channel=client.transport.grpc_channel._channel
288+
first_channel=channel_wrapper._channel
289289
assertlen(rows) ==2
290290
awaitCrossSync.sleep(2)
291291
rows_after_refresh=awaittable.read_rows({})
292292
assertlen(rows_after_refresh) ==2
293293
assertclient.transport.grpc_channelischannel_wrapper
294-
assertclient.transport.grpc_channel._channelisnotfirst_channel
295-
# ensure gapic's logging interceptor is still active
294+
updated_channel=channel_wrapper._channel
295+
assertupdated_channelisnotfirst_channel
296+
# ensure interceptors are kept (gapic's logging interceptor, and metric interceptor)
296297
ifCrossSync.is_async:
297-
interceptors= (
298-
client.transport.grpc_channel._channel._unary_unary_interceptors
299-
)
300-
assertGapicInterceptorin [type(i) foriininterceptors]
298+
unary_interceptors=updated_channel._unary_unary_interceptors
299+
assertlen(unary_interceptors) ==2
300+
assertGapicInterceptorin [type(i) foriinunary_interceptors]
301+
assertclient._metrics_interceptorinunary_interceptors
302+
stream_interceptors=updated_channel._unary_stream_interceptors
303+
assertlen(stream_interceptors) ==1
304+
assertclient._metrics_interceptorinstream_interceptors
301305
else:
302306
assertisinstance(
303307
client.transport._logged_channel._interceptor, GapicInterceptor
304308
)
309+
assertupdated_channel._interceptor==client._metrics_interceptor
305310
finally:
306311
awaitclient.close()
307312

‎tests/system/data/test_system_autogen.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,18 @@ def test_channel_refresh(self, table_id, instance_id, temp_rows):
237237
withclient.get_table(instance_id, table_id) astable:
238238
rows=table.read_rows({})
239239
channel_wrapper=client.transport.grpc_channel
240-
first_channel=client.transport.grpc_channel._channel
240+
first_channel=channel_wrapper._channel
241241
assertlen(rows) ==2
242242
CrossSync._Sync_Impl.sleep(2)
243243
rows_after_refresh=table.read_rows({})
244244
assertlen(rows_after_refresh) ==2
245245
assertclient.transport.grpc_channelischannel_wrapper
246-
assertclient.transport.grpc_channel._channelisnotfirst_channel
246+
updated_channel=channel_wrapper._channel
247+
assertupdated_channelisnotfirst_channel
247248
assertisinstance(
248249
client.transport._logged_channel._interceptor, GapicInterceptor
249250
)
251+
assertupdated_channel._interceptor==client._metrics_interceptor
250252
finally:
251253
client.close()
252254

0 commit comments

Comments
 (0)