layer1: add namespace dispatch step 17

This commit is contained in:
Naeel
2026-04-26 10:22:35 +03:00
parent ae8275d0a7
commit b1e2e6462d
3 changed files with 139 additions and 12 deletions
@@ -0,0 +1,22 @@
# 2026-04-26 — NamespaceManager rewrite, step 17
## Цель шага
Добавить dispatch helper для прогона namespace через subscriber-ов в add/resync path.
## Что меняем
1. В manager interface добавляем:
- `DispatchAdd()`
- `DispatchResync()`
2. Manager вызывает subscriber-ов последовательно.
3. Для каждого subscriber-а manager проставляет part state:
- `registering`
- `active` или `failed`
4. Добавляем unit tests на success и failure path.
## Что НЕ меняем
- не подключаем dispatch к production watcher-ам;
- не добавляем remove dispatch;
- не меняем runtime components.
+56
View File
@@ -22,6 +22,8 @@ type NamespaceManager interface {
Subscribe(subscriber NamespaceSubscriber)
SnapshotSubscribers() []string
Upsert(event NamespaceEvent) NamespaceRecord
DispatchAdd(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)
@@ -85,6 +87,23 @@ func (m *inMemoryNamespaceManager) SnapshotSubscribers() []string {
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()
@@ -170,6 +189,43 @@ func (m *inMemoryNamespaceManager) Upsert(event NamespaceEvent) NamespaceRecord
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) 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) {
record, ok := m.Get(namespace)
if !ok {
return NamespaceRecord{}, false, nil
}
var firstErr error
for _, subscriber := range m.snapshotSubscriberObjects() {
_, _ = m.MarkPartRegistering(namespace, subscriber.Name())
currentRecord, _ := m.Get(namespace)
err := handler(subscriber, currentRecord)
if err != nil {
_, _ = m.MarkPartFailed(namespace, subscriber.Name(), err)
if firstErr == nil {
firstErr = err
}
continue
}
_, _ = m.MarkPartActive(namespace, subscriber.Name())
}
record, _ = m.Get(namespace)
return record, true, firstErr
}
func (m *inMemoryNamespaceManager) MarkPartState(namespace string, part string, state NamespacePartState) (NamespaceRecord, bool) {
m.mu.Lock()
defer m.mu.Unlock()
+61 -12
View File
@@ -9,23 +9,29 @@ import (
)
type testNamespaceSubscriber struct {
name string
name string
addErr error
resyncErr error
addCalls int
resyncCalls int
}
func (s testNamespaceSubscriber) Name() string {
func (s *testNamespaceSubscriber) Name() string {
return s.name
}
func (s testNamespaceSubscriber) OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error {
func (s *testNamespaceSubscriber) OnNamespaceAdd(ctx context.Context, record NamespaceRecord) error {
s.addCalls++
return s.addErr
}
func (s *testNamespaceSubscriber) OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error {
return nil
}
func (s testNamespaceSubscriber) OnNamespaceRemove(ctx context.Context, record NamespaceRecord) error {
return nil
}
func (s testNamespaceSubscriber) OnNamespaceResync(ctx context.Context, record NamespaceRecord) error {
return nil
func (s *testNamespaceSubscriber) OnNamespaceResync(ctx context.Context, record NamespaceRecord) error {
s.resyncCalls++
return s.resyncErr
}
func TestNamespaceManagerSnapshotAndGet(t *testing.T) {
@@ -122,9 +128,9 @@ func TestNamespaceManagerSnapshotRecordsReturnsCopies(t *testing.T) {
func TestNamespaceManagerSubscribers(t *testing.T) {
manager := NewNamespaceManager()
manager.Subscribe(testNamespaceSubscriber{name: "router"})
manager.Subscribe(testNamespaceSubscriber{name: "buildermgr"})
manager.Subscribe(testNamespaceSubscriber{name: "router"})
manager.Subscribe(&testNamespaceSubscriber{name: "router"})
manager.Subscribe(&testNamespaceSubscriber{name: "buildermgr"})
manager.Subscribe(&testNamespaceSubscriber{name: "router"})
expected := []string{"buildermgr", "router"}
if !reflect.DeepEqual(expected, manager.SnapshotSubscribers()) {
@@ -216,3 +222,46 @@ func TestNewBootstrappedNamespaceManager(t *testing.T) {
t.Fatalf("expected env source, got %s", record.Source)
}
}
func TestNamespaceManagerDispatchAdd(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr"}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchAdd(context.Background(), "tenant-a")
if !ok || err != nil {
t.Fatalf("expected dispatch add success, ok=%v err=%v", ok, err)
}
if router.addCalls != 1 || builder.addCalls != 1 {
t.Fatalf("expected both subscribers to receive add call")
}
if record.Phase != NamespacePhaseActive {
t.Fatalf("expected active phase after successful dispatch, got %s", record.Phase)
}
}
func TestNamespaceManagerDispatchResyncFailure(t *testing.T) {
manager := NewNamespaceManager()
manager.Upsert(NamespaceEvent{Type: NamespaceEventAdd, Name: "tenant-a", Source: NamespaceSourceWatcher})
router := &testNamespaceSubscriber{name: "router"}
builder := &testNamespaceSubscriber{name: "buildermgr", resyncErr: errors.New("resync failed")}
manager.Subscribe(router)
manager.Subscribe(builder)
record, ok, err := manager.DispatchResync(context.Background(), "tenant-a")
if !ok || err == nil {
t.Fatalf("expected dispatch resync failure, ok=%v err=%v", ok, err)
}
if builder.resyncCalls != 1 {
t.Fatalf("expected failing subscriber to receive resync call")
}
if record.Phase != NamespacePhaseFailed {
t.Fatalf("expected failed phase after dispatch error, got %s", record.Phase)
}
if record.RegisteredParts["buildermgr"].LastError != "resync failed" {
t.Fatalf("expected subscriber error to be stored")
}
}