Synchronous Firestore call unbinds the caller thread's `contextvars` Context (breaks Flask request context) under gunicorn sync worker
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 45/100
Direzione di ricerca
Inizia riproducendo il flusso sincrono get()/batch.commit() con Flask 2.3 e gunicorn sync workers, quindi esamina grpc/_channel.py intorno a _run_channel_spin_thread, ForkManagedThread e segregated_call. Confronta il comportamento con grpc/grpc#40123 e con i punti di chiamata del client Firestore in google/cloud/firestore_v1; il lavoro è completato quando è stato identificato se la modifica del contesto riguarda gRPC in generale o è specifica di Firestore e quando è stata documentata una correzione o una soluzione alternativa supportata.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Determine this is the right repository
- I determined this is the correct repository in which to report this bug.
Summary of the issue
A synchronous Firestore call (gRPC transport) made from inside a request-handling
thread switches/unbinds that thread's contextvars Context partway through. Any
framework that stores per-request state in a ContextVar (Flask 2.3+ keeps its request
context in _cv_request) sees that state disappear on the same thread immediately
after the Firestore call. In our Flask app this surfaces as:
RuntimeError: Working outside of request context
raised during response finalization — a 500 to the client, even though the view code
completed successfully.
Firebase support directed us here, and pointed to a similar regression in the gRPC repo:
grpc/grpc#40123 ("contextvar loss when using thread pool executors in newer gRPC
versions"). Filing so the Cloud Firestore team can determine whether this is global gRPC
behavior or specific to the Firestore client's transport.
Environment
- Python: 3.9
- google-cloud-firestore: 2.18.0
- firebase-admin: 6.5.0 (thin wrapper over the client above)
- google-api-core: 2.19.2
- grpcio / grpcio-status: 1.66.1
- Transport: default (gRPC)
- Web stack: Flask 2.3.0 / Werkzeug 3.0.3, served by gunicorn sync worker
(one request at a time per worker process; stable OS thread) - Client init: module-level
firestore.client()(channel created lazily on first RPC)
Observed behavior
We instrumented contextvars.copy_context and logged has_request_context() (hrc)
with a full stack at each call. Within a single request, on the same OS thread:
(A) First Firestore call — hrc is True (context healthy). Fires at channel creation:
grpc/_channel.py:1761 _run_channel_spin_thread -> cygrpc.ForkManagedThread(target=channel_spin)
grpc/_channel.py:1821 create -> _run_channel_spin_thread(state)
grpc/_channel.py:1414 __call__ -> self._managed_call(...)
google/api_core/grpc_helpers.py:164 error_remapped_callable
google/api_core/timeout.py / retry_unary.py / gapic_v1/method.py
google/cloud/firestore_v1/services/firestore/client.py:1154 batch_get_documents
google/cloud/firestore_v1/document.py:402 get() # <- our doc_ref.get()
... application view ... gunicorn sync worker.handle_request
[hrc=True]
(B) A later Firestore call in the SAME request — hrc is now False (context gone):
grpc/_channel.py:1146 _blocking -> self._channel.segregated_call(...)
grpc/_channel.py:1178 __call__ -> self._blocking(...)
google/api_core/grpc_helpers.py:76 error_remapped_callable
google/api_core/timeout.py / retry_unary.py / gapic_v1/method.py
google/cloud/firestore_v1/services/firestore/client.py:1372 commit
google/cloud/firestore_v1/batch.py:59 commit() # <- our batch.commit()
... same application/gunicorn stack ...
[hrc=False]
Between (A) and (B) our code runs only in-process work (SQLAlchemy queries + a list
comprehension) — nothing that touches contextvars, spawns threads, or uses
asyncio/greenlets. The only machinery that manipulates context between the two points
is the Firestore/gRPC transport (_run_channel_spin_thread / cygrpc.ForkManagedThread
at channel creation, and segregated_call on subsequent RPCs).
Expected vs actual
- Expected: a synchronous Firestore RPC leaves the calling thread's
contextvars
Context unchanged. - Actual: after the RPC, the calling thread is running in a different Context; any
ContextVarset before the call (e.g. Flask's_cv_request) reads as unset.
Reproduction
Flask 2.3 app served by gunicorn sync worker. In a view: log has_request_context(),
do a synchronous Firestore read+write (doc_ref.get() then batch.commit()), log
has_request_context() again. On the first Firestore-touching request after a worker
starts (fresh gRPC channel), the second check is False and Flask raises "Working outside
of request context" during response finalization. Minimal standalone repro available on
request.
Questions
- Is the Firestore Python client (or the underlying gRPC transport) expected to alter
the calling thread'scontextvarsContext during a synchronous RPC? - Is this the same root cause as grpc/grpc#40123, or specific to the Firestore client's
ForkManagedThread/segregated_callusage? - Supported ways to avoid it:
- Pre-creating/warming the gRPC channel at process (post-fork) startup — does that
help, or doessegregated_calldisturb context on every RPC regardless of channel
age? - Using the REST transport (
transport="rest") — is that a supported path to avoid
the gRPC thread/context machinery entirely? - A known-good
grpcioversion to pin below the regression, if this is version-specific?
- Pre-creating/warming the gRPC channel at process (post-fork) startup — does that
Current workaround
We run all Firestore calls on a short-lived daemon thread so the context churn lands on
a disposable thread and the request thread's context stays intact. This resolves the
500s, but we would prefer a supported fix rather than thread-isolating every call.
Impact
Intermittent 500s (correlated with the first Firestore call per worker / gRPC channel
creation) on any endpoint that makes a synchronous Firestore call, in production.
- Lingua principale
- Python
- Stelle
- 5.4k
- Fork
- 1.8k
- Merge medio
- 2g 7h
- PR unite (30g)
- 173
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di googleapis/google-cloud-python
-
firestore: field path with a quoted name ending in a backslash is misparsedForse già presa @sebxt l’ha presa 25 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
googleapis/google-cloud-python#18771 ·
I maintainer di solito rispondono entro 1 giorno
-
api: spanner type: feature request
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
googleapis/google-cloud-python#18584 ·
I maintainer di solito rispondono entro 1 giorno
-
auth: `impersonated_credentials` loses `subject` on copy, so `with_scopes()` and `with_quota_project()` turn off domain wide delegationForse già presa @Om-singhaI l’ha presa 55 giorni fa. Apertaauth priority: p2 type: bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
googleapis/google-cloud-python#18428 ·
I maintainer di solito rispondono entro 1 giorno
-
A client called `run_in_transaction` be stuck if Spanner is downForse già presa @founderinaction l’ha presa 7 giorni fa. Apertapriority: p2 type: bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
googleapis/google-cloud-python#18375 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
ci: enable --strict flag in presubmit twine_checkForse di nuovo libera @ohmayr l’ha presa 44 giorni fa e non c’è nessuna pull request aperta. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 76/100
googleapis/google-cloud-python#18339 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di googleapis/google-cloud-python
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
cpp-best-practices/project_validator#267 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Project-N-E-K-O/N.E.K.O#3725 ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug] Sending a Telegram message chain twice duplicates mentionsForse già presa @123XieC l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
AstrBotDevs/AstrBot#11125 ·
I maintainer di solito rispondono entro 1 giorno
-
backend bug priority: high
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
nooriegarg/ContribIQ#73 ·
I maintainer di solito rispondono entro 1 giorno
-
bug p3 security
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno