[Feature] Recover from panics in the reconcile loop and background goroutines to preserve observability
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 68/100
- Issue type
- Feature
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- go, redis
- Domain
- backend, observability, testing-qa
Research direction
Start with internal/reconciler/reconciler.go and trace Start() through reconcile(), then inspect fan-out goroutines in internal/reconciler/scale_reconciler.go and the three goroutines in cmd/main.go. Run the existing unit tests before adding coverage for recovered panics, continued scheduling, server availability, logging, and the panic counter. Done means one shared recovery helper protects every listed goroutine without changing normal error backoff.
Written by the indexing model from the issue text.
Description
Motivation
The primary goal of this change is to preserve observability when something goes wrong.
Today, any unrecovered panic crashes the entire Robin process. Because the metrics/health HTTP server runs in the same process, a panic takes down /metrics (and the future /healthz) exactly when an operator most needs them to diagnose the problem. On top of that, repeated panics drive the pod into CrashLoopBackOff (backoff up to ~5 min), during which the cluster is not managed and no telemetry is being served.
Robin is effectively stateless between cycles — every reconcile rebuilds its state from the live cluster (re-reads config, recreates Redis clients, rediscovers nodes). So the usual "don't continue after a panic, the in-memory state may be corrupt" argument does not apply here: there is no cross-cycle state to corrupt. This makes in-process recovery both safe and clearly beneficial.
Problem
Robin has no panic recovery anywhere in internal/:
- The reconciliation loop (
internal/reconciler/reconciler.go,Start()→reconcile()) calls reconcile logic with norecover(). - Several paths fan out into background goroutines (e.g. concurrent master checks in
internal/reconciler/scale_reconciler.go) with norecover(). A panic in a goroutine cannot be recovered by its caller and unconditionally kills the process. - The three goroutines launched in
cmd/main.go(reconciler, metrics collector, metrics server) are likewise unprotected.
Unlike the operator — whose controller-runtime manager recovers reconcile panics by default (RecoverPanic defaults to true) — Robin is a plain polling loop and inherits no such protection.
Proposed solution
Add defensive panic recovery so a single bad reconcile can neither crash the process nor kill observability:
- Reconcile loop: wrap each
reconcile()invocation so a panic is caught and converted into a failed cycle that reschedules on the existing error interval (intervalOnError) — keeping the process (and the metrics/health server) alive. - Background goroutines: add a shared
recover()helper used at the entry point of every goroutine running reconcile/Redis logic (fan-outs inscale_reconciler.go, and the three goroutines incmd/main.go). A panicking goroutine fails its unit of work, it does not abort the process. - Keep panics loud (non-negotiable): every recovered panic MUST be logged at
errorlevel with the recovered value and a full stack trace (runtime/debug.Stack()), and increment arobin_reconcile_panics_total(or similar) counter. Recovery must not hide bugs — it only prevents them from destroying visibility.
Acceptance criteria
- A panic inside
reconcile()is recovered: logged with stack trace, counted, and the loop continues (reschedules onintervalOnError) instead of exiting. - A panic inside any background goroutine (fan-out checks, collector, metrics server, reconciler goroutine in
main.go) is recovered and does not crash the process. - The metrics/health HTTP server stays up across a recovered reconcile panic (observability preserved).
- A
..._panics_totalcounter is incremented on each recovered panic, and each panic is logged aterrorwith value + stack. - Recovery does not alter behavior for normal
errorreturns: existing scheduling/backoff is unchanged. - Unit tests: a panicking reconcile leaves the loop alive, keeps the server serving, and increments the metric; a panicking goroutine is contained.
- A single shared helper (e.g.
recoverPanic(logger, metric)) is used consistently instead of ad-hoc recovers.
Implementation notes
- Loop:
internal/reconciler/reconciler.go(Start()/reconcile()) — adefer func(){ if r := recover(); r != nil { ... ; onError = true } }()around the reconcile call is enough to convert a panic into a throttled failed cycle. - Goroutines: add the helper in a shared package and apply it at each
go func(){...}()entry (notablyscale_reconciler.gofan-outs and the three goroutines incmd/main.go). - Throttling comes for free from
intervalOnError, so a persistently panicking condition does not become a hot loop. - Log stacks with
runtime/debug.Stack().
Scope / non-goals
- This change targets panics only. It does not detect a wedged/deadlocked loop that never panics — that is covered by the health-endpoint feature (issue A:
/healthzliveness based on "last successful reconcile"). - The recovered-panic state may later feed the
/healthzliveness signal (repeated panics → unhealthy → Kubernetes restart) as a follow-up.
Related
- Complements redkey-robin issue A (
/healthzhealth endpoint): recovery keeps observability up; liveness catches non-panic hangs. - Mirrors the operator's built-in behavior from controller-runtime's default
RecoverPanic.
- Dominant language
- Go
- Stars
- 18
- Forks
- 0
- Avg merge
- 11h 16m
- Merged PRs (30d)
- 11
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from InditexTech/redkey-robin
-
enhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
InditexTech/redkey-robin#81 ·
Maintainers usually reply within 1 day
-
[Bug] Stale series for string-valued INFO metrics (e.g. `redkey_version`) after the value changesOpenbug
Difficulty 4/5 3-5 days Newbie friendliness 55/100
InditexTech/redkey-robin#76 ·
Maintainers usually reply within 1 day
-
[Bug] Rolling N+1 upgrade stalls indefinitely when a `redis-cli --cluster reshard` does not finishOpenenhancement
Difficulty 4/5 3-5 days Newbie friendliness 57/100
InditexTech/redkey-robin#70 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 22/100
InditexTech/redkey-robin#68 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 3/5 1-2 days Newbie friendliness 62/100
InditexTech/redkey-robin#46 ·
Maintainers usually reply within 1 day
All issues in InditexTech/redkey-robin
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
coder/openstorage#807 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
coder/coder-logstream-kube#255 · 1 comment ·
-
[FEATURE]: 录入 AIDEV Agent Package v1 接入参考文档Possibly taken A pull request linked to this issue is open or already merged. Openkind/features
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
TencentBlueKing/bk-nodemgr#3991 ·
Maintainers usually reply within 1 day