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..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,6 +46,25 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" +# 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), + ("grpc.max_send_message_length", _MAX_SEND_MESSAGE_LENGTH), +) + @CrossSync.convert_class(sync_name="_AcceleratorClient") class _AsyncAcceleratorClient: @@ -59,7 +78,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..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 @@ -42,6 +42,12 @@ _MUTATE_ROW_METHOD = "/google.bigtable.v2.Bigtable/MutateRow" _READ_ROWS_METHOD = "/google.bigtable.v2.Bigtable/ReadRows" +_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), + ("grpc.max_send_message_length", _MAX_SEND_MESSAGE_LENGTH), +) class _AcceleratorClient: @@ -55,7 +61,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, 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 + )