Skip to content

fix(resource): idempotent DELETE — no double-deprovision (bug-bash #4/#12)#229

Merged
mastermanas805 merged 2 commits into
masterfrom
fix/resource-delete-idempotent-2026-06-04
Jun 3, 2026
Merged

fix(resource): idempotent DELETE — no double-deprovision (bug-bash #4/#12)#229
mastermanas805 merged 2 commits into
masterfrom
fix/resource-delete-idempotent-2026-06-04

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

What

A repeated DELETE /resources/:token (retry, double-click, concurrent request) re-ran SoftDeleteResource and re-issued the backend deprovision a second time. The second teardown races the first and can error against an already-gone backend — surfacing a spurious 5xx for what is logically a success.

Fix

Idempotent early-return after the team-ownership check: when the resource is already status='deleted', report success and do nothing.

if resource.Status == "deleted" {
    return c.JSON(fiber.Map{"ok": true, "already_deleted": true, "id": resource.ID.String()})
}

The first DELETE already tore the backend down; the ownership/404 posture is preserved (a resource you don't own still 404s before this check).

Coverage

Symptom:        repeated DELETE re-deprovisions backend / spurious 5xx on retry
Enumeration:    rg -F 'SoftDeleteResource' + resource.Delete handler
Sites found:    1 (single Delete handler)
Sites touched:  1
Coverage test:  TestResourceDelete_Idempotent_DoubleDelete
Live verified:  passes vs real Postgres+Redis locally; runs in CI build-and-test

First DELETE → 200; second DELETE → 200 with already_deleted=true.

🤖 Generated with Claude Code

@mastermanas805 mastermanas805 enabled auto-merge (squash) June 3, 2026 18:39
@mastermanas805 mastermanas805 force-pushed the fix/resource-delete-idempotent-2026-06-04 branch from 62bd730 to b968b1e Compare June 3, 2026 18:54
…12)

A repeated DELETE on a resource (retry, double-click, or concurrent
request) re-ran SoftDeleteResource and re-issued the backend deprovision
a second time. The second teardown races the first and can error against
an already-gone backend, surfacing a spurious 5xx for what is logically
a success.

Add an idempotent early-return after the team-ownership check: when the
resource is already status='deleted', report success
({"ok":true,"already_deleted":true,"id":...}) and do nothing. The first
DELETE already tore the backend down.

Test: TestResourceDelete_Idempotent_DoubleDelete — first DELETE → 200,
second DELETE → 200 with already_deleted=true. Verified against real
Postgres+Redis locally; runs in CI build-and-test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 force-pushed the fix/resource-delete-idempotent-2026-06-04 branch from b968b1e to b0061db Compare June 3, 2026 19:12
@mastermanas805 mastermanas805 merged commit 893bed9 into master Jun 3, 2026
18 checks passed
mastermanas805 added a commit that referenced this pull request Jun 3, 2026
…e-deprovision TOCTOU (#236)

Design-review follow-up to #229. The idempotent-DELETE fix used a
`resource.Status == "deleted"` PRE-READ before SoftDeleteResource +
deprovision — a check-then-act TOCTOU: two concurrent DELETEs both read
status='active', both pass the pre-read, and BOTH fire the destructive
provisioner Deprovision / storage Deprovision RPC against the same backend.
That is the truehomie-db DROP incident class (an active customer DB dropped
via a path that re-ran teardown).

Fix: make the soft-delete the authoritative atomic guard. New
models.SoftDeleteResourceIfActive does `UPDATE ... SET status='deleted'
WHERE id=$1 AND status != 'deleted'` and returns RowsAffected>0. The handler
deprovisions ONLY when this call won the transition (deleted==true); any
racer or sequential retry gets deleted==false → idempotent
{"ok":true,"already_deleted":true} with NO second deprovision. Removes the
redundant pre-read (the rows-affected check subsumes it and is race-safe).
SoftDeleteResource is kept unchanged for the ~18 provision-rollback callers
that always operate on a just-created row.

Tests: models.TestSoftDeleteResourceIfActive_Branches (sqlmock: won / no-op /
error — 100% of the new fn). The existing TestResourceDelete_Idempotent_
DoubleDelete now exercises the rows-affected !deleted branch via a sequential
retry (200 already_deleted, no re-deprovision). All changed handler lines
covered. Verified vs real Postgres+Redis.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant