From 60235629bce339fef41d03138e71e02ef9ddffd9 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Jun 2026 16:50:01 -0700 Subject: [PATCH 1/3] fix(firestore): BulkWriter pop from an empty deque Fixes https://github.com/googleapis/google-cloud-python/issues/16138 --- .../google/cloud/firestore_v1/bulk_writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py index 141bc7aa6cda..030d77785e40 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py @@ -508,8 +508,9 @@ def _schedule_ready_retries(self) -> None: ) for _ in range(take_until_index): - retry: OperationRetry = self._retries.popleft() - retry.retry(self) + if self._retries: + retry: OperationRetry = self._retries.popleft() + retry.retry(self) return None def _request_send(self, batch_size: int) -> bool: From 0770b5dab7eb963e21db65b683d186297b81844b Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 17 Jun 2026 13:32:48 -0700 Subject: [PATCH 2/3] Update packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/firestore_v1/bulk_writer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py index 030d77785e40..638471cbb9e8 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/bulk_writer.py @@ -508,9 +508,10 @@ def _schedule_ready_retries(self) -> None: ) for _ in range(take_until_index): - if self._retries: - retry: OperationRetry = self._retries.popleft() - retry.retry(self) + if not self._retries: + break + retry: OperationRetry = self._retries.popleft() + retry.retry(self) return None def _request_send(self, batch_size: int) -> bool: From 810d0f8b3cb5e2990dc6b9007a316aa3ca8ea8aa Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 24 Jul 2026 15:26:25 -0700 Subject: [PATCH 3/3] added test --- .../tests/unit/v1/test_bulk_writer.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_bulk_writer.py b/packages/google-cloud-firestore/tests/unit/v1/test_bulk_writer.py index 17486600b836..7fb6776e06eb 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_bulk_writer.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_bulk_writer.py @@ -759,6 +759,46 @@ def test_basebulkwriter_update_raises_with_bad_option(self): option=ExistsOption(exists=True), ) + def test_basebulkwriter_reentrancy_empty_deque_retry(self): + # See https://github.com/googleapis/google-cloud-python/issues/16138 + import bisect + import datetime + + from google.cloud.firestore_v1.bulk_writer import ( + BulkWriterCreateOperation, + OperationRetry, + ) + + client = self._make_client() + bw = _make_no_send_bulk_writer(client) + + ref1 = _get_document_reference(client, id="doc1") + ref2 = _get_document_reference(client, id="doc2") + data = {"field": "value"} + + # Put ref1 in current operations to trigger a document path collision + # and subsequent flush/enqueue when we create ref1 again. + bw.create(ref1, data) + + run_at = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta( + seconds=1 + ) + + retry1 = OperationRetry( + operation=BulkWriterCreateOperation(reference=ref1, document_data=data), + run_at=run_at, + ) + retry2 = OperationRetry( + operation=BulkWriterCreateOperation(reference=ref2, document_data=data), + run_at=run_at, + ) + + bisect.insort(bw._retries, retry1) + bisect.insort(bw._retries, retry2) + + # This should not raise IndexError: pop from an empty deque due to re-entrant scheduling + bw._schedule_ready_retries() + class TestSyncBulkWriter(_SyncClientMixin, _BaseBulkWriterTests): """All BulkWriters are opaquely async, but this one simulates a BulkWriter