Repository navigation
fix(firmware): delete a version whose update rolled back - #331
Conversation
A rolled-back or failed attempt parks the knob's target on that version, and otaTargets counted the parked target as in use, so DELETE answered 409 and the user had no way out (#330). - otaTargets skips a target parked after its version failed or rolled back. Retention still keeps parked targets. - DELETE resets the knob's record for that attempt (target, version, phase, error; mode, blocked and the attempt counter stay) so no target or Retry points at a missing image, and auto mode is not held behind a dangling target. - A checkin drops a target whose image is gone outside an active phase, for a Retry racing a DELETE. - PUT target:null also dismisses a failed or rolled-back attempt (phase idle, error cleared, the version stays blocked), so the app can offer Dismiss.
- A refused delete shows under the image's row instead of the section footer; a 409 says a knob is set to update to that version and how to free it. - The failure line gets Dismiss next to Retry, and a waiting target gets Cancel Update; both send PUT target:null through KnobOTAModel.cancel(), which now reports under its own action.
Codex reviewCodex traced the code paths and lock ordering but didn't run the tests. Posted on its behalf.
|
Opus reviewNo blocking findings. The state machine holds up for the cases in #330. Below, findings ranked by severity, then answers to the hunt questions. Findings1. Medium-low: retention eviction still leaves the stale state this PR fixes for DELETE. 2. Low: the 409 copy promises a Cancel that isn't always there. "A knob is set to update to this version. Cancel that update in the Firmware row, or wait until it has finished…" is also shown when the 409 comes from an auto offer: 3. Low: Cancel Update races a download, without stranding the knob. In 4. Low (wording/docs): "won't offer this version again" is not always true.
A manual 5. Nit (tests): one mutation survives. Removing Hunt questions
Verification
|
Review of #331 found races where a checkin or a PUT acted on a store read that a DELETE or an eviction had already overtaken: a Retry revived a deleted parked version, Dismiss then DELETE left a retryable missing image, eviction left the same stale record, and a checkin's stale absence could clear a target re-selected after a re-upload. - The registry keeps an in-memory firmware generation. Every removal bumps it in the same locked step as its in-use check (otaTargets for DELETE and replace, otaKeeps per evicted version), under the store lock. - A checkin reads the generation and epoch before the store and offers or clears only if the generation, target, mode and attempt are unchanged; the dangling clear also needs an unchanged epoch. - PUT target/retry checks the image of the version it would set and commits only if the generation and that version are unchanged, retrying up to 3 times, then 409 firmware_changed. A retry of a version no longer stored is 400. - DELETE and eviction (not replace) retire the version on every knob outside an active attempt: a target naming it is cleared, a non-done record of it is reset. - Clearing a target while downloading is 409 ota_in_progress. - otaReadHook is a test seam between the store read and the registry update.
The 409 also comes from auto offers and downloads, which Cancel Update can't withdraw, and Dismiss doesn't always leave the version blocked. Map the new firmware_changed conflict to a plain retry hint.
Review fixes (7be310e, cc1b415)The fix: an in-memory firmware generation (
Why an offer for a removed version can't happen now. A check-in that reads the store before the claim either commits before it, which makes the version active so the claim answers 409, or commits after it and sees the The app maps Checks: |
Opus re-checkHead: 7be310e (server) and cc1b415 (app). I worked read-only and ran the repros in a scratch copy, which is now deleted. Verdict: approve. No blockers. Every Codex and Opus finding is fixed. The Findings, verifiedI reproduced each one with
Hunt results
Low and nits (none blocking)
Checks
|
…fers An auto checkin that read the store before a DELETE or an eviction claimed its candidate, and commits while the purge runs, must not offer it. These tests kill the mutations that drop the checkin's fwGen compare or the fwGen bump in otaTargets or otaKeeps. Docs: every PUT goes through the compare, and the lock order names otaRetire. App: a refused Cancel Update says the knob has already started the update.
|
Re-check follow-up (tests, docs and copy only; no server logic changes):
|
Closes #330
Why
After a rollback the knob's target stays parked on the failed version.
otaTargetscounted it as in use, soDELETE /v1/firmware/{v}returned 409. The app showed the error only in the section footer, and it had no way to clear the target.Server
otaTargetsskips a target parked after its version failed or rolled back (version == target, phasefailed/rolled_back). Active offers and pending targets still return 409. Retention (otaKeeps) still keeps parked targets.otaForget, which prunesblockedand resets a knob whose last attempt of that version failed:target,retry,version,phaseanderrorare cleared.mode,blockedand theattemptcounter are kept. Clearing the target bumps the epoch. Without this, the target would point at a missing image: no offer, auto mode stuck behind it, and Retry left at "waiting for check-in" for good.PUT …/ota {"target":null}also dismisses a failed or rolled-back attempt: phase becomesidle,erroris cleared and the version stays inblocked. This works with or without a target.App
KnobOTAModel.cancel()(PUT target:null), which now reports errors under its own.cancelaction ("Couldn't clear the update: …").scripts/strings.sh sync, thencheckpasses).Docs
API.md, openapi.yaml (Redocly lint passes) and ARCHITECTURE "Knob firmware updates".
Evidence
go test -race ./...passes, plusgo vetandgofmt. New tests: delete of a parked target (rolled_back and failed) returns 204 and clears the state, after which no offer is made and Retry returns 400; auto mode offers again after the delete; a pending target still returns 409; an active offer returns 409; retention keeps a parked target; check-in clears a dangling target; Dismiss works with and without a target. The first three fail without the fix.swift test --package-path macospasses (963 tests). New tests cover the per-row delete error and the 409 mapping,cancel()under.cancel, andcanCancel.