Skip to content

Commit 6e0f1d9

Browse files
authored
feat(firestore): configure gRPC message length limits for large documents (#18220)
- Configure `grpc.max_send_message_length` and `grpc.max_receive_message_length` options on production and emulator gRPC channels. - Update unit tests in `test_base_client.py` to assert gRPC message size options. - Add system tests for large document CRUD and pipeline execution across sync and async clients.
1 parent a7e09c9 commit 6e0f1d9

4 files changed

Lines changed: 145 additions & 5 deletions

File tree

packages/google-cloud-firestore/google/cloud/firestore_v1/base_client.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@
7777
_INACTIVE_TXN: str = "There is no active transaction."
7878
_CLIENT_INFO: Any = client_info.ClientInfo(client_library_version=__version__)
7979
_FIRESTORE_EMULATOR_HOST: str = "FIRESTORE_EMULATOR_HOST"
80+
_GRPC_MSG_SIZE_OPTIONS: List[Tuple[str, int]] = [
81+
("grpc.max_send_message_length", -1),
82+
("grpc.max_receive_message_length", -1),
83+
]
84+
_DEFAULT_CHANNEL_OPTIONS: List[Tuple[str, Any]] = [
85+
("grpc.keepalive_time_ms", 30000),
86+
*_GRPC_MSG_SIZE_OPTIONS,
87+
]
8088

8189

8290
class BaseClient(ClientWithProject):
@@ -173,7 +181,7 @@ def _firestore_api_helper(self, transport, client_class, client_module) -> Any:
173181
channel = transport.create_channel(
174182
self._target,
175183
credentials=self._credentials,
176-
options={"grpc.keepalive_time_ms": 30000}.items(),
184+
options=_DEFAULT_CHANNEL_OPTIONS,
177185
)
178186

179187
self._transport = transport(host=self._target, channel=channel)
@@ -204,7 +212,10 @@ def _emulator_channel(self, transport):
204212
and getattr(self._credentials, "id_token", None) is not None
205213
):
206214
token = self._credentials.id_token
207-
options = [("Authorization", f"Bearer {token}")]
215+
options = [
216+
("Authorization", f"Bearer {token}"),
217+
*_GRPC_MSG_SIZE_OPTIONS,
218+
]
208219

209220
if "GrpcAsyncIOTransport" in str(transport.__name__):
210221
return grpc.aio.insecure_channel(self._emulator_host, options=options)

packages/google-cloud-firestore/tests/system/test_system.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3836,3 +3836,39 @@ def in_transaction(transaction, rollback):
38363836
assert len(result) == 1
38373837
assert len(result[0]) == 1
38383838
assert result[0][0].value == expected
3839+
3840+
3841+
@pytest.mark.skip(reason="Temporarily skipped. Not yet in production.")
3842+
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
3843+
def test_large_document_standard_writes(client, cleanup, database):
3844+
"""Test standard write and read operations for 5MB document on Enterprise DB."""
3845+
collection_id = "large_docs_" + UNIQUE_RESOURCE_ID
3846+
doc_ref = client.collection(collection_id).document("large_doc")
3847+
cleanup(doc_ref.delete)
3848+
3849+
large_payload = "a" * (5 * 1024 * 1024)
3850+
doc_ref.set({"payload": large_payload})
3851+
3852+
snapshot = doc_ref.get()
3853+
assert snapshot.exists
3854+
assert snapshot.to_dict() == {"payload": large_payload}
3855+
3856+
3857+
@pytest.mark.skip(reason="Temporarily skipped. Not yet in production.")
3858+
@pytest.mark.parametrize("method", ["execute", "stream"])
3859+
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
3860+
def test_large_document_pipeline(client, cleanup, database, method):
3861+
"""Test pipeline execution over 5MB document on Enterprise DB."""
3862+
collection_id = "large_pipeline_" + UNIQUE_RESOURCE_ID
3863+
col_ref = client.collection(collection_id)
3864+
doc_ref = col_ref.document("large_doc")
3865+
cleanup(doc_ref.delete)
3866+
3867+
large_payload = "b" * (5 * 1024 * 1024)
3868+
doc_ref.set({"payload": large_payload})
3869+
3870+
pipeline = client.pipeline().collection(collection_id)
3871+
method_under_test = getattr(pipeline, method)
3872+
3873+
results = list(method_under_test())
3874+
assert [doc.data() for doc in results] == [{"payload": large_payload}]

packages/google-cloud-firestore/tests/system/test_system_async.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3708,3 +3708,41 @@ async def in_transaction(transaction):
37083708
await in_transaction(transaction)
37093709
# make sure we didn't skip assertions in inner function
37103710
assert inner_fn_ran is True
3711+
3712+
3713+
@pytest.mark.skip(reason="Temporarily skipped. Not yet in production.")
3714+
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
3715+
async def test_large_document_standard_writes_async(client, cleanup, database):
3716+
"""Test standard write and read operations for 5MB document on Enterprise DB (async)."""
3717+
collection_id = "large_docs_async_" + UNIQUE_RESOURCE_ID
3718+
doc_ref = client.collection(collection_id).document("large_doc")
3719+
cleanup(doc_ref.delete)
3720+
3721+
large_payload = "c" * (5 * 1024 * 1024)
3722+
await doc_ref.set({"payload": large_payload})
3723+
3724+
snapshot = await doc_ref.get()
3725+
assert snapshot.exists
3726+
assert snapshot.to_dict() == {"payload": large_payload}
3727+
3728+
3729+
@pytest.mark.skip(reason="Temporarily skipped. Not yet in production.")
3730+
@pytest.mark.parametrize("method", ["execute", "stream"])
3731+
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
3732+
async def test_large_document_pipeline_async(client, cleanup, database, method):
3733+
"""Test async pipeline execution over 5MB document on Enterprise DB."""
3734+
collection_id = "large_pipeline_async_" + UNIQUE_RESOURCE_ID
3735+
col_ref = client.collection(collection_id)
3736+
doc_ref = col_ref.document("large_doc")
3737+
cleanup(doc_ref.delete)
3738+
3739+
large_payload = "d" * (5 * 1024 * 1024)
3740+
await doc_ref.set({"payload": large_payload})
3741+
3742+
pipeline = client.pipeline().collection(collection_id)
3743+
if method == "execute":
3744+
results = await pipeline.execute()
3745+
else:
3746+
results = [doc async for doc in pipeline.stream()]
3747+
3748+
assert [doc.data() for doc in results] == [{"payload": large_payload}]

packages/google-cloud-firestore/tests/unit/v1/test_base_client.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,48 @@ def test_baseclient__firestore_api_helper_wo_emulator():
110110

111111
assert api is client_class.return_value
112112
assert client._firestore_api_internal is api
113-
channel_options = {"grpc.keepalive_time_ms": 30000}
113+
channel_options = [
114+
("grpc.keepalive_time_ms", 30000),
115+
("grpc.max_send_message_length", -1),
116+
("grpc.max_receive_message_length", -1),
117+
]
114118
transport_class.create_channel.assert_called_once_with(
115-
target, credentials=client._credentials, options=channel_options.items()
119+
target, credentials=client._credentials, options=channel_options
120+
)
121+
transport_class.assert_called_once_with(
122+
host=target,
123+
channel=transport_class.create_channel.return_value,
124+
)
125+
client_class.assert_called_once_with(
126+
transport=transport_class.return_value, client_options=client_options
127+
)
128+
129+
130+
def test_baseclient__firestore_api_helper_async_wo_emulator():
131+
from google.cloud.firestore_v1.services.firestore.transports.grpc_asyncio import (
132+
FirestoreGrpcAsyncIOTransport,
133+
)
134+
135+
client = _make_default_base_client()
136+
client_options = client._client_options = mock.Mock()
137+
target = client._target
138+
assert client._firestore_api_internal is None
139+
140+
transport_class = mock.Mock(spec=FirestoreGrpcAsyncIOTransport)
141+
client_class = mock.Mock()
142+
client_module = mock.Mock()
143+
144+
api = client._firestore_api_helper(transport_class, client_class, client_module)
145+
146+
assert api is client_class.return_value
147+
assert client._firestore_api_internal is api
148+
channel_options = [
149+
("grpc.keepalive_time_ms", 30000),
150+
("grpc.max_send_message_length", -1),
151+
("grpc.max_receive_message_length", -1),
152+
]
153+
transport_class.create_channel.assert_called_once_with(
154+
target, credentials=client._credentials, options=channel_options
116155
)
117156
transport_class.assert_called_once_with(
118157
host=target,
@@ -236,7 +275,23 @@ def test_baseclient__emulator_channel():
236275
with mock.patch("grpc.insecure_channel") as insecure_channel:
237276
channel = client._emulator_channel(FirestoreGrpcTransport)
238277
insecure_channel.assert_called_once_with(
239-
emulator_host, options=[("Authorization", "Bearer test")]
278+
emulator_host,
279+
options=[
280+
("Authorization", "Bearer test"),
281+
("grpc.max_send_message_length", -1),
282+
("grpc.max_receive_message_length", -1),
283+
],
284+
)
285+
286+
with mock.patch("grpc.aio.insecure_channel") as aio_insecure_channel:
287+
channel = client._emulator_channel(FirestoreGrpcAsyncIOTransport)
288+
aio_insecure_channel.assert_called_once_with(
289+
emulator_host,
290+
options=[
291+
("Authorization", "Bearer test"),
292+
("grpc.max_send_message_length", -1),
293+
("grpc.max_receive_message_length", -1),
294+
],
240295
)
241296

242297

0 commit comments

Comments
 (0)