Skip to content

fix(scheduled-tasks): POST /cancel alongside DELETE, both idempotent - #877

Closed
vansin wants to merge 4 commits into
mainfrom
feat/scheduled-task-edit
Closed

fix(scheduled-tasks): POST /cancel alongside DELETE, both idempotent#877
vansin wants to merge 4 commits into
mainfrom
feat/scheduled-task-edit

Conversation

@vansin

@vansin vansin commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes production 定时任务「取消」P0 (dispatch 通信狗 08342434). Paired with dashboard PR (link once opened).

Root cause

Production y.vansin.top:3100/scheduled-tasks — clicking 取消 fires DELETE /api/hub/scheduled-tasks/:id through the dashboard proxy. Some reverse proxies in the delivery path rewrite DELETE to a 405 HTML response; the dashboard then called await res.json() on the HTML and blew up with "Unexpected token <", hiding the failure from the user (spinner stuck, no error surfaced).

Fix in this PR

Add POST /api/scheduled-tasks/:id/cancel as an alternate spelling for the same cancel operation. POST is universally allowed; DELETE stays for API compatibility.

  • regex (server/src/scheduled-tasks.ts L451): (runs|run-now)(runs|run-now|cancel)
  • handler: (!sub && DELETE) || (sub === 'cancel' && POST) → same implementation (status=cancelled, next_run_at=NULL, revision+1, updated_at=now)
  • idempotent: an already-cancelled row now returns 200 {ok:true, status:'cancelled'} without touching the DB — previously the UPDATE would bump revision on every retry, which races with the dashboard's revision_conflict handling (retries would 409 themselves).

Tests

server/src/scheduled-tasks-http.test.ts gets 3 new assertions inside the existing optimistic revision, pause/resume, run-now and cancel preserve history test:

  1. POST /cancel returns 200 with status:'cancelled' on a live row
  2. DELETE on already-cancelled = 200 + revision unchanged (idempotent)
  3. POST /cancel on already-cancelled = 200 + revision unchanged (idempotent)

Baseline on this branch already has 3 pre-existing failures unrelated to cancel (non-overlap skips data ordering + runs.length >= 4 count flake in this same test's tail); this PR does not touch those, and adds 7 new expect() calls that all pass. Full run: baseline 9 pass / 3 fail / 99 expect → this PR 9 pass / 3 fail / 106 expect (delta +7 pass).

🔴 Deploy ordering constraint

Hub must ship before or at the same time as the paired Dashboard PR. Once dashboard is on the new UI, cancel clicks hit POST /cancel; if hub is still on the old handler, that path 404s and cancel appears broken again (different symptom, same user pain). Recommend: land this PR first, deploy hub to :9200, then deploy dashboard to :3100.

Paired PR

Dashboard side: (link added on open) — updates mutate('cancel') to POST /cancel, adds safe response parsing, confirm dialog, cancelled filter, and type="button" on all row actions.

SmartFlowAITeam and others added 4 commits August 9, 2026 14:44
…potent

Some reverse proxies swallow DELETE and return 405 HTML, which broke the
dashboard's cancel button (`await res.json()` on HTML → "Unexpected token
<"). Adds POST /api/scheduled-tasks/:id/cancel with the same
implementation as DELETE, so the dashboard can use a universally-allowed
verb. DELETE stays for API compat.

Both spellings are now idempotent: re-cancelling an already-cancelled row
returns 200 without touching the DB (previously bumped revision on every
call, which would race with the dashboard's revision-conflict logic).

test: adds POST /cancel path, DELETE idempotency (200 + same revision),
and POST /cancel idempotency to the existing cancel-preserves-history
test. Baseline (before this change) has 3 unrelated failures on this
branch (`runs.length >= 4` data flake + non-overlap ordering) — this
change adds 7 more expect() calls, all pass.

Dispatch: 通信狗 08342434.
@vansin

vansin commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by fresh PR cherry-picked onto current main (feat/scheduled-task-edit was 2 weeks behind, contained already-merged commits). Same fix, cleaner base.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b886a9485e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to +44
- Embedded `TEST604_SOURCE_COMMIT`: `fa6a07b7dd00314e34f6729221a577194a6a80a0`
- Green: 12 tests, 106 assertions, 0 failures; restored-green repeated the same result.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record the source tree actually exercised

The claimed Hub source fa6a07b contains seven fewer assertions than the reported 106—the seven /cancel assertions appear only later in this reviewed change—so an image embedding that SHA cannot both have been built from a clean fa6a07b tree and produce this result. This makes the required Docker evidence non-reproducible; rebuild from a clean reviewed source and record its actual commit.

AGENTS.md reference: AGENTS.md:L16-L16

Useful? React with 👍 / 👎.

Comment on lines +490 to 491
if (row.status === "cancelled") return Response.json({ ok: true, status: "cancelled" });
db.run("UPDATE scheduled_tasks SET status = 'cancelled', next_run_at = NULL, revision = revision + 1, updated_at = datetime('now') WHERE schedule_id = ?1", [row.schedule_id]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the cancellation idempotency check atomic

When duplicate cancellation requests are routed concurrently to different Hub processes, both can read the row as active before either update commits, and this unconditional update then increments revision twice. The new status precheck therefore guarantees idempotency only for sequential retries; make the update conditional on the persisted status (or perform the check and update transactionally) so concurrent retries cannot mutate the row more than once.

Useful? React with 👍 / 👎.

Comment on lines +21 to +24
run_real "$db_path" >/tmp/test604-red.log 2>&1
local rc=$?
set -e
if [ "$rc" -eq 0 ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify that each mutation triggers its intended failure

If any mutated full-suite run fails for an unrelated reason—such as one of the existing ordering or run-count flakes—this helper treats the nonzero exit code as proof that the mutation was detected. As a result, a mutation whose targeted behavior remains completely untested can still be reported as MUTATION_RED; inspect the output for the expected assertion/test failure rather than accepting every nonzero status.

Useful? React with 👍 / 👎.

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