diff --git a/doc/thinking/2026-04-26-namespace-manager-step45.md b/doc/thinking/2026-04-26-namespace-manager-step45.md new file mode 100644 index 00000000..a20d5571 --- /dev/null +++ b/doc/thinking/2026-04-26-namespace-manager-step45.md @@ -0,0 +1,20 @@ +# 2026-04-26 — NamespaceManager rewrite, step 45 + +## Цель шага + +Подготовить компактный status/debug surface для `NamespaceManager`. + +## Что меняем + +1. Добавляем `NamespaceManagerSummary`. +2. В `NamespaceManager` добавляем `Summary()`. +3. Summary считает: + - общее число namespace-ов; + - число по phase; + - список subscriber-ов. +4. Добавляем unit tests. + +## Что НЕ меняем + +- не публикуем summary наружу через HTTP; +- не меняем watcher behavior. \ No newline at end of file diff --git a/pkg/utils/namespace_manager.go b/pkg/utils/namespace_manager.go index 5188bcca..a057655b 100644 --- a/pkg/utils/namespace_manager.go +++ b/pkg/utils/namespace_manager.go @@ -59,6 +59,7 @@ func (s NamespaceSubscriberFuncs) OnNamespaceResync(ctx context.Context, 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) @@ -240,6 +241,19 @@ func StartManagedNamespaceWatcher(ctx context.Context, logger *zap.Logger, compo }) } +func (m *inMemoryNamespaceManager) Summary() NamespaceManagerSummary { + records := m.SnapshotRecords() + summary := NamespaceManagerSummary{ + TotalNamespaces: len(records), + PhaseCounts: make(map[NamespacePhase]int), + Subscribers: m.SnapshotSubscribers(), + } + for _, record := range records { + summary.PhaseCounts[record.Phase]++ + } + return summary +} + func (m *inMemoryNamespaceManager) Subscribe(subscriber NamespaceSubscriber) { m.mu.Lock() defer m.mu.Unlock() diff --git a/pkg/utils/namespace_manager_model.go b/pkg/utils/namespace_manager_model.go index fc79c901..4ddedc1e 100644 --- a/pkg/utils/namespace_manager_model.go +++ b/pkg/utils/namespace_manager_model.go @@ -73,6 +73,12 @@ type NamespaceEvent struct { ObservedAt time.Time } +type NamespaceManagerSummary struct { + TotalNamespaces int + PhaseCounts map[NamespacePhase]int + Subscribers []string +} + func NewNamespaceEvent(eventType NamespaceEventType, name string, labels map[string]string, source NamespaceSource, observedAt time.Time) NamespaceEvent { return NamespaceEvent{ Type: eventType, diff --git a/pkg/utils/namespace_manager_model_test.go b/pkg/utils/namespace_manager_model_test.go index 71d0c2a0..644eee08 100644 --- a/pkg/utils/namespace_manager_model_test.go +++ b/pkg/utils/namespace_manager_model_test.go @@ -154,3 +154,10 @@ func TestNamespaceRemovalStrategyValues(t *testing.T) { t.Fatalf("unexpected dispatch-remove strategy value") } } + +func TestNamespaceManagerSummaryZeroValue(t *testing.T) { + summary := NamespaceManagerSummary{} + if summary.TotalNamespaces != 0 { + t.Fatalf("expected zero total namespaces") + } +} diff --git a/pkg/utils/namespace_manager_test.go b/pkg/utils/namespace_manager_test.go index f90149e8..b276aca8 100644 --- a/pkg/utils/namespace_manager_test.go +++ b/pkg/utils/namespace_manager_test.go @@ -59,6 +59,10 @@ func TestNamespaceManagerSnapshotAndGet(t *testing.T) { if record.Name != "tenant-a" || record.Phase != NamespacePhaseDiscovered { t.Fatalf("unexpected record returned: %+v", record) } + summary := manager.Summary() + if summary.TotalNamespaces != 2 { + t.Fatalf("expected summary total namespaces to be 2, got %d", summary.TotalNamespaces) + } } func TestNamespaceManagerUpsertIncrementsGeneration(t *testing.T) { @@ -448,6 +452,29 @@ func TestPrepareManagedNamespaceWatcher(t *testing.T) { } } +func TestNamespaceManagerSummary(t *testing.T) { + manager := NewNamespaceManager() + manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher}) + manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-b", Source: NamespaceSourceWatcher}) + _, _ = manager.MarkPartRegistering("tenant-a", "router") + manager.Upsert(NamespaceEvent{Type: NamespaceEventRemove, Name: "tenant-b", Source: NamespaceSourceWatcher}) + manager.Subscribe(&testNamespaceSubscriber{name: "router"}) + + summary := manager.Summary() + if summary.TotalNamespaces != 2 { + t.Fatalf("expected total namespaces 2, got %d", summary.TotalNamespaces) + } + if summary.PhaseCounts[NamespacePhaseRegistering] != 1 { + t.Fatalf("expected one registering namespace") + } + if summary.PhaseCounts[NamespacePhaseRemoved] != 1 { + t.Fatalf("expected one removed namespace") + } + if !reflect.DeepEqual([]string{"router"}, summary.Subscribers) { + t.Fatalf("expected router subscriber in summary") + } +} + func TestNamespaceManagerDispatchAdd(t *testing.T) { manager := NewNamespaceManager() manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})