Check every statement in the webhook deletion transaction (closes #262) #273
Reference in New Issue
Block a user
Delete Branch "issue-262-delete-transaction"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #262
The defect
deleteWebhookResourcesissued three deletes without checking any of them. A failing delete sets.Erroron the returned session but leaves the transaction usable, so the handler committed whatever succeeded, redirected to/sourcesas though deletion worked, and then hard-deleted the per-webhook event database anyway (its own error logged and swallowed). The result was a webhook whose entrypoints or targets survived in the main database while its entire event history was permanently gone — reported to the operator as success.The change
commitWebhookDeletion, which checks each statement andtx.Rollback()s on any failure, matchingcommitWebhooktwenty lines above rather than inventing a new idiom.deleteWebhookResourcesreports the error withh.serverError— the failure convention already used throughout this file — and returns without touching the event database.The ordering decision (config commit vs.
DeleteDB)No transaction spans SQLite and the filesystem, so one side has to go first and a window exists either way. The configuration commit goes first, and the event database is only removed after it succeeds.
DeleteDBafter the commit: the webhook is gone and an unreferenced event database file is left on disk.That is the side chosen to fail on because a leftover file can be removed by hand while deleted history cannot be recovered. It is not reported as success:
DeleteDBfailure now returnsh.serverErrorinstead of the previous redirect, and the error names the file path (DeleteDBwraps it asdeleting webhook database file %s: %w), so the operator has what they need to clean up. A retry of the delete returns 404, since the configuration really is gone — the logged error is the record of what is left behind.Audit of the same pattern
The repo has exactly three
Begin()sites (grep -rn '\.Begin()' internal/), and this was the only defective one:commitWebhook(internal/handlers/source_management.go) — checks each statement, rolls back. Known good.createAndFanOut(internal/handlers/webhook.go) — checkstx.Create(event)andbuildDeliveryTasks(which checks eachtx.Create(dlv)), rolls back on either. Safe.Also checked every other write in
internal/handlers(.Create/.Save/.Updates/.Delete): all single-statement, all with.Errorchecked, includingdeleteChildResource, which is the target/entrypoint deletion path. Nothing else to fix.Verification
make checkgreen withGOFLAGS=-count=1— all packages ran (no(cached)lines), lint executed in Docker for 47s and reported0 issues.Two new tests in
internal/handlers/source_delete_test.go:TestHandleSourceDelete_FailedDeleteKeepsEverything— injects a failure into thetargetsdelete via a GORM before-delete callback, which reproduces the exact condition in the report:.Errorset on the statement with the surrounding transaction still usable. Asserts 500 with noLocationheader, and that the webhook, the entrypoint (deleted by the statement before the failing one, so this is the rollback proof), the target and the event database file all survive.TestHandleSourceDelete_RemovesConfigAndEventDatabase— positive control: an ordinary deletion still returns 303 to/sourcesand removes the webhook, entrypoint, target and event database file.Fail-first, with only the handler reverted, the failure test fails on every assertion — including
unable to find file .../events-<id>.db, which is the data loss itself:End-to-end against a running instance (
PORT=19140, ownDATA_DIR), normal deletion still works:with the webhook, its default entrypoint and a
databasetarget created through the web UI first, and the log showingwebhook deletedfollowed bydeleted per-webhook database.PASS — verified by execution with an independent harness, not by reading the author's tests.
Injection at each of the three deletes independently (
entrypoints,targets,webhooks): on the parent commit all three reproduce the loss (303 to/sources, partial config committed,events-<id>.dbdestroyed); on this head all three give 500, noLocation, and webhook + entrypoint + target + event database all intact. Positive control (normal delete) unchanged.DeleteDBfailure after the commit returns 500 with the full file path in the logged error. Ordering decision ruled correct: the post-commit window leaves only a recoverable file, retry is a 404 as documented, and a stale event database can never be adopted by a new webhook (UUIDv4 IDs and the soft-deletedwebhooksrow keeps the primary key occupied). Audit reconfirmed independently: threeBegin()sites, no.Transaction(sites, every other write ininternal/handlerssingle-statement with.Errorchecked.make checkgreen from a clean clone withGOFLAGS=-count=1in 95s (20 packages, zero(cached); lint in Docker 61.7s,0 issues.); CI green on27d4f1c; merges cleanly intonext.Disclosure: the per-injection-point probes were run as scoped
go test -runin throwaway copies of the tree rather than throughmake; the reviewed tree itself was only ever exercised viamake bootstrap/make check/make fmt-checkand is unmodified.