fix: guard sync GraphQL calls to prevent DeadlockError inside Temporal workflows (OPS-4402)#71
Conversation
There was a problem hiding this comment.
Overall Assessment
The change adds an asyncio.get_running_loop() guard to all sync entry points (query/query_one/mutate/execute plus batch paths) so that a clear RuntimeError is raised immediately instead of producing a downstream DeadlockError when pygqlc is misused inside a Temporal workflow (or any running event loop). The implementation, test, version bump, and changelog entry are all sound; no blocking bugs.
Findings
No actionable findings.
Notes
- The guard is deliberately placed before the broad
except Exceptionwrappers in the high-level methods, ensuring the prescriptiveRuntimeErroris never swallowed (as stated in the PR description). - Test is hermetic, exercises all four sync paths plus the "still works from plain sync" negative case, and follows the AGENTS.md rules (TDD first, no sockets/sleeps, own client, no shared fixtures).
- Batch entry points (
batchQuery/batchMutate) correctly rely on the guard being hit later insideMutationBatchoperations rather than duplicating the check. Considered but not flagged because the docstring and behavior match the stated intent.
| import orjson | ||
| import logging | ||
| import websocket | ||
| from tenacity import retry, retry_if_result, stop_after_attempt, wait_random |
|
The Exact failing items (quoted from PR file list ( None of the failing tests (test_query_one_flatten, the three subscribe tests) or their source files appear in the diff. Our added test ( This is a pre-existing condition on (The code change itself, the new hermetic TDD test, formatting, bandit, version, changelog, and lock refresh are all clean; the Palantir reviewer already passed with "No actionable findings".) |
ed2e776 to
4b4cd60
Compare
…vent Temporal DeadlockError) (OPS-4402)
4b4cd60 to
cff1a7e
Compare
There was a problem hiding this comment.
Overall Assessment
This PR adds a defensive guard (_assert_not_in_async_context) to the four public sync entrypoints (query, query_one, mutate, execute) that immediately raises a clear RuntimeError (with guidance to use async_* or a Temporal Activity) when called inside a running event loop. The change prevents the downstream DeadlockError from blocking httpx inside Temporal workflows. Implementation is minimal, follows AGENTS.md (TDD, hermetic tests, version+changelog, ruff/bandit), and has no blocking bugs.
Findings
No actionable findings.
Notes
- The guard is placed at the top of each sync method before any work or I/O, matching the documented intent in
_assert_not_in_async_context's docstring. - Test
test_sync_query_disallowed_in_async_contextis hermetic (own client, no network, no 'gql' fixture) and covers the positive (raises in loop) and negative (works from plain sync) cases for all four entrypoints. - Version bumped to 3.8.3 and CHANGELOG entry added per repo rules; the PR description text saying "3.8.2" is only a prose inconsistency, not a code defect.
- Considered whether
batchQuery/batchMutateneeded explicit guards: they delegate toMutationBatchwhich ultimately calls the guarded methods, so no additional guard is required.
| import orjson | ||
| import logging | ||
| import websocket | ||
| from tenacity import retry, retry_if_result, stop_after_attempt, wait_random |
⏸️ Awaiting human inputHow to push the rebased commit (local 61378bd on main b8a1958) when push-safe exits 3 on CHANGELOG conflict vs stale remote PR tip cff1a7e? Reason: push-safe rebase recovery conflicted on CHANGELOG.md; do not force; PR#71 is conflicting Reply on this PR (or the linked Linear thread) to unblock the agent. |
❌ Agent run failedThe implementer agent encountered a failure and has been parked Reason: If this is a transient provider error (e.g. xAI capacity), the |
Summary
Guard the synchronous blocking API (
query,query_one,mutate,execute, and batch paths) so that calling them while an asyncio event loop is running immediately raises a clearRuntimeErrordirecting callers to theasync_*methods or a Temporal Activity. This turns a downstream DeadlockError (blockinghttpxinside Temporal) into a fast, obvious failure at the call site.Why
See Linear OPS-4402. The stack trace showed
gql.query(...)(pygqlc/GraphQLClient.py:339 via execute:1046) from inside a@workflow.run(valiotworkflows) in valuechainos_queues/.../workflow.py:76. Temporal workflows must never block; the library's primary sync surface had no defense against a running event loop. Triage: NOTIFY+FIX, code_bug, high.The change is the smallest effective guard:
asyncio.get_running_loop()probe (no extra deps) at the entry of every public sync method, before any work or I/O. Follows this repo's AGENTS.md (TDD first, hermetic tests with no sockets/sleeps,uv, ruff format, bandit, version bump + CHANGELOG on every change, lock refresh on every PR, "majestic monolith" / simplest-thing style).Changes
pygqlc/GraphQLClient.py: added_assert_not_in_async_context()(raises with prescriptive guidance) and calls fromquery,query_one,mutate,execute(and transitively batch paths via the same methods).tests/pygqlc/gql_client/test_query.py: hermetic regression test (test_sync_query_disallowed_in_async_context) — own client, no 'gql' fixture, no network; asserts raise underasyncio.run()for all four entry points and that the guard is a no-op from plain sync context.pygqlc/__version__.py: 3.8.3 (patch for bugfix).CHANGELOG.md: top## [3.8.3] - 2026-06-24entry in the required style.Test plan
.mutate,.query_one, direct.execute, and the "still works from sync" negative case.uv run pytest -k "sync_query_disallowed or helpers or parsers"— 25+ hermetic tests pass (full suite has pre-existing env-dependent failures unrelated to this diff; those tests do not appear ingit diff --stat origin/main...HEAD).uvx ruff format --checkclean.uvx bandit -r . -s B101 -ll --exclude $(find . -type d -name '.venv' | paste -sd, -)clean (2 low-confidence items pre-existing, suppressed with# nosecas before).uv lock --upgraderun (no net diff in this rebase; lock already current).git diff origin/main...HEADreview: exactly the guard + test + version + changelog; no debug prints, no scope creep, no unrelated files.origin/main(cb88649), resolved conflicts cleanly, pushed withgit push-safe(force-with-lease against the prior remote PR tip).Closes OPS-4402