Skip to content

feat: add a status view for task polling and honor limit on GET /tasks - #227

Merged
kaiitunnz merged 10 commits into
mainfrom
fix/225-task-status-view
Oct 17, 2026
Merged

kaiitunnz merged 10 commits into
mainfrom
fix/225-task-status-view

Conversation

@tjluyao

@tjluyao tjluyao commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Relates to #225. Status pollers overload the server:

  • Every poll returns the whole task. GET /api/v1/tasks/{id} returns the task's spec plus its YAML source, ~230 KB each for a Lumilake inference task. A poller that only reads status pulls ~0.5–1 MB per call.
  • GET /api/v1/tasks ignored limit. It was treated as an unknown filter key, so limit=100 returned every task: 240+ MB on the home cluster on 2026-10-12. That was the day heavy status polling preceded refused connections and 502s.

With this PR, a poller can ask for a task's status view, which holds no spec or source. limit keeps the newest matching tasks, and the SDK and CLI pollers use the status view.

Changes

  • Status view. ?view=status on GET /tasks/{id} and GET /tasks returns a TaskStatusView (src/server/schemas/task.py). It holds status, placement, timing, errors and the task's latest update (SSH/serve info, private keys stripped). Each field has the type, description and requiredness of its TaskInfo counterpart. The default view stays full.
  • Listing order and limit. GET /tasks lists tasks oldest submission first. limit (1–10000) keeps the newest matching tasks, applied after filtering and permissions. Neither view nor limit is treated as a field filter.
  • SDK.
    • tasks.status(id) and tasks.list_statuses(...) return status views, sync and async.
    • tasks.list(limit=...).
    • tasks.wait_status() polls status() until the task is terminal and returns its status view, sync and async.
    • tasks.wait() waits the same way and then fetches the full task once; its return type is unchanged.
    • tasks.ssh_info() and tasks.wait_for_ssh() poll the status view.
  • CLI. flowmesh task watch, flowmesh task stop and flowmesh ssh poll the status view. task watch still prints the full task at the end; task stop and ssh never fetch it.
  • Dependencies. mcp 1.28.1 → 1.30.0 (worker CPU runtime) for CVE-2026-59951, which fails pip-audit on main too.
  • Documentation. docs/API.md and docs/SDK.md cover view, limit, tasks.status(), tasks.list_statuses() and tasks.wait_status(). The cursor-pagination notes in docs/API.md, docs/SDK.md and docs/ARCHITECTURE.md cover log queries only, the routes that take before/after, and describe a cursor as the opaque entries[].cursor string.
  • Tests. Router, SDK request-building, SDK model, schema parity and CLI tests.

Design

The status view is opt-in, because the SDK's TaskInfo requires source and task, and making the slim view the default would break every released SDK. An older server ignores view and returns the full task, which validates as a status view, so the new SDK calls work against any server.

limit returns the newest tasks, ordered by submission time and then id. That matches the log routes, where limit without a cursor returns the newest entries. Without cursors, a lower cap would leave no way to fetch more than the cap, so limit caps at 10000, as the log routes do.

kaiitunnz#64 adds cursor pages for /tasks and /workflows, but as a breaking {entries, next_cursor, prev_cursor} response. This PR adopts its listing order and its newest-first limit. A port of the cursor pages belongs in its own PR.

These parts of #225 are left for later:

  • The default GET /tasks still returns full tasks, including both source and its deprecated raw_yaml copy.
  • A batch status endpoint (POST /tasks/status).

The load drops once Lumilake's FlowMeshRuntimeManager.fetch_task_status, which calls fm.tasks.retrieve() and reads only status, error and last_error, switches to fm.tasks.status() after this SDK is released.

Test Plan

uv run pre-commit run --all-files
uvx bandit==1.9.4 -c pyproject.toml -r src/
uv sync --all-packages --group ci --frozen
uv run pytest tests/ --ignore=tests/worker/test_mp_executor_cleanup_gpu.py
grep -v '@ git+' src/worker/requirements/requirements.txt > /tmp/req-cpu.txt
uvx pip-audit==2.10.1 --strict --ignore-vuln PYSEC-2026-3740 --ignore-vuln PYSEC-2026-3804 -r /tmp/req-cpu.txt

Test Result

  • uv run pre-commit run --all-files: passed (gitleaks, isort, black, ruff, codespell, mypy, and requirements sync).
  • bandit: no issues.
  • uv run pytest tests/ --ignore=tests/worker/test_mp_executor_cleanup_gpu.py: 3098 passed, 21 skipped.
  • pip-audit (worker CPU): no known vulnerabilities, 2 ignored.
  • New tests cover:
    • the status view returning exactly its declared fields, equal to the full view's, with private latest_update keys stripped;
    • each view field matching the type and description of its TaskInfo counterpart;
    • a bad view being a 422 on both routes, and limit=0 and limit=10001 being 422s;
    • the list ordering by submission, with limit keeping the newest;
    • status(), list_statuses() and wait_for_ssh() requesting view=status, sync and async;
    • list(limit=) sending limit;
    • wait() polling the status view and then fetching the full task once;
    • wait_status() returning the terminal status view with no extra fetch, sync and async;
    • the SDK status view parsing both a status-view payload and a full task;
    • flowmesh task watch polling the status view and printing the full task once.
  • End to end, on an isolated stack with two CPU workers, echo_local.yaml tasks and an ssh_debug_shell.yaml session were checked over REST, the SDK and the CLI, and all checks passed:
    • REST: the status view returns exactly its fields with values equal to the full task's, at about a fifth of its size. GET /tasks lists in submission order, limit keeps the newest with filters still applied, and bad view and limit values return 422.
    • SDK: wait_status(), wait(), status(), list_statuses() and wait_for_ssh() return the right types and values, sync and async. The server's access log shows the waits polling ?view=status and no full fetch of the SSH task. The session's latest_update.ssh comes through the status view unchanged.
    • CLI: flowmesh task watch made one full fetch at the end. flowmesh task stop stopped the SSH session to DONE using only status-view polls.

Pre-submission Checklist
  • I have read the contribution guidelines.
  • I have run pre-commit run --all-files and fixed any issues.
  • I have added or updated tests covering my changes (if applicable).
  • I have verified that uv run pytest tests/ passes locally.
  • If I changed shared schemas or proto definitions, I have checked downstream compatibility across Server and Worker.
  • If I changed the SDK or CLI, I have verified the affected packages work (uv sync --all-packages --group ci --frozen).
  • If this is a breaking change, I have prefixed the PR title with [BREAKING] and described migration steps above.
  • I have updated documentation or config examples if user-facing behavior changed.

@tjluyao
tjluyao requested a review from kaiitunnz as a code owner October 12, 2026 13:05
@kaiitunnz kaiitunnz changed the title feat(tasks): ?view=status polling view + honour limit on GET /tasks (#225) feat: add a status view for task polling and honor limit on GET /tasks Oct 17, 2026
@kaiitunnz
kaiitunnz force-pushed the fix/225-task-status-view branch from 7285b7c to 710ae20 Compare October 17, 2026 04:36
@kaiitunnz
kaiitunnz requested a review from timzsu as a code owner October 17, 2026 04:36
Yao Lu and others added 8 commits October 17, 2026 05:42
)

GET /api/v1/tasks/{id} returns the full task: spec plus its YAML source,
~230 KB each for a Lumilake inference task, so a poller reading one status
field pulled ~0.5-1 MB per call. GET /api/v1/tasks ignored `limit` (unknown
key -> no-op), so `limit=100` returned every task: 240+ MB on the home
cluster on 2026-10-12, the day status polling under load preceded refused
connections and 502s.

Server:
- `?view=status` on GET /tasks/{id} and GET /tasks returns TaskStatusView:
  status, placement, timing, errors. Default stays `full` (compatible).
- GET /tasks honours `limit` (after filtering). view/limit are not treated
  as model-field filters.

SDK:
- tasks.status(id) -> TaskStatusView (sync + async). Works against older
  servers too: they ignore ?view and the slim model ignores extra fields.
- tasks.wait() polls status() and fetches the full task once at the end,
  so its return type is unchanged.
- tasks.list(limit=...).

Tests: router (status view omits spec/source and matches full on shared
keys; limit; bad view -> 422) and SDK/server parity for TaskStatusView.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
…y name

Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
@kaiitunnz
kaiitunnz force-pushed the fix/225-task-status-view branch from 710ae20 to 950e6d5 Compare October 17, 2026 05:43
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>

@kaiitunnz kaiitunnz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@kaiitunnz
kaiitunnz merged commit b31463e into main Oct 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants