layer1: add namespace manager skeleton step 8
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-04-26 — NamespaceManager rewrite, step 8
|
||||||
|
|
||||||
|
## Цель шага
|
||||||
|
|
||||||
|
Добавить skeleton `NamespaceManager` с in-memory state и unit tests.
|
||||||
|
|
||||||
|
## Что меняем
|
||||||
|
|
||||||
|
1. Добавляем interface `NamespaceManager`.
|
||||||
|
2. Добавляем in-memory реализацию с mutex.
|
||||||
|
3. Добавляем операции:
|
||||||
|
- `Snapshot()`
|
||||||
|
- `SnapshotRecords()`
|
||||||
|
- `Get()`
|
||||||
|
- `Upsert()`
|
||||||
|
- `MarkPartState()`
|
||||||
|
- `Remove()`
|
||||||
|
4. Добавляем unit tests на snapshot/get/upsert/remove/part-state.
|
||||||
|
|
||||||
|
## Что НЕ меняем
|
||||||
|
|
||||||
|
- не подключаем manager к watcher-ам;
|
||||||
|
- не меняем текущий resolver path;
|
||||||
|
- не трогаем runtime components;
|
||||||
|
- не затрагиваем отдельное незакоммиченное изменение в `serviceaccount.go`.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamespaceManager interface {
|
||||||
|
Snapshot() []string
|
||||||
|
SnapshotRecords() []NamespaceRecord
|
||||||
|
Get(name string) (NamespaceRecord, bool)
|
||||||
|
Upsert(event NamespaceEvent) NamespaceRecord
|
||||||
|
MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool)
|
||||||
|
Remove(name string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type inMemoryNamespaceManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
records map[string]NamespaceRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNamespaceManager() NamespaceManager {
|
||||||
|
return &inMemoryNamespaceManager{
|
||||||
|
records: make(map[string]NamespaceRecord),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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.UpdatedAt = state.UpdatedAt
|
||||||
|
m.records[namespace] = record
|
||||||
|
return record.Clone(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNamespaceManagerSnapshotAndGet(t *testing.T) {
|
||||||
|
manager := NewNamespaceManager()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-b", Source: NamespaceSourceWatcher, ObservedAt: now})
|
||||||
|
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now})
|
||||||
|
|
||||||
|
expected := []string{"tenant-a", "tenant-b"}
|
||||||
|
if !reflect.DeepEqual(expected, manager.Snapshot()) {
|
||||||
|
t.Fatalf("expected snapshot %v, got %v", expected, manager.Snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
record, ok := manager.Get("tenant-a")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected tenant-a to exist")
|
||||||
|
}
|
||||||
|
if record.Name != "tenant-a" || record.Phase != NamespacePhaseDiscovered {
|
||||||
|
t.Fatalf("unexpected record returned: %+v", record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceManagerUpsertIncrementsGeneration(t *testing.T) {
|
||||||
|
manager := NewNamespaceManager()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
first := manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now})
|
||||||
|
second := manager.Upsert(NamespaceEvent{Type: NamespaceEventUpdate, Name: "tenant-a", Source: NamespaceSourceWatcher, ObservedAt: now.Add(time.Second)})
|
||||||
|
|
||||||
|
if first.Generation != 1 {
|
||||||
|
t.Fatalf("expected first generation to be 1, got %d", first.Generation)
|
||||||
|
}
|
||||||
|
if second.Generation != 2 {
|
||||||
|
t.Fatalf("expected second generation to be 2, got %d", second.Generation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceManagerMarkPartState(t *testing.T) {
|
||||||
|
manager := NewNamespaceManager()
|
||||||
|
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
|
||||||
|
|
||||||
|
record, ok := manager.MarkPartState("tenant-a", "router", NamespacePartState{State: "active"})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected mark part state to succeed")
|
||||||
|
}
|
||||||
|
if record.RegisteredParts["router"].State != "active" {
|
||||||
|
t.Fatalf("expected router part state to be stored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceManagerRemove(t *testing.T) {
|
||||||
|
manager := NewNamespaceManager()
|
||||||
|
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
|
||||||
|
|
||||||
|
if !manager.Remove("tenant-a") {
|
||||||
|
t.Fatalf("expected remove to succeed")
|
||||||
|
}
|
||||||
|
if _, ok := manager.Get("tenant-a"); ok {
|
||||||
|
t.Fatalf("expected record to be removed")
|
||||||
|
}
|
||||||
|
if manager.Remove("tenant-a") {
|
||||||
|
t.Fatalf("expected second remove to report false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamespaceManagerSnapshotRecordsReturnsCopies(t *testing.T) {
|
||||||
|
manager := NewNamespaceManager()
|
||||||
|
manager.Upsert(NamespaceEvent{
|
||||||
|
Type: NamespaceEventAdd,
|
||||||
|
Name: "tenant-a",
|
||||||
|
Source: NamespaceSourceWatcher,
|
||||||
|
Labels: map[string]string{"fission.io/managed": "true"},
|
||||||
|
})
|
||||||
|
_, _ = manager.MarkPartState("tenant-a", "router", NamespacePartState{State: "active"})
|
||||||
|
|
||||||
|
records := manager.SnapshotRecords()
|
||||||
|
records[0].Labels["fission.io/managed"] = "false"
|
||||||
|
records[0].RegisteredParts["router"] = NamespacePartState{State: "failed"}
|
||||||
|
|
||||||
|
record, ok := manager.Get("tenant-a")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected tenant-a to exist")
|
||||||
|
}
|
||||||
|
if record.Labels["fission.io/managed"] != "true" {
|
||||||
|
t.Fatalf("expected snapshot records to be detached copies")
|
||||||
|
}
|
||||||
|
if record.RegisteredParts["router"].State != "active" {
|
||||||
|
t.Fatalf("expected part states to be detached copies")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user