Repository navigation
Return 404 from task state store endpoints for unknown task instances - #70983
Conversation
df32c25 to
81fb7f3
Compare
jason810496
left a comment
There was a problem hiding this comment.
Thanks for the fix.
It seems only those route calling the _require_ti helper need those new deps. Perhaps we can introduce different dependency injection helper (e.g. one for mapping the path parameters to the TaskScope another one leverage the first one but also validate the TI).
81fb7f3 to
108780e
Compare
|
Thanks for reviewing. I've addressed all comments. |
jason810496
left a comment
There was a problem hiding this comment.
Thanks! LGTM overall after these comments.
We will need to update the PR description as well, since we don't validate those task_state_store routes now (list_task_state_store, get_task_state_store ).
108780e to
8b82e0d
Compare
|
Thanks for reviewing. I've changed the code to show the By the way, I noticed that list endpoint can never return 404 now that it doesn't validate the task instance, so I dropped its create_openapi_http_exception_doc entry — same as get_pools and get_variables. PR description is updated as well. |
Summary
The task state store is a per-task-instance key/value store that survives worker
crashes and retries within a Dag run — the place operators keep external job IDs,
checkpoints and progress metadata. Its REST API is addressed by four values that
together identify one task instance:
dag_id+dag_run_id+task_id+map_index.The two
DELETEendpoints never checked that the task instance those four valuesname actually exists, so they reported success while touching nothing. All six
endpoints declared
404increate_openapi_http_exception_doc(...)— that doc ismaintained by hand and had drifted from the handlers in both directions, promising a
404the deletes could not raise and one the list endpoint has no way to raiseeither.
_require_ti()was written as a plain helper that each handler had to remember tocall, and it was called from
PUTandPATCHonly. The delete handlers wentstraight to the query, and because those queries filter the four columns on
task_state_storedirectly — they never jointask_instance— a request naming atask instance that doesn't exist simply matches zero rows. At the SQL level that is
indistinguishable from "this task instance exists but has no state stored", so the
delete succeeded.
What that looked like
A typo in
task_idmade a delete report success while touching nothing:Clearing state store is typically a pre-rerun cleanup step — clearing a task
instance in the UI deliberately does not wipe its state store, so a stale
checkpoint survives into the next attempt. An operator wiping a bad external job ID
before re-running gets
204, believes the state is gone, re-runs, and the taskresumes from exactly the state they thought they had removed. Nothing in the
response hints that the
task_idwas wrong.The same resource also answered the same question two different ways depending on
the verb — a write refused, the matching delete claimed success:
Before / after
DELETE .../state-store/{key}204404DELETE .../state-store204404PUT .../state-store/{key}404PATCH .../state-store/{key}404GET .../state-store200+ empty collection404)GET .../state-store/{key}404, message names the keyA missing key on an existing task instance still reports the key, unchanged:
Task state store key 'job_id' not found.The reads are deliberately left alone (per review): an empty collection and a
"key not found" are already honest answers, and they would otherwise pay for an
extra
task_instancelookup on every read. Since the list endpoint therefore hasno
404path at all —requires_access_dagonly raises401/403, andpaginated_selectraises nothing here — itscreate_openapi_http_exception_docentry is dropped, matching the other list endpoints (
get_pools,get_variables,…) that declare no error responses.
The fix
asset_state_store.py— the sibling module from the same feature — already solvesthis correctly:
_get_asset_or_404is wired up asAssetIdDep = Annotated[int, Depends(...)]and every one of its endpoints takesasset_id: AssetIdDep, so thecheck rides on the parameter and cannot be forgotten when an endpoint is added.
This applies the same pattern on the task side, split in two so the reads do not
pay for a check they don't need:
TaskScopeDepmaps the path and query parameters onto theTaskScopethehandlers already needed, absorbing the old
_get_scope()helper. Every endpointtakes it, so each handler drops from four path/query parameters to a single
scope.ValidatedTaskScopeDepbuilds on it and adds the existence check. Only thewrites and deletes take it.
Why
clearneeds its own dependencyDELETE .../state-store?all_map_indices=truemeans "clear this task at every mapindex", and documents that
map_indexis ignored.ValidatedTaskScopeDepvalidatesone exact map index, defaulting to
-1— and an expanded mapped task has nomap_index=-1instance:TaskMap.expand_mapped_taskeither converts that row intomap_index=0or deletes it. Validating withValidatedTaskScopeDepwould thereforereject a perfectly valid request:
A dependency only receives the parameters it declares, so
ValidatedTaskScopeDepcannot see
all_map_indicesand can only ever take the strict path.ValidatedClearTaskScopeDepdeclares the flag, and when it is set drops themap_indexpredicate so any instance of that task in the run satisfies the check.The 404 it raises names the flag rather than a
map_indexthat was never applied:The strict path is untouched:
?map_index=7on a task expanded to 0..2 stillreturns
404.Generated files
v2-rest-api-generated.yamlandui/openapi-gen/are regenerated. Two kinds ofchange: the
404removed fromGET .../state-store, and parameter ordering —moving path/query parameters behind a dependency changes the order FastAPI reports
them in, not the parameters themselves.
Tests
New
TestUnknownTaskInstancecovers every request shape against both a badtask_idand a badrun_id, a non-existentmap_index, theall_map_indiceserror detail, and regression tests that the reads still answer as they did — the
list returns an empty collection, and a genuinely missing key still reports the key
rather than the task instance.
76 → 96 tests in this file;
test_asset_state_store.pypasses unchanged.Was generative AI tooling used to co-author this PR?