Repository navigation
fix(merchant): degraded mode retries wallet init (bounded backoff) and surfaces the honest degrade reason - #1078
Conversation
|
Review — verdict: BLOCKERS(1) + CONCERNS(1); disposition: request-changes The honest-reason half of this PR is good and well-tested: the degrade reason is set by both of Blocker — every "failed" wallet retry actually succeeds with a fresh degraded instance, cascading forever at the initial interval.
Reproduced with a never-healing reachable mint (scratch test, test-shrunk constants Suggested fix (either closes it): make the wallet-fail branch of Concern — failed attempts of the transient classes kill the loop after one try.
Notes, not concerns: the evidence (all at head fc40332, worktree)— agent evidence pass (review-debt wave R1); not an approval. |
c03rad0r
left a comment
There was a problem hiding this comment.
Technical review — head fc40332 (content approved; branch needs a rebase before it can merge).
This fixes a real stuck-state: the first-reachable trigger was a one-shot latch, so a wallet that failed to construct stayed degraded for life even with every mint reachable — and every stub then blamed "no reachable mints" while the probes said 200. Carrying an explicit degrade reason through both branches and surfacing it on every stub is the honest-reasoning half of the storage-degraded family, and the bounded retry (30 s → 5 min, single-flighted through AttemptUpgrade's mutex, skipped while the reachable set is empty) is the right shape. Guarding onUpgrade against the retry/callback race is a genuine -race find, not cleanup.
Verified at this head:
cd src/merchant && go test -tags testenv -count=1 -run 'Degraded|WalletRetry|Reason' ./...
ok github.com/OpenTollGate/tollgate-module-basic-go/src/merchant 221.853s
The fund-safety statement is accurate: construction only, no monetary operation reordered, and a failed attempt leaves the merchant serving exactly as before.
The only reason this is not "merge now": the branch currently conflicts with main (CONFLICTING/DIRTY). Rebase it (the merchant files moved under #1108/#1117's storage-verdict work) and I will flip to an approval. The rebase should keep #1108's walletDirFor seam if #1080 lands first — walletDirFor is the same function this branch calls for construction, so order does not matter as long as the branch does not reintroduce filepath.Dir(...) at those sites.
Disposition: approve content; rebase required (I will re-approve on the rebased head).
…est reason (#1078) Rebuilt on current main: keeps walletInitErr (storage class) and adds the degradeReason plus the bounded retry loop. The upgrade hand-off keeps both the retry stop/upgraded transition and the no-consumer Shutdown fallback. WalletNotInitializedError calls become the reason-aware notInitializedError.
|
Rebased and hand-merged. Head Main's storage-verdict work and this PR's retry machinery are now combined:
Verified: |
fc40332 to
f970bc9
Compare
Local CI gate — PR #1078: fix(merchant): degraded mode retries wallet init (bounded backoff) and surfaces the honest degrade reasonRun of
Verdict: 5 leg(s) RED — 1 green. What this gate does not run (no vacuous green)
failing-leg log tailscontract-fences — tailpackaging-sequential — tailshell-suites — tailpackaging-concurrent — tailgo-battery — tail |
Summary
Slice 2 of the storage-degraded family (#583/#1035 — the storage half itself stays out of scope). Two defects in the degraded state machine kept a broken-wallet (with perfectly reachable mints) degraded for life and lying about why:
onFirstReachablefires once, the upgrade attempt fails,hadReachableMintis set, and nothing ever fires again — degraded until a manual restart.wallet not initialized: no reachable mints— while the mints probe 200 OK. Wallet never initializes on jffs2-overlay devices (squashfs NOR targets): bbolt requires shared mmap; silent degraded mode + misleading "no reachable mints" #583/Wallet InitStorage fails with EINVAL on armv7 + overlayfs/jffs2: backend permanently degraded despite healthy mint #1035 triage burned days on that misdirection (23 PRTA failures chased to one root cause).What changes
MerchantDegradedcarries a degrade reason — set by both ofNew()'s degraded branches (no reachable mintsvswallet initialization failed: …), updated to the last failed attempt's error — and every API stub surfaces it (notInitializedError()). Empty default keeps the legacy text for direct constructions.WireWalletRetry(): a bounded, backing-off wallet-init retry loop (30 s initial → 5 min cap; skips its tick while the reachable set is empty — that class stays with the first-reachable trigger), armed idempotently wherever the trigger is wired (WireStartupDegraded,WireRecoveryTrigger) and inNew()'s degraded branches. Single-flighted against the trigger throughAttemptUpgrade's mutex; stopped on successful upgrade orShutdown.New()'s two degraded branches replace their inline duplicate of the trigger wiring withWireRecoveryTrigger(identical behavior, one owner).onUpgradeis now guarded — the retry loop can be mid-attempt while the caller is still attaching the callback (data race caught by-racein this suite).Fund-safety statement
No monetary operations added or reordered; the retry re-runs wallet construction only. A failed attempt leaves the degraded merchant serving exactly as before (its offline wallet was already shut down by the pre-existing upgrade flow). The loop cannot spin: backoff caps at the proactive cadence, and it never runs while no mint is reachable.
Evidence (ai-legion, go1.26.8;
/tmp/opencode/tg-storage/evidence/go-battery-degraded-retry.log)make go-battery— 18 modules green (gofmt/vet/build/go test -race -count=1 -tags testenvper module).-raceafter the callback guard.Independent of the #1063 → #908 → #1077 chain (different files; based on current main, pin v0.13.0 — the honest-keyset fixture id used survives the v0.14.x guard). OWNER-GATED: no self-merge.