Cache InProcessExecutionAPI in dag.test() to avoid per-task FastAPI app creation#65235
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves dag.test() performance by reusing a single InProcessExecutionAPI instance instead of creating a new FastAPI/a2wsgi stack per task execution.
Changes:
- Cache
in_process_api_server()usingfunctools.lru_cache(maxsize=1)to reuse the in-process Execution API server across task executions. - Ensure stale
dependency_overridesfordag_bag_from_appare removed when_api_client(dag=None)is used. - Add tests/fixtures to validate and manage the caching contract (
cache_clear()usage).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
task-sdk/src/airflow/sdk/execution_time/supervisor.py |
Caches in_process_api_server() and clears dag_bag_from_app dependency override when no DAG is provided. |
task-sdk/tests/task_sdk/execution_time/test_supervisor.py |
Adds a unit test asserting the caching/clearing behavior of in_process_api_server(). |
airflow-core/tests/conftest.py |
Adds an autouse fixture to clear the cached in-process API server after each test to prevent state leakage. |
InProcessExecutionAPI in dag.test()dag.test() creates a new InProcessExecutionAPI for every task execution. Each instance spins up a new FastAPI app, a2wsgi ASGI-to-WSGI bridge with its own event loop thread, and DB connections. Profiling shows the cold event loop thread dominates: 1.25s/task uncached vs 0.20s/task cached (6.2x speedup for a 10-task DAG). Add @functools.lru_cache(maxsize=1) to in_process_api_server() so the FastAPI app and warm event loop are reused across all tasks. DagFileProcessorManager already uses this pattern (manager.py:269). Also clean up stale dependency_overrides when _api_client(dag=None) is called (e.g. from run_trigger_in_process), add a cache_clear fixture in conftest.py, and add a test for the caching contract.
30fa0cc to
2445be7
Compare
InProcessExecutionAPI in dag.test() to avoid per-task FastAPI app creation
ashb
approved these changes
Apr 14, 2026
pankajkoti
approved these changes
Apr 14, 2026
Member
pankajkoti
left a comment
There was a problem hiding this comment.
Thanks for optimising this piece!
Member
Author
|
Static check failure is unrelated |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dag.test()creates a newInProcessExecutionAPIfor every task execution. Each instance spins up a full FastAPI app with Cadwyn versioning, plus ana2wsgiASGI-to-WSGI bridge that starts its own event loop thread. The dominant cost is the cold event loop thread startup and DB connection establishment per HTTP request.Add
@functools.lru_cache(maxsize=1)toin_process_api_server()so the FastAPI app and warm event loop are reused across all tasks within adag.test()call.DagFileProcessorManageralready uses this caching pattern (manager.py:269).Also clean up stale
dependency_overrideswhen_api_client(dag=None)is called (e.g. fromrun_trigger_in_process), and add acache_clearfixture + caching contract test.Benchmarks (breeze, SQLite)
Per-task overhead (profiled, 10-task noop DAG):
Test suite improvement (avg of 3 runs, cache cleared between tests):
The test suite improvement is smaller because fixture setup/teardown and
cache_clear()between tests dominate. For downstream projects like Cosmos that run multi-task DAGs with a session-scoped cache, the per-task speedup applies directly -- consistent with their reported 47min->30min improvement.Bottleneck breakdown (5-task profiled run, uncached):
_thread.lock.acquire(a2wsgi sync bridge): 91%_api_client()(FastAPI+Cadwyn creation): 7%Why this is safe
dependency_overridescleanup:_api_client(dag=None)now pops stale overrides from the cached instancedag.test()runs tasks sequentiallyin_process_api_server()Downstream impact
Cosmos CI tests (astronomer/astronomer-cosmos#2547) worked around this with a session-scoped pytest fixture that monkeypatches
in_process_api_server(). This fix makes that workaround unnecessary.