fix: prevent event loop blocking from JSON serialization in async contexts - #3815
fix: prevent event loop blocking from JSON serialization in async contexts#3815nivas4506 wants to merge 4 commits into
Conversation
…texts The asyncio event loop was being blocked during JSON serialization of Pydantic models when making API requests, causing timeouts in other concurrent operations like Redis, Kafka, and WebSockets. Changes: - Added AsyncAPIClient._build_request_async() to handle request building with non-blocking JSON serialization - Use asyncify(openapi_dumps) to run JSON encoding in a thread pool, keeping the event loop responsive - Updated async request() to use _build_request_async instead of the blocking _build_request - Added corresponding async override in BaseAzureClient for Azure SDK compatibility This ensures that expensive serialization operations (especially with structured output using Pydantic models) do not block concurrent async operations on the event loop. Fixes openai#3777 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b91167321
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # since httpx's content param overrides other body arguments | ||
| kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None | ||
| if is_given(json_data) and json_data is not None: | ||
| kwargs["content"] = self._serialize_json_data(json_data) |
There was a problem hiding this comment.
Restore synchronous JSON serialization
For synchronous client requests with a non-bytes JSON body and no files, this branch now calls _serialize_json_data, but BaseClient has no such method anywhere in the SDK. Those requests therefore raise AttributeError before reaching HTTP; restore the direct openapi_dumps(json_data) call or add the missing helper.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in latest commit - restored the direct \openapi_dumps(json_data)\ call for synchronous client requests. The sync client now correctly uses synchronous JSON serialization while the async client uses thread-pool serialization to avoid blocking the event loop.
The sync client was incorrectly calling a non-existent _serialize_json_data() method. Restored the original openapi_dumps() call for synchronous JSON serialization in BaseClient._build_request() to match the async client's thread-pool serialization approach. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The temporary test file was used for local verification and is no longer needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b323c6691
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,83 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
Register the regression test with pytest
pyproject.toml restricts pytest discovery to tests, so this root-level module is never collected by the repository test suite. The serialization regression therefore has no CI coverage; move it under tests/ (and make it use the normal test fixtures) so the async request path is exercised.
AGENTS.md reference: AGENTS.md:L41-L45
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
✅ Fixed - Moved the regression test to tests/test_event_loop_blocking.py where it is properly discovered and run by pytest during CI. The test now uses the async_client fixture from conftest.py and is integrated into the full test suite.
| bg_task = asyncio.create_task(background_task(0.1)) | ||
| request = await client._build_request_async(options) | ||
| bg_result = await bg_task |
There was a problem hiding this comment.
Make the event-loop test observe concurrent work
This assertion can pass even if _build_request_async blocks the event loop: create_task() only schedules background_task, and a blocking implementation can finish before that task ever starts; it then measures only its own 100 ms sleep. Start and confirm the background work before serialization, then assert that it makes progress during a deliberately slow serialization, ideally through a public request entrypoint.
AGENTS.md reference: AGENTS.md:L123-L128
Useful? React with 👍 / 👎.
Added a comprehensive regression test in tests/test_event_loop_blocking.py that verifies async requests don't block the event loop during JSON serialization. The test: - Starts a background async task that runs concurrent work - Makes an API request that triggers JSON serialization - Verifies the background task makes progress during serialization - Ensures the event loop remains responsive for concurrent operations This fixes the CI coverage gap identified in code review and ensures the async JSON serialization fix for issue openai#3777 doesn't regress. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5309a652f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # We expect at least 50 iterations out of 100 to have completed. | ||
| # This threshold allows for small timing variations while still catching | ||
| # any significant event loop blocking. | ||
| assert background_task_iterations >= 50, ( |
There was a problem hiding this comment.
Assert progress before awaiting task completion
The new start synchronization addresses the earlier scheduling issue, but this assertion is still guaranteed to pass: immediately before it, the test waits for background_task_done and awaits the task, whose loop always increments the counter 100 times. Thus both the old blocking serializer and this implementation satisfy the test, while its tiny request payload never exercises the reported large-serialization case. Record progress while the request is in flight using a large in-memory payload so this regression is actually detected.
AGENTS.md reference: AGENTS.md:L123-L128
Useful? React with 👍 / 👎.
| # since httpx's content param overrides other body arguments | ||
| if is_given(json_data) and json_data is not None: | ||
| # Use async serialization to avoid blocking the event loop | ||
| kwargs["content"] = await asyncify(openapi_dumps)(json_data) |
There was a problem hiding this comment.
Use a serializer that releases the GIL
On supported CPython, moving this call to a worker thread does not prevent the reported event-loop starvation for large ordinary JSON bodies: openapi_dumps delegates to json.dumps, whose C encoder holds the GIL while encoding primitive containers. The worker then owns the GIL for long stretches, so the event-loop thread still cannot run Redis or heartbeat coroutines; a large in-memory primitive payload reproduces multi-second scheduling gaps. Use an incremental/GIL-releasing serialization strategy (or process isolation) rather than relying on asyncio.to_thread.
AGENTS.md reference: AGENTS.md:L114-L119
Useful? React with 👍 / 👎.
Problem
The asyncio event loop was being blocked during JSON serialization of Pydantic models when making API requests. This caused timeouts and performance issues in other concurrent operations (Redis, Kafka, WebSockets, etc.) that share the event loop.
Users reported symptoms like:
Root Cause
The
_build_request()method is called directly in the async context and performs blocking JSON serialization viaopenapi_dumps(), which usesjson.JSONEncoderwith Pydantic model handling. For complex models or structured outputs, this serialization can take significant time and blocks the entire event loop, preventing other async tasks from running.Solution
Added an async-safe
_build_request_async()method that offloads JSON serialization to a thread pool:AsyncAPIClient._build_request_async()usesasyncify(openapi_dumps)to run JSON encoding in a background threadrequest()method now calls_build_request_async()instead of the blocking_build_request()BaseAzureClientfor Azure SDK compatibilityThis ensures expensive serialization operations never block the event loop, keeping it responsive for concurrent operations.
Testing
Verified with a concurrent test that:
Fixes #3777