Dev-iL commented on code in PR #73966:
URL: https://github.com/apache/airflow/pull/73966#discussion_r4157153992


##########
airflow-core/src/airflow/api_fastapi/execution_api/routes/task_instances.py:
##########
@@ -1336,10 +1336,10 @@ def get_task_instance_states(
     if map_index is not None:
         query = query.where(TI.map_index == map_index)
 
-    results = session.scalars(query).all()
+    results = (await session.scalars(query)).all()

Review Comment:
   Thanks! I confirmed the fix: the new test fails with the query change 
reverted (it lists the `task1` and `group1.add_one` entities) and passes 
otherwise, and `TestGetTaskStates` + `TestGetCount` pass on SQLite, PostgreSQL 
and MySQL.
   
   One more step before this is ready, though. With 5,001 states the route 
still blocks the event loop for longer than the old sync handler did (median 
~30–40 ms vs ~10–14 ms). Profiling the loop showed it's no longer the query: 
most of it is building the response row by row (`Row` attribute access plus a 
one-item `dict.update` per row), plus ORM result processing in one piece. 
Unpacking tuples and streaming the result in partitions brings it well below 
the sync baseline, on SQLite and PostgreSQL:
   
   ```diff
   -    results = (await session.execute(query)).all()
   +    def add_states(rows) -> None:
   +        for run_id, task_id, ti_map_index, state in rows:
   +            key = task_id if ti_map_index < 0 else 
f"{task_id}_{ti_map_index}"
   +            run_id_task_state_map[run_id][key] = state
   
   -    if task_group_id:
   -        group_tasks = await _get_group_tasks(
   -            dag_id, task_group_id, session, dag_bag, logical_dates, 
run_ids, map_index
   -        )
   -
   -        results = results + group_tasks if task_ids else group_tasks
   +    # Without task_ids, task_group_id replaces the dag-wide match instead 
of extending it.
   +    if task_ids or not task_group_id:
   +        streamed = await 
session.stream(query.execution_options(yield_per=500))
   +        async for partition in streamed.tuples().partitions():
   +            add_states(partition)
   
   -    [
   -        run_id_task_state_map[task.run_id].update(
   -            {task.task_id: task.state}
   -            if task.map_index < 0
   -            else {f"{task.task_id}_{task.map_index}": task.state}
   +    if task_group_id:
   +        add_states(
   +            await _get_group_tasks(dag_id, task_group_id, session, dag_bag, 
logical_dates, run_ids, map_index)
            )
   -        for task in results
   -    ]
   ```
   
   It also skips the dag-wide query when `task_group_id` is given without 
`task_ids`, since that result was discarded anyway. Your tests pass unchanged 
on all three backends with this applied.
   
   <details><summary>Measurements</summary>
   
   Warm requests to `/execution/task-instances/states`, one run, 10 requests 
per size with the first dropped, ticker on the app's event loop:
   
   | Backend | States | | Sync (base) | This PR | With the change above |
   |---|---:|---|---:|---:|---:|
   | SQLite | 1,001 | median / max stall | 6.3–6.7 / 313–337 ms | 10–12 / 13–16 
ms | 2.7 / 3.3–4.0 ms |
   | SQLite | 5,001 | median / max stall | 8.7–10.4 / 305–359 ms | 27–46 / 
46–55 ms | 3.3–6.2 / 7.2–8.2 ms |
   | PostgreSQL 14 (psycopg) | 5,001 | median / max stall | 13.8 / 424 ms | 
39.3 / 46.6 ms | 5.6 / 7.8 ms |
   | PostgreSQL 14 (psycopg) | 5,001 | request time | 632 ms | 54 ms | 52 ms |
   
   Small local samples, so treat them as event-loop availability rather than 
throughput. I didn't measure MySQL timings or test PgBouncer; streaming opens a 
server-side cursor inside the request's transaction.
   
   </details>



-- 
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