fix(scheduled-tasks): POST /cancel alongside DELETE, both idempotent - #877
fix(scheduled-tasks): POST /cancel alongside DELETE, both idempotent#877vansin wants to merge 4 commits into
Conversation
…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.
|
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. |
There was a problem hiding this comment.
💡 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".
| - Embedded `TEST604_SOURCE_COMMIT`: `fa6a07b7dd00314e34f6729221a577194a6a80a0` | ||
| - Green: 12 tests, 106 assertions, 0 failures; restored-green repeated the same result. |
There was a problem hiding this comment.
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 👍 / 👎.
| 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]); |
There was a problem hiding this comment.
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 👍 / 👎.
| run_real "$db_path" >/tmp/test604-red.log 2>&1 | ||
| local rc=$? | ||
| set -e | ||
| if [ "$rc" -eq 0 ]; then |
There was a problem hiding this comment.
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 👍 / 👎.
Fixes production 定时任务「取消」P0 (dispatch 通信狗 08342434). Paired with dashboard PR (link once opened).
Root cause
Production
y.vansin.top:3100/scheduled-tasks— clicking 取消 firesDELETE /api/hub/scheduled-tasks/:idthrough the dashboard proxy. Some reverse proxies in the delivery path rewrite DELETE to a 405 HTML response; the dashboard then calledawait 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/cancelas an alternate spelling for the same cancel operation. POST is universally allowed; DELETE stays for API compatibility.server/src/scheduled-tasks.tsL451):(runs|run-now)→(runs|run-now|cancel)(!sub && DELETE) || (sub === 'cancel' && POST)→ same implementation (status=cancelled,next_run_at=NULL,revision+1,updated_at=now)200 {ok:true, status:'cancelled'}without touching the DB — previously the UPDATE would bumprevisionon every retry, which races with the dashboard'srevision_conflicthandling (retries would 409 themselves).Tests
server/src/scheduled-tasks-http.test.tsgets 3 new assertions inside the existingoptimistic revision, pause/resume, run-now and cancel preserve historytest:POST /cancelreturns 200 withstatus:'cancelled'on a live rowDELETEon already-cancelled = 200 + revision unchanged (idempotent)POST /cancelon already-cancelled = 200 + revision unchanged (idempotent)Baseline on this branch already has 3 pre-existing failures unrelated to cancel (
non-overlap skipsdata ordering +runs.length >= 4count flake in this same test's tail); this PR does not touch those, and adds 7 newexpect()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, andtype="button"on all row actions.