Files
fission-src/pkg/utils/namespace_manager.go
T
“Naeel” 919e84396c fix(reconciler): propagate SA/executor errors to NamespaceManager so failed NSes are retried
Problem
-------
The namespace reconciler (RunReconciler, added previously) retries namespaces
in NamespacePhaseFailed every 30s by calling DispatchResync. But the phase
could never actually reach NamespacePhaseFailed for the executor component
because the executor's NamespaceSubscriber always returned nil — swallowing
any SA-provisioning or informer-init errors. The reconciler was dead code for
the executor path.

Root cause chain
----------------
1. setupSAAndRoleBindings() — void, errors only logged internally.
2. EnsureNamespaceSA()      — void, just called setupSAAndRoleBindings.
3. registerNamespace()      — void, errors from both functions lost.
4. Executor AddFunc/ResyncFunc — always returned nil to dispatch().
5. dispatch() marks parts Active unconditionally   → NamespacePhaseFailed
   is never triggered for executor   → RunReconciler never fires for executor.

Consequence: if EnsureNamespaceSA failed (transient k8s 503, RBAC webhook
timeout, etc.) the namespace appeared Active in the manager but the fetcher
ServiceAccount was missing. Pool pods would CrashLoopBackOff on every call
to that namespace until a full process restart.

Changes
-------
pkg/utils/serviceaccount.go
  - setupSAAndRoleBindings: void → error. Returns the first k8s API error
    so callers can decide whether to retry.
  - runSACheck: ignores the error with _ = (same behaviour as before, it's
    a periodic background loop that already logs internally).
  - EnsureNamespaceSA: void → error, propagates setupSAAndRoleBindings.
    Updated godoc to explain the retry contract.

pkg/executor/multitenant/ns_watcher.go
  - registerNamespace: void → error.
    * EnsureNamespaceSA error → wrapped as 'EnsureNamespaceSA: ...' and returned.
    * registerExecutorTypes error → wrapped as 'registerExecutorTypes: ...' and returned.
    * Success log line only emitted when both succeed.
  - Added 'fmt' import for error wrapping.

pkg/executor/multitenant/namespace_subscriber.go
  - AddFunc:    return registerNamespace(...) instead of ignoring its error.
  - ResyncFunc: same — plus a comment explaining why it is safe to call
    registerNamespace again (SA creation is idempotent, executor-type
    AddNamespace guards against duplicate informer creation).

pkg/utils/namespace_manager.go
  - RunReconciler interface signature: added *zap.Logger parameter.
    Callers pass the component logger so retries are visible in prod logs.
  - RunReconciler implementation:
    * Accepts logger; falls back to zap.NewNop() if nil.
    * Skips the tick entirely when no failed namespaces are found (no log spam).
    * Logs 'retrying failed namespaces' with count + list when found.
    * Logs per-namespace 'dispatching resync'.
    * Logs 'resync succeeded' or 'resync still failing, will retry' with error.
  - RunManagedNamespaceWatcher: passes logger to RunReconciler.

End-to-end flow after this fix
-------------------------------
1. EnsureNamespaceSA fails (k8s 503).
2. registerNamespace returns error.
3. Executor AddFunc returns error.
4. dispatch() calls MarkPartFailed("executor") → deriveNamespacePhase →
   NamespacePhaseFailed.
5. RunReconciler tick (30s) finds the namespace → DispatchResync →
   registerNamespace called again → EnsureNamespaceSA (idempotent) →
   if API recovered: success → MarkPartActive → NamespacePhaseActive.
6. Log line 'namespace reconciler: resync succeeded' confirms recovery.

Backward compatibility
----------------------
- NamespaceManager interface: RunReconciler gained a *zap.Logger param.
  There is exactly one implementation (inMemoryNamespaceManager) and one
  call site (RunManagedNamespaceWatcher). No external mocks.
- EnsureNamespaceSA: callers outside this codebase (if any) that ignore
  the error will still compile (Go allows ignoring return values).
- All 26 affected tests pass: go test ./pkg/utils/... ./pkg/executor/...
  ./pkg/buildermgr/... ./pkg/router/...
2026-05-18 11:10:46 +04:00

687 lines
22 KiB
Go

package utils
import (
"context"
"errors"
"sort"
"sync"
"time"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sInformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
k8sCache "k8s.io/client-go/tools/cache"
managerPkg "github.com/fission/fission/pkg/utils/manager"
)
type NamespaceSubscriber interface {
Name() string
OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error
OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error
OnNamespaceResync(ctx context.Context, record NamespaceRecord) error
}
type NamespaceSubscriberFuncs struct {
SubscriberName string
AddFunc func(ctx context.Context, record NamespaceRecord) error
RemoveFunc func(ctx context.Context, record NamespaceRecord) error
ResyncFunc func(ctx context.Context, record NamespaceRecord) error
}
func (s NamespaceSubscriberFuncs) Name() string {
return s.SubscriberName
}
func (s NamespaceSubscriberFuncs) OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error {
if s.AddFunc == nil {
return nil
}
return s.AddFunc(ctx, record)
}
func (s NamespaceSubscriberFuncs) OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error {
if s.RemoveFunc == nil {
return nil
}
return s.RemoveFunc(ctx, record)
}
func (s NamespaceSubscriberFuncs) OnNamespaceResync(ctx context.Context, record NamespaceRecord) error {
if s.ResyncFunc == nil {
return nil
}
return s.ResyncFunc(ctx, record)
}
type NamespaceManager interface {
Snapshot() []string
SnapshotRecords() []NamespaceRecord
Summary() NamespaceManagerSummary
Get(name string) (NamespaceRecord, bool)
Bootstrap(namespaces []string, source NamespaceSource, observedAt time.Time) []NamespaceRecord
BootstrapAndDispatch(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time) ([]NamespaceRecord, error)
Subscribe(subscriber NamespaceSubscriber)
SnapshotSubscribers() []string
Upsert(event NamespaceEvent) NamespaceRecord
DispatchAdd(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
DispatchRemove(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
DispatchResync(ctx context.Context, namespace string) (NamespaceRecord, bool, error)
MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool)
MarkPartRegistering(namespace string, part string) (NamespaceRecord, bool)
MarkPartActive(namespace string, part string) (NamespaceRecord, bool)
MarkPartFailed(namespace string, part string, err error) (NamespaceRecord, bool)
Remove(name string) bool
// RunReconciler periodically retries namespaces stuck in NamespacePhaseFailed.
// Must be started as a goroutine; exits when ctx is cancelled.
// logger is used to report retry attempts and outcomes; pass zap.NewNop() to silence.
RunReconciler(ctx context.Context, logger *zap.Logger)
}
type inMemoryNamespaceManager struct {
mu sync.RWMutex
records map[string]NamespaceRecord
subs map[string]NamespaceSubscriber
}
func NewNamespaceManager() NamespaceManager {
return &inMemoryNamespaceManager{
records: make(map[string]NamespaceRecord),
subs: make(map[string]NamespaceSubscriber),
}
}
func NewBootstrappedNamespaceManager(resolver *NamespaceResolver, source NamespaceSource, observedAt time.Time) NamespaceManager {
manager := NewNamespaceManager()
if resolver == nil {
return manager
}
manager.Bootstrap(resolver.Snapshot(), source, observedAt)
return manager
}
func NewWatcherNamespaceManager(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time, subscribers ...NamespaceSubscriber) (NamespaceManager, error) {
manager := NewNamespaceManager()
for _, subscriber := range subscribers {
if subscriber == nil {
continue
}
manager.Subscribe(subscriber)
}
_, err := manager.BootstrapAndDispatch(ctx, namespaces, source, observedAt)
return manager, err
}
func NewDefaultManagedNamespaceWatcherConfig(component string, subscriber NamespaceSubscriber) ManagedNamespaceWatcherConfig {
return ManagedNamespaceWatcherConfig{
Component: component,
Namespaces: DefaultNSResolver().Snapshot(),
RemovalStrategy: NamespaceRemovalStrategyTrackOnly,
Subscriber: subscriber,
}
}
func namespaceManagerLogger(logger *zap.Logger) *zap.Logger {
if logger != nil {
return logger
}
return zap.NewNop()
}
func PrepareManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, config ManagedNamespaceWatcherConfig) (NamespaceManager, k8sCache.ResourceEventHandlerFuncs, error) {
logger = namespaceManagerLogger(logger)
strategy := config.RemovalStrategy
if strategy == "" {
strategy = NamespaceRemovalStrategyTrackOnly
}
manager, err := NewWatcherNamespaceManager(ctx, config.Namespaces, NamespaceSourceEnv, time.Now().UTC(), config.Subscriber)
handlers := NewNamespaceWatcherEventHandlers(ctx, logger, config.Component, manager, strategy)
LogNamespaceManagerSummary(logger, config.Component+": prepared namespace manager", manager.Summary())
return manager, handlers, err
}
func RunManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, kubeClient kubernetes.Interface, mgr managerPkg.Interface, config ManagedNamespaceWatcherConfig) (NamespaceManager, error) {
logger = namespaceManagerLogger(logger)
manager, handlers, err := PrepareManagedNamespaceWatcher(ctx, logger, config)
StartManagedNamespaceWatcher(ctx, logger, config.Component, kubeClient, mgr, handlers)
// Start reconciler: retries namespaces stuck in NamespacePhaseFailed every 30s.
mgr.Add(ctx, func(ctx context.Context) {
logger.Info(config.Component + ": namespace reconciler started")
manager.RunReconciler(ctx, logger)
logger.Info(config.Component + ": namespace reconciler stopped")
})
LogNamespaceManagerSummary(logger, config.Component+": started namespace watcher", manager.Summary())
return manager, err
}
func LogNamespaceManagerSummary(logger *zap.Logger, message string, summary NamespaceManagerSummary) {
if logger == nil {
return
}
logger.Info(message,
zap.Int("total_namespaces", summary.TotalNamespaces),
zap.Int("live_namespaces", summary.LiveNamespaces),
zap.Bool("has_active_namespaces", summary.HasActiveNamespaces()),
zap.Any("phase_counts", summary.PhaseCounts),
zap.Any("source_counts", summary.SourceCounts),
zap.Strings("subscribers", summary.Subscribers),
)
}
func NamespaceBecameUnmanaged(oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace) bool {
if oldNamespace == nil || newNamespace == nil {
return false
}
return IsManagedNamespace(oldNamespace.Labels) && !IsManagedNamespace(newNamespace.Labels)
}
func DispatchNamespaceAdd(ctx context.Context, manager NamespaceManager, namespace *corev1.Namespace, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool, error) {
if manager == nil || namespace == nil || namespace.Name == "" {
return NamespaceRecord{}, false, nil
}
manager.Upsert(NamespaceEventFromNamespace(NamespaceEventAdd, namespace, source, observedAt))
return manager.DispatchAdd(ctx, namespace.Name)
}
func DispatchNamespaceResync(ctx context.Context, manager NamespaceManager, namespace *corev1.Namespace, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool, error) {
if manager == nil || namespace == nil || namespace.Name == "" {
return NamespaceRecord{}, false, nil
}
manager.Upsert(NamespaceEventFromNamespace(NamespaceEventUpdate, namespace, source, observedAt))
return manager.DispatchResync(ctx, namespace.Name)
}
func RecordNamespaceRemoval(manager NamespaceManager, obj interface{}, source NamespaceSource, observedAt time.Time) (NamespaceRecord, bool) {
if manager == nil {
return NamespaceRecord{}, false
}
event := NamespaceEventFromObject(NamespaceEventRemove, obj, source, observedAt)
if event.Name == "" {
return NamespaceRecord{}, false
}
record := manager.Upsert(event)
return record, true
}
func HandleWatcherNamespaceAdd(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, namespace *corev1.Namespace) {
logger = namespaceManagerLogger(logger)
if namespace == nil || namespace.Name == "" {
return
}
if _, _, err := DispatchNamespaceAdd(ctx, manager, namespace, NamespaceSourceWatcher, time.Now().UTC()); err != nil {
logger.Error(component+": DispatchAdd failed", zap.String("namespace", namespace.Name), zap.Error(err))
return
}
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func HandleWatcherNamespaceUpdate(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, oldNamespace *corev1.Namespace, newNamespace *corev1.Namespace, strategy NamespaceRemovalStrategy) {
logger = namespaceManagerLogger(logger)
if newNamespace == nil {
return
}
if NamespaceBecameUnmanaged(oldNamespace, newNamespace) {
HandleWatcherNamespaceRemoval(ctx, logger, component, manager, newNamespace, strategy)
return
}
if !IsManagedNamespace(newNamespace.Labels) {
return
}
if _, _, err := DispatchNamespaceResync(ctx, manager, newNamespace, NamespaceSourceWatcher, time.Now().UTC()); err != nil {
logger.Error(component+": DispatchResync failed", zap.String("namespace", newNamespace.Name), zap.Error(err))
return
}
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func HandleWatcherNamespaceDelete(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) {
HandleWatcherNamespaceRemoval(ctx, logger, component, manager, obj, strategy)
}
func HandleWatcherNamespaceRemoval(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, obj interface{}, strategy NamespaceRemovalStrategy) {
logger = namespaceManagerLogger(logger)
record, ok := RecordNamespaceRemoval(manager, obj, NamespaceSourceWatcher, time.Now().UTC())
if !ok {
return
}
// Always remove from global resolver so Snapshot() and idleObjectReaper
// stop iterating this namespace. This is safe: if the same name is re-added
// later, AddNamespace will return true and all subscribers will re-register.
DefaultNSResolver().RemoveNamespace(record.Name)
if strategy == NamespaceRemovalStrategyDispatchRemove {
if _, _, err := manager.DispatchRemove(ctx, record.Name); err != nil {
logger.Error(component+": DispatchRemove failed", zap.String("namespace", record.Name), zap.Error(err))
return
}
logger.Info(component+": namespace removal dispatched through subscribers",
zap.String("namespace", record.Name))
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
return
}
logger.Info(component+": namespace removed from manager state; runtime registrations kept",
zap.String("namespace", record.Name))
LogNamespaceManagerSummary(logger, component+": namespace manager summary", manager.Summary())
}
func NewNamespaceWatcherEventHandlers(ctx context.Context, logger *zap.Logger, component string, manager NamespaceManager, strategy NamespaceRemovalStrategy) k8sCache.ResourceEventHandlerFuncs {
logger = namespaceManagerLogger(logger)
return k8sCache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
namespace, ok := obj.(*corev1.Namespace)
if ok {
HandleWatcherNamespaceAdd(ctx, logger, component, manager, namespace)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldNamespace, _ := oldObj.(*corev1.Namespace)
newNamespace, ok := newObj.(*corev1.Namespace)
if ok {
HandleWatcherNamespaceUpdate(ctx, logger, component, manager, oldNamespace, newNamespace, strategy)
}
},
DeleteFunc: func(obj interface{}) {
HandleWatcherNamespaceDelete(ctx, logger, component, manager, obj, strategy)
},
}
}
func StartManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, component string, kubeClient kubernetes.Interface, mgr managerPkg.Interface, handlers k8sCache.ResourceEventHandlerFuncs) {
logger = namespaceManagerLogger(logger)
factory := k8sInformers.NewSharedInformerFactoryWithOptions(
kubeClient,
30*time.Minute,
k8sInformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
opts.LabelSelector = ManagedNamespaceLabelSelector()
}),
)
nsInformer := factory.Core().V1().Namespaces().Informer()
_, _ = nsInformer.AddEventHandler(handlers)
mgr.Add(ctx, func(ctx context.Context) {
logger.Info(component+": started", zap.String("label", ManagedNamespaceLabelSelector()))
factory.Start(ctx.Done())
factory.WaitForCacheSync(ctx.Done())
logger.Info(component + ": cache synced — watching for new namespaces")
<-ctx.Done()
logger.Info(component + ": stopped")
})
}
func (m *inMemoryNamespaceManager) Summary() NamespaceManagerSummary {
records := m.SnapshotRecords()
summary := NamespaceManagerSummary{
TotalNamespaces: len(records),
LiveNamespaces: len(m.Snapshot()),
PhaseCounts: make(map[NamespacePhase]int),
SourceCounts: make(map[NamespaceSource]int),
Subscribers: m.SnapshotSubscribers(),
}
if summary.Subscribers == nil {
summary.Subscribers = []string{}
}
for _, record := range records {
summary.PhaseCounts[record.Phase]++
summary.SourceCounts[record.Source]++
}
return summary
}
func (m *inMemoryNamespaceManager) Subscribe(subscriber NamespaceSubscriber) {
m.mu.Lock()
defer m.mu.Unlock()
m.subs[subscriber.Name()] = subscriber
}
func (m *inMemoryNamespaceManager) Bootstrap(namespaces []string, source NamespaceSource, observedAt time.Time) []NamespaceRecord {
records := make([]NamespaceRecord, 0, len(namespaces))
for _, namespace := range namespaces {
records = append(records, m.Upsert(NamespaceEvent{
Type: NamespaceEventAdd,
Name: namespace,
Source: source,
ObservedAt: observedAt,
}))
}
sort.Slice(records, func(i, j int) bool {
return records[i].Name < records[j].Name
})
return records
}
func (m *inMemoryNamespaceManager) BootstrapAndDispatch(ctx context.Context, namespaces []string, source NamespaceSource, observedAt time.Time) ([]NamespaceRecord, error) {
records := m.Bootstrap(namespaces, source, observedAt)
var joinErr error
for _, record := range records {
updatedRecord, ok, err := m.DispatchAdd(ctx, record.Name)
if ok {
record = updatedRecord
}
if err != nil {
joinErr = errors.Join(joinErr, err)
}
for index := range records {
if records[index].Name == record.Name {
records[index] = record
break
}
}
}
return records, joinErr
}
func (m *inMemoryNamespaceManager) SnapshotSubscribers() []string {
m.mu.RLock()
defer m.mu.RUnlock()
names := make([]string, 0, len(m.subs))
for name := range m.subs {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (m *inMemoryNamespaceManager) snapshotSubscriberObjects() []NamespaceSubscriber {
m.mu.RLock()
defer m.mu.RUnlock()
names := make([]string, 0, len(m.subs))
for name := range m.subs {
names = append(names, name)
}
sort.Strings(names)
subscribers := make([]NamespaceSubscriber, 0, len(names))
for _, name := range names {
subscribers = append(subscribers, m.subs[name])
}
return subscribers
}
func (m *inMemoryNamespaceManager) Snapshot() []string {
m.mu.RLock()
defer m.mu.RUnlock()
namespaces := make([]string, 0, len(m.records))
for name, record := range m.records {
if record.Phase == NamespacePhaseRemoved {
continue
}
namespaces = append(namespaces, name)
}
sort.Strings(namespaces)
return namespaces
}
func (m *inMemoryNamespaceManager) SnapshotRecords() []NamespaceRecord {
m.mu.RLock()
defer m.mu.RUnlock()
records := make([]NamespaceRecord, 0, len(m.records))
for _, record := range m.records {
records = append(records, record.Clone())
}
sort.Slice(records, func(i, j int) bool {
return records[i].Name < records[j].Name
})
return records
}
func (m *inMemoryNamespaceManager) Get(name string) (NamespaceRecord, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
record, ok := m.records[name]
if !ok {
return NamespaceRecord{}, false
}
return record.Clone(), true
}
func (m *inMemoryNamespaceManager) Upsert(event NamespaceEvent) NamespaceRecord {
m.mu.Lock()
defer m.mu.Unlock()
now := event.ObservedAt
if now.IsZero() {
now = time.Now().UTC()
}
record, exists := m.records[event.Name]
if !exists {
record = NamespaceRecord{
Name: event.Name,
RegisteredParts: make(map[string]NamespacePartState),
}
if event.Type == NamespaceEventRemove {
record.Generation = 0
} else {
record.Generation = 1
}
} else if event.Type != NamespaceEventRemove {
record.Generation++
}
record.Name = event.Name
record.Source = event.Source
record.Labels = cloneStringMap(event.Labels)
record.UpdatedAt = now
switch event.Type {
case NamespaceEventAdd, NamespaceEventUpdate, NamespaceEventResync:
record.Phase = NamespacePhaseDiscovered
record.LastError = ""
case NamespaceEventRemove:
record.Phase = NamespacePhaseRemoved
}
if record.RegisteredParts == nil {
record.RegisteredParts = make(map[string]NamespacePartState)
}
m.records[event.Name] = record
return record.Clone()
}
func (m *inMemoryNamespaceManager) DispatchAdd(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
return m.dispatch(ctx, namespace, func(subscriber NamespaceSubscriber, record NamespaceRecord) error {
return subscriber.OnNamespaceAdd(ctx, record)
})
}
func (m *inMemoryNamespaceManager) DispatchRemove(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
record, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
var firstErr error
for _, subscriber := range m.snapshotSubscriberObjects() {
err := subscriber.OnNamespaceRemove(ctx, record)
if err != nil && firstErr == nil {
firstErr = err
}
}
removedRecord := m.Upsert(NamespaceEvent{
Type: NamespaceEventRemove,
Name: record.Name,
Labels: record.Labels,
Source: record.Source,
ObservedAt: time.Now().UTC(),
})
return removedRecord, true, firstErr
}
func (m *inMemoryNamespaceManager) DispatchResync(ctx context.Context, namespace string) (NamespaceRecord, bool, error) {
return m.dispatch(ctx, namespace, func(subscriber NamespaceSubscriber, record NamespaceRecord) error {
return subscriber.OnNamespaceResync(ctx, record)
})
}
func (m *inMemoryNamespaceManager) dispatch(ctx context.Context, namespace string, handler func(NamespaceSubscriber, NamespaceRecord) error) (NamespaceRecord, bool, error) {
_, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
subscribers := m.snapshotSubscriberObjects()
for _, sub := range subscribers {
_, _ = m.MarkPartRegistering(namespace, sub.Name())
}
// Run all subscriber handlers in parallel — each handler makes independent k8s API calls.
// MarkPart* methods are internally mutex-protected and safe for concurrent calls.
var (
wg sync.WaitGroup
mu sync.Mutex
errs error
)
for _, sub := range subscribers {
sub := sub
wg.Add(1)
go func() {
defer wg.Done()
currentRecord, _ := m.Get(namespace)
err := handler(sub, currentRecord)
if err != nil {
_, _ = m.MarkPartFailed(namespace, sub.Name(), err)
mu.Lock()
errs = errors.Join(errs, err)
mu.Unlock()
return
}
_, _ = m.MarkPartActive(namespace, sub.Name())
}()
}
wg.Wait()
record, _ := m.Get(namespace)
return record, true, errs
}
func (m *inMemoryNamespaceManager) MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool) {
m.mu.Lock()
defer m.mu.Unlock()
record, ok := m.records[namespace]
if !ok {
return NamespaceRecord{}, false
}
if record.RegisteredParts == nil {
record.RegisteredParts = make(map[string]NamespacePartState)
}
if state.UpdatedAt.IsZero() {
state.UpdatedAt = time.Now().UTC()
}
record.RegisteredParts[part] = state
record.Phase = deriveNamespacePhase(record)
record.UpdatedAt = state.UpdatedAt
m.records[namespace] = record
return record.Clone(), true
}
func (m *inMemoryNamespaceManager) MarkPartRegistering(namespace string, part string) (NamespaceRecord, bool) {
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateRegistering})
}
func (m *inMemoryNamespaceManager) MarkPartActive(namespace string, part string) (NamespaceRecord, bool) {
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateActive})
}
func (m *inMemoryNamespaceManager) MarkPartFailed(namespace string, part string, err error) (NamespaceRecord, bool) {
lastError := ""
if err != nil {
lastError = err.Error()
}
return m.MarkPartState(namespace, part, NamespacePartState{State: NamespacePartStateFailed, LastError: lastError})
}
func (m *inMemoryNamespaceManager) Remove(name string) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.records[name]; !ok {
return false
}
delete(m.records, name)
return true
}
// RunReconciler periodically finds namespaces in NamespacePhaseFailed and retries them
// via DispatchResync. This ensures transient k8s API errors (e.g. temporary 503 on
// EnsureNamespaceSA or executor-type AddNamespace) do not permanently strand a namespace.
// logger receives one log line per retry attempt and per outcome.
// Exits when ctx is cancelled.
func (m *inMemoryNamespaceManager) RunReconciler(ctx context.Context, logger *zap.Logger) {
if logger == nil {
logger = zap.NewNop()
}
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
m.mu.RLock()
var failedNS []string
for ns, rec := range m.records {
if rec.Phase == NamespacePhaseFailed {
failedNS = append(failedNS, ns)
}
}
m.mu.RUnlock()
if len(failedNS) == 0 {
continue
}
logger.Info("namespace reconciler: retrying failed namespaces",
zap.Int("count", len(failedNS)),
zap.Strings("namespaces", failedNS),
)
for _, ns := range failedNS {
logger.Info("namespace reconciler: dispatching resync", zap.String("namespace", ns))
_, _, err := m.DispatchResync(ctx, ns)
if err != nil {
logger.Error("namespace reconciler: resync still failing, will retry",
zap.String("namespace", ns),
zap.Error(err),
)
} else {
logger.Info("namespace reconciler: resync succeeded", zap.String("namespace", ns))
}
}
}
}
}
func deriveNamespacePhase(record NamespaceRecord) NamespacePhase {
if len(record.RegisteredParts) == 0 {
return record.Phase
}
hasRegistering := false
for _, part := range record.RegisteredParts {
switch part.State {
case NamespacePartStateFailed:
return NamespacePhaseFailed
case NamespacePartStateRegistering:
hasRegistering = true
case NamespacePartStateActive:
continue
default:
hasRegistering = true
}
}
if hasRegistering {
return NamespacePhaseRegistering
}
return NamespacePhaseActive
}