ColtenOuO opened a new pull request, #70983:
URL: https://github.com/apache/airflow/pull/70983

   ## Sumarry
   
   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`.
   
   Only two of the six endpoints ever checked that the task instance those four 
values
   name actually exists.
   
   `_require_ti()` was written as a plain helper that each handler had to 
remember to
   call, and it was called from `PUT` and `PATCH` only. The read and delete 
handlers
   went straight to the query, and because those queries filter on the four 
columns
   directly — they never join `task_instance` — a request naming a task 
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 API 
answered with
   the latter.
   
   All six endpoints declare `404` in `create_openapi_http_exception_doc(...)`, 
so for
   three of them the spec described a status the handler could never return.
   
   ## What that looked like
   
   A typo in `task_id` was silently successful:
   
   ```console
   $ curl .../taskInstances/proces/state-store          # note the missing 's'
   200 {"task_state_store": [], "total_entries": 0}
   ```
   
   The caller cannot tell "this task has no state" from "this task does not 
exist".
   
   Worse, a delete reported success while touching nothing:
   
   ```console
   $ curl -X DELETE .../taskInstances/proces/state-store
   204 No Content
   ```
   
   That is the damaging one. 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 task resumes from exactly the state they thought they had 
removed.
   Nothing in the response hints that the `task_id` was wrong.
   
   The same resource also answered the same question three different ways 
depending on
   the verb:
   
   ```console
   $ curl -X PUT    .../taskInstances/proces/state-store/job_id -d 
'{"value":"x"}'
   404 {"detail": "Task instance not found for dag_id='etl', ..., 
task_id='proces', map_index=-1"}
   
   $ curl          .../taskInstances/proces/state-store/job_id
   404 {"detail": "Task state store key 'job_id' not found"}     # right 
status, wrong reason
   
   $ curl -X DELETE .../taskInstances/proces/state-store/job_id
   204 No Content
   ```
   
   The `GET` case is the most misleading: it happens to return `404`, but 
blames the
   key, sending the reader off to check their key name when the real problem is 
the
   task ID.
   
   ## Before / after
   
   | Endpoint | Before | After |
   | --- | --- | --- |
   | `GET .../state-store` | `200` + empty collection | `404` |
   | `GET .../state-store/{key}` | `404`, message blames the key | `404`, 
message names the task instance |
   | `PUT .../state-store/{key}` | `404` | unchanged |
   | `PATCH .../state-store/{key}` | `404` | unchanged |
   | `DELETE .../state-store/{key}` | `204` | `404` |
   | `DELETE .../state-store` | `204` | `404` |
   
   A missing *key* on an existing task instance still reports the key, 
unchanged:
   `Task state store key 'job_id' not found`.
   
   ## The fix
   
   `asset_state_store.py` — the sibling module from the same feature — already 
solves
   this correctly: `_get_asset_or_404` is wired up as `AssetIdDep = 
Annotated[int,
   Depends(...)]` and **every** one of its endpoints takes `asset_id: 
AssetIdDep`, so
   the check rides on the parameter and cannot be forgotten when an endpoint is 
added.
   
   This applies the same pattern on the task side. `_require_ti` becomes a 
dependency
   returning the `TaskScope` the handlers already needed, which also absorbs 
the old
   `_get_scope()` helper — each handler drops from four path/query parameters 
plus two
   helper calls to a single `scope`. The diff removes more lines from the 
handlers than
   it adds.
   
   ### Why `clear` needs its own dependency
   
   `DELETE .../state-store?all_map_indices=true` means "clear this task at 
every map
   index", and documents that `map_index` is ignored. `TaskScopeDep` validates 
one
   *exact* map index, defaulting to `-1` — and an expanded mapped task has no
   `map_index=-1` instance: `TaskMap.expand_mapped_task` either converts that 
row into
   `map_index=0` or deletes it. Validating with `TaskScopeDep` would therefore 
reject a
   perfectly valid request:
   
   ```console
   # 'process' expanded to map_index 0, 1, 2 — there is no map_index=-1 row
   $ curl -X DELETE .../taskInstances/process/state-store?all_map_indices=true
   404 Task instance not found ... map_index=-1     # wrong
   ```
   
   A dependency only receives the parameters it declares, so `TaskScopeDep` 
cannot see
   `all_map_indices` and can only ever take the strict path. `ClearTaskScopeDep`
   declares the flag, and passes `map_index=None` when it is set, which drops 
the
   `map_index` predicate and matches any instance of that task in the run. The 
strict
   path is untouched: `?map_index=7` on a task expanded to 0..2 still returns 
`404`.
   
   ## Generated files
   
   `v2-rest-api-generated.yaml` and `ui/openapi-gen/` are regenerated.
   
   ## Tests
   
   New `TestUnknownTaskInstance` covers all seven request shapes against both a 
bad
   `task_id` and a bad `run_id`, a non-existent `map_index`, and a regression 
test that
   a genuinely missing key still reports the key rather than the task instance.
   
   `test_all_map_indices_clears_across_mapped_instances` inserted state rows at 
map
   indices that have no task instance and then read them back through `GET`. It 
now
   asserts on the rows directly — the test is about `clear`, not about `get`, 
and the
   read endpoints legitimately reject a map index with no task instance.
   
   76 → 97 tests in this file; `test_asset_state_store.py` passes unchanged.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes — Claude Code (Opus 5)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to