Skip to content

Commit 281a643

Browse files
markturanskyAmbient Code Botclaude
authored
fix(control-plane): retry handler errors with exponential backoff in informer (#1182)
## Summary - Transient errors (e.g. TSA RoleBinding not yet propagated when the project reconciler runs `ensureRunnerSecrets`) caused events to be permanently dropped — the informer logged the error and moved on with no retry. - Add a `retryLoop` goroutine alongside `dispatchLoop`. Failed handlers are requeued onto a buffered `retryCh` with exponential backoff: 2s → 4s → 8s → 16s → 30s (cap). - After `retryMaxAttempts` (5) the error is logged as permanent. - Fixes the race where a newly-created project namespace's TSA `RoleBinding` isn't propagated by the time `ensureRunnerSecrets` runs. ## Test plan - [ ] Create a new project — CP logs should show `namespace provisioned` and `ambient-runner-secrets created` without permanent failure - [ ] If a transient forbidden error occurs, CP logs should show `handler failed, will retry` with `attempt` and `retry_in` fields, followed by eventual success - [ ] After 5 failed attempts, CP logs `handler failed after max retries` 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added credential management support: create, delete, view, and list credentials * Added JSON output format flag to agent start command * Added GitHub integration demo script for end-to-end workflow testing * **Improvements** * Enhanced event handler resilience with exponential backoff retry logic * Improved namespace cleanup handling during project deletion <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8ad69d4 commit 281a643

1 file changed

Lines changed: 70 additions & 11 deletions

File tree

  • components/ambient-control-plane/internal/informer

components/ambient-control-plane/internal/informer/informer.go

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77
"sync"
8+
"time"
89

910
pb "github.com/ambient-code/platform/components/ambient-api-server/pkg/api/grpc/ambient/v1"
1011
"github.com/ambient-code/platform/components/ambient-control-plane/internal/watcher"
@@ -78,13 +79,26 @@ type ResourceEvent struct {
7879

7980
type EventHandler func(ctx context.Context, event ResourceEvent) error
8081

82+
const (
83+
retryMaxAttempts = 5
84+
retryBaseDelay = 2 * time.Second
85+
retryMaxDelay = 30 * time.Second
86+
)
87+
88+
type retryEvent struct {
89+
event ResourceEvent
90+
attempt int
91+
fireAt time.Time
92+
}
93+
8194
type Informer struct {
8295
sdk *sdkclient.Client
8396
watchManager *watcher.WatchManager
8497
handlers map[string][]EventHandler
8598
mu sync.RWMutex
8699
logger zerolog.Logger
87100
eventCh chan ResourceEvent
101+
retryCh chan retryEvent
88102

89103
sessionCache map[string]types.Session
90104
projectCache map[string]types.Project
@@ -98,6 +112,7 @@ func New(sdk *sdkclient.Client, watchManager *watcher.WatchManager, logger zerol
98112
handlers: make(map[string][]EventHandler),
99113
logger: logger.With().Str("component", "informer").Logger(),
100114
eventCh: make(chan ResourceEvent, 256),
115+
retryCh: make(chan retryEvent, 256),
101116
sessionCache: make(map[string]types.Session),
102117
projectCache: make(map[string]types.Project),
103118
projectSettingsCache: make(map[string]types.ProjectSettings),
@@ -118,6 +133,7 @@ func (inf *Informer) Run(ctx context.Context) error {
118133
}
119134

120135
go inf.dispatchLoop(ctx)
136+
go inf.retryLoop(ctx)
121137

122138
inf.wireWatchHandlers()
123139

@@ -133,18 +149,61 @@ func (inf *Informer) dispatchLoop(ctx context.Context) {
133149
case <-ctx.Done():
134150
return
135151
case event := <-inf.eventCh:
136-
inf.mu.RLock()
137-
handlers := inf.handlers[event.Resource]
138-
inf.mu.RUnlock()
139-
140-
for _, handler := range handlers {
141-
if err := handler(ctx, event); err != nil {
142-
inf.logger.Error().
143-
Err(err).
144-
Str("resource", event.Resource).
145-
Str("event_type", string(event.Type)).
146-
Msg("handler failed")
152+
inf.dispatchEvent(ctx, event, 0)
153+
}
154+
}
155+
}
156+
157+
func (inf *Informer) retryLoop(ctx context.Context) {
158+
for {
159+
select {
160+
case <-ctx.Done():
161+
return
162+
case re := <-inf.retryCh:
163+
wait := time.Until(re.fireAt)
164+
if wait > 0 {
165+
select {
166+
case <-time.After(wait):
167+
case <-ctx.Done():
168+
return
169+
}
170+
}
171+
inf.dispatchEvent(ctx, re.event, re.attempt)
172+
}
173+
}
174+
}
175+
176+
func (inf *Informer) dispatchEvent(ctx context.Context, event ResourceEvent, attempt int) {
177+
inf.mu.RLock()
178+
handlers := inf.handlers[event.Resource]
179+
inf.mu.RUnlock()
180+
181+
for _, handler := range handlers {
182+
if err := handler(ctx, event); err != nil {
183+
if attempt < retryMaxAttempts {
184+
delay := retryBaseDelay * (1 << attempt)
185+
if delay > retryMaxDelay {
186+
delay = retryMaxDelay
187+
}
188+
inf.logger.Warn().
189+
Err(err).
190+
Str("resource", event.Resource).
191+
Str("event_type", string(event.Type)).
192+
Int("attempt", attempt+1).
193+
Int("max_attempts", retryMaxAttempts).
194+
Dur("retry_in", delay).
195+
Msg("handler failed, will retry")
196+
select {
197+
case inf.retryCh <- retryEvent{event: event, attempt: attempt + 1, fireAt: time.Now().Add(delay)}:
198+
case <-ctx.Done():
147199
}
200+
} else {
201+
inf.logger.Error().
202+
Err(err).
203+
Str("resource", event.Resource).
204+
Str("event_type", string(event.Type)).
205+
Int("attempts", attempt+1).
206+
Msg("handler failed after max retries")
148207
}
149208
}
150209
}

0 commit comments

Comments
 (0)