Repository navigation
Conversation
|
cc @jason810496 PTAL |
jason810496
left a comment
There was a problem hiding this comment.
Nice, thanks for covering up the regression.
High level question: Would it be possible to make this a unit test instead of prek hook?
Since we’re targeting to shrink the static check as much as possible, I’d like to double check the feasibility before adding a hook.
Thanks.
665f39a to
d17db53
Compare
A response field annotated with Iterable is validated by pydantic lazily, while FastAPI serializes the response on the event loop. Each item's attributes are resolved there, on the sync engine: a lazy relationship or a selectin post-load becomes a query that blocks the whole API server once the connection pool is busy. PoolCollectionResponse.pools was fixed by hand in apache#74247, which left an explanatory comment in the datamodel, and the dags and deadlines fields in apache#76525. Twenty fields still have the shape, and nothing stops a new one from being added, because nothing about the annotation hints at the cost: list[...] validates on construction, inside the route, and renders to the same type: array in the OpenAPI schema, so the two read as interchangeable. The known fields are grandfathered as strict xfails rather than fixed here, so the list stays visible in the test report and can only shrink. The shape of the check follows the one proposed for the same class of problem in apache#76424. Incomplete models are rebuilt before their fields are read. An annotation naming something defined later in its own module stays an unparsed ForwardRef that get_origin cannot see through, as AssetResponse.aliases does, so a lazy field behind one would go unnoticed.
d17db53 to
f877295
Compare
That's a good idea, changing to use unit test way |
jason810496
left a comment
There was a problem hiding this comment.
IIRC, some of those fields in Pydantic model are intentionally kept as iterable instead of list - #57878.
Would it be possible to make those only involve SQLA query in the known exception rather than adding all of them as known invalidation?
Thanks.
|
|
||
| The fields that predate this test are listed in | ||
| ``generated/known_lazy_response_fields.txt`` as strict xfails. The collection ones are | ||
| tracked for migration in #76425. |
There was a problem hiding this comment.
It seems like wrong issue id?
Thanks for you were info. I measured it, and only Does that work? Or we should juxt fix |
related: #76683
Replaces the prek hook from the first revision with a unit test, per the review question — the hook, its allowlist manager, its 33 tests and its
.pre-commit-config.yamlentry are gone.Why
A response field annotated with
Iterable[...]is validated by pydantic lazily, while FastAPI serializes the response on the event loop. Each item's attributes are resolved there, on the sync engine: a lazy relationship or aselectinpost-load becomes a query that blocks the whole API server once the connection pool is busy.PoolCollectionResponse.poolswas fixed by hand in #74247, which left an explanatory comment in the datamodel, and thedagsanddeadlinesfields in #76525. Eight fields are still handed something unresolved by their route, and nothing stops a new one from being added — nothing about the annotation hints at the cost.list[...]validates on construction, inside the route, and renders to the sametype: arrayin the OpenAPI schema, so the two read as interchangeable.What
The
generated/+ strict-xfail shape proposed for the same class of problem in #76424, rather than a static check.airflow-core/tests/unit/api_fastapi/test_lazy_response_fields.pyairflow.api_fastapiand collects the pydantic models from each module's namespace, not frompydantic.BaseModel.__subclasses__().create_modelattributes a model to the module that called it, somake_partial_model's output — and any model defined in another test module — shows up as a subclass and would make the result depend on what else the session had imported. Models nested in another model's body are picked up too.Iterable, aTypeAliasTypealias,Iterable[X] | None,Annotated[Iterable[X], Field(...)]and an inherited field all match. OnlyIterableandGeneratorare actually validated lazily;Iterator/AsyncIterable/AsyncIteratorraisePydanticSchemaGenerationErrorat class definition, and are listed to keep the rule one thing to remember.ForwardRefthatget_origincannot see through —AssetResponse.aliasesis one — so a lazy field behind one would go unnoticed. A separate test fails if any model still cannot be resolved, so the check cannot quietly become a no-op.main,gunicorn_appandgunicorn_config. None defines a model; importingmainbuilds a whole app and sets_AIRFLOW_PROCESS_CONTEXT=serverfor the rest of the pytest process, and the gunicorn modules import an optional extra at module level.*CollectionResponse: two of the existing fields are on plain response models in the execution API. A model defined inside a function, or one living outside the package, is out of reach, and the docstring says so.The existing fields, split by what their route passes
Declaring these fields
Iterable[...]was deliberate in #57878, to spare the routes alist()or.all()that SQLA2's typing had made them add. Whether that costs anything is decided at the route, which the annotation cannot show, so the twenty existing fields are split by reading every construction site.The split is by what reaches the event loop, not by whether the route built a list. Pydantic validates each item during serialization either way, so an ORM row the response then lazy-loads from is as costly as passing the query itself —
DAGRunCollectionResponse.dag_runsis exactly that case, through one of its four construction sites.generated/known_lazy_response_fields.txt— 8 pending, tracked for migration in #76683:asset_aliases,backfills,connections,dag_runs,dag_versions,dag_warnings,deadline_alerts,variables. One<file>::<Class>.<field>per line, the same shape asknown_api_server_threadpool_offloads.txtin #76424. The qualified name disambiguates the class names the core and execution APIs share, and no entry carries a line number, so an edit above one does not invalidate the list. Each line is a strict xfail, so every pending field is visible in the test report, and migrating one turns into anXPASS(strict)failure naming the exact line to delete. The list can only shrink.generated/*is in.rat-excludes, so the file carries no licence header.LAZY_FIELDS_WITHOUT_SYNC_QUERY— 12 exempt. The items arrive already resolved, so validating them on the loop queries nothing. Eight routes build the response objects themselves —task_coordinate_responsesandserialize_asset_eventsboth return a list of responses and prefetch what building them needs.import_errorspasses ORM rows, but the response reads only their own columns;jobspasses ORM rows too, andjoinedload(Job.dag_model)covers the one relationship it reads throughdag_display_name'sAliasPath.TaskBreadcrumbsResponse.breadcrumbsis a generator, but over a result its route already awaited on the async engine, andHITLDetailResponse.chosen_optionsis a JSON column's value.That set is closed: a new field has to be
list[...], because a route that resolves its items today is one refactor away from not doing so. A test fails if an exempt entry stops being declared lazily, so a dead entry cannot sit there unnoticed. The annotation still permits the hazard in all twelve; no test can see that from the datamodel, which is the limit of this approach.Verification
29 passed, 8 xfailedfor the file, and5775 passed, 30 skippedfor the wholeairflow-core/tests/unit/api_fastapisuite in a single process. The ratchet was exercised in every direction: a new lazy field (plain, bare and aliased) fails the sweep, migrating a pending one fails as a strictXPASS, and converting an exempt one tolist[...]fails the staleness test. Rebasing ontomainmid-review was itself a live test — #76525 had merged, sodagsanddeadlinesturned intoXPASSfailures and their lines came out of the list.The exempt/pending split was checked twice: once by reading every construction site, and once independently, which is how
dag_runswas caught. The second pass also ran the route tests with a probe recording SQL issued during response serialization; the only statements it saw came from the bulkclearDagRunsroute, thedag_runssite in question._read_knownand_build_xfail_paramsare verbatim copies of #76424's helpers. Whichever of the two merges second should move them intodevel-common/src/tests_common/test_utils/.generated/README.mdsays the files there are hook-generated and are not to be edited by hand, which is not true of a ratchet like this one or of #76424's two lists. Worth settling in one place for all of them, rather than in either PR.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Claude Opus 5) following the guidelines
🤖 Generated with Claude Code