From c9edfad44484d2220d92d03c6815d3ad08eb6bbf Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Tue, 20 Oct 2026 17:46:06 +0000 Subject: [PATCH 1/2] feat(bigtable): raise the UDS channel inbound message cap to 256 MB Change-Id: I6adb2982640fe2d6736ea63ed165c9ffd6feacb7 --- .../bigtable/data/_async/_accelerator_client.py | 14 +++++++++++++- .../data/_sync_autogen/_accelerator_client.py | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py index e6c2dd9db41c..8b168c232b53 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py @@ -46,6 +46,16 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" +# Inbound message cap on the UDS channel. gRPC defaults this to 4 MiB, a limit +# meant for a channel that crosses a network. This one doesn't leave the +# machine: the daemon has already paid the cost of pulling the response off the +# wire, and all the cap does on this hop is turn an otherwise-fine read into +# RESOURCE_EXHAUSTED. Raise it well past anything the service itself produces so +# the UDS leg is never the narrower of the two. +_MAX_RECEIVE_MESSAGE_LENGTH = 256 * 1024 * 1024 + +_CHANNEL_OPTIONS = (("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH),) + @CrossSync.convert_class(sync_name="_AcceleratorClient") class _AsyncAcceleratorClient: @@ -59,7 +69,9 @@ class _AsyncAcceleratorClient: def __init__(self, uds_path: str, auth_secret: str): self._uds_path = uds_path self._metadata = (("x-accelerator-token", auth_secret),) - self._channel = insecure_channel(_uds_target(uds_path)) + self._channel = insecure_channel( + _uds_target(uds_path), options=_CHANNEL_OPTIONS + ) self._mutate_row_stub = self._channel.unary_unary( _MUTATE_ROW_METHOD, request_serializer=MutateRowRequest.serialize, diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py index 452fcc9c7b72..6b61599736fb 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py @@ -42,6 +42,8 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" +_MAX_RECEIVE_MESSAGE_LENGTH = 256 * 1024 * 1024 +_CHANNEL_OPTIONS = (("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH),) class _AcceleratorClient: @@ -55,7 +57,9 @@ class _AcceleratorClient: def __init__(self, uds_path: str, auth_secret: str): self._uds_path = uds_path self._metadata = (("x-accelerator-token", auth_secret),) - self._channel = insecure_channel(_uds_target(uds_path)) + self._channel = insecure_channel( + _uds_target(uds_path), options=_CHANNEL_OPTIONS + ) self._mutate_row_stub = self._channel.unary_unary( _MUTATE_ROW_METHOD, request_serializer=MutateRowRequest.serialize, From 4adc62b4f9bd65357d8af7fe0885bf291fd1419c Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Mon, 26 Oct 2026 17:41:54 +0000 Subject: [PATCH 2/2] feat(bigtable): raise the UDS channel message caps to the service limits Change-Id: Ib476eb9173ad0116a6a2c6fd743d6669a831d871 --- .../data/_async/_accelerator_client.py | 23 +++++--- .../data/_sync_autogen/_accelerator_client.py | 6 +- .../unit/data/test_accelerator_client.py | 59 +++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 packages/google-cloud-bigtable/tests/unit/data/test_accelerator_client.py diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py index 8b168c232b53..a619247cee69 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/_accelerator_client.py @@ -46,15 +46,24 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" -# Inbound message cap on the UDS channel. gRPC defaults this to 4 MiB, a limit -# meant for a channel that crosses a network. This one doesn't leave the -# machine: the daemon has already paid the cost of pulling the response off the -# wire, and all the cap does on this hop is turn an otherwise-fine read into -# RESOURCE_EXHAUSTED. Raise it well past anything the service itself produces so -# the UDS leg is never the narrower of the two. +# Message caps on the UDS channel. gRPC defaults both directions to 4 MiB, a +# limit meant for a channel that crosses a network. This one doesn't leave the +# machine, and all the default does on this hop is turn an otherwise-fine call +# into RESOURCE_EXHAUSTED before it ever reaches the daemon's own client. Each +# direction is instead set to the matching service-side limit, so the UDS leg is +# never the narrower of the two and an oversized call fails where it should -- +# at the service, with the service's own error. +# +# Inbound: 256 MiB, the hard limit on the combined size of all values in a +# single row, which bounds the largest response the service can produce. +# Outbound: 100 MB, the request size the service itself accepts. _MAX_RECEIVE_MESSAGE_LENGTH = 256 * 1024 * 1024 +_MAX_SEND_MESSAGE_LENGTH = 100 * 1024 * 1024 -_CHANNEL_OPTIONS = (("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH),) +_CHANNEL_OPTIONS = ( + ("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH), + ("grpc.max_send_message_length", _MAX_SEND_MESSAGE_LENGTH), +) @CrossSync.convert_class(sync_name="_AcceleratorClient") diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py index 6b61599736fb..716daef672f9 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/_accelerator_client.py @@ -43,7 +43,11 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" _MAX_RECEIVE_MESSAGE_LENGTH = 256 * 1024 * 1024 -_CHANNEL_OPTIONS = (("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH),) +_MAX_SEND_MESSAGE_LENGTH = 100 * 1024 * 1024 +_CHANNEL_OPTIONS = ( + ("grpc.max_receive_message_length", _MAX_RECEIVE_MESSAGE_LENGTH), + ("grpc.max_send_message_length", _MAX_SEND_MESSAGE_LENGTH), +) class _AcceleratorClient: diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_accelerator_client.py b/packages/google-cloud-bigtable/tests/unit/data/test_accelerator_client.py new file mode 100644 index 000000000000..244e54a036c1 --- /dev/null +++ b/packages/google-cloud-bigtable/tests/unit/data/test_accelerator_client.py @@ -0,0 +1,59 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unit tests for the UDS channel configuration in _accelerator_client.py.""" + +from unittest import mock + +import pytest + +from google.cloud.bigtable.data._async import _accelerator_client as async_module +from google.cloud.bigtable.data._sync_autogen import ( + _accelerator_client as sync_module, +) + +_MODULES = pytest.mark.parametrize( + "module, client_name", + [ + (async_module, "_AsyncAcceleratorClient"), + (sync_module, "_AcceleratorClient"), + ], + ids=["async", "sync"], +) + + +class TestChannelOptions: + """The caps are load-bearing, and the option keys are easy to typo. + + A misspelled key is silently ignored by gRPC, which leaves the channel on + the 4 MiB default and fails only on a large row. + """ + + @_MODULES + def test_options_cap_each_direction_at_the_service_limit(self, module, client_name): + # Inbound 256 MiB: the hard limit on all values in a single row. + # Outbound 100 MB: the request size the service accepts. + assert module._MAX_RECEIVE_MESSAGE_LENGTH == 256 * 1024 * 1024 + assert module._MAX_SEND_MESSAGE_LENGTH == 100 * 1024 * 1024 + assert module._CHANNEL_OPTIONS == ( + ("grpc.max_receive_message_length", 256 * 1024 * 1024), + ("grpc.max_send_message_length", 100 * 1024 * 1024), + ) + + @_MODULES + def test_channel_is_built_with_the_options(self, module, client_name): + with mock.patch.object(module, "insecure_channel") as channel_mock: + getattr(module, client_name)("/tmp/sock", "secret") + channel_mock.assert_called_once_with( + module._uds_target("/tmp/sock"), options=module._CHANNEL_OPTIONS + )