doc: add namespace manager target design
This commit is contained in:
@@ -0,0 +1,697 @@
|
|||||||
|
# 2026-04-26 — Target design: полноценный NamespaceManager для Layer 1
|
||||||
|
|
||||||
|
## Зачем нужен ещё один документ
|
||||||
|
|
||||||
|
Уже есть подробный документ про сделанные шаги 1-6.
|
||||||
|
Но этого недостаточно для следующего этапа, потому что:
|
||||||
|
|
||||||
|
1. История исправлений не равна целевой архитектуре.
|
||||||
|
2. Локальные фиксы уже уменьшили риск, но не дали единого lifecycle contract.
|
||||||
|
3. Следующий этап уже нельзя начинать как серию хаотичных патчей по месту.
|
||||||
|
|
||||||
|
Нужен отдельный документ, который отвечает на вопрос:
|
||||||
|
|
||||||
|
какой именно Layer 1 мы хотим получить в результате bounded rewrite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Коротко: что именно строим
|
||||||
|
|
||||||
|
Нужен не просто thread-safe registry namespace-ов, а orchestration layer с явным lifecycle.
|
||||||
|
|
||||||
|
То есть не объект вида:
|
||||||
|
|
||||||
|
- `map[string]string` + `AddNamespace()`
|
||||||
|
|
||||||
|
а объект вида:
|
||||||
|
|
||||||
|
- обнаружение namespace;
|
||||||
|
- нормализация состояния;
|
||||||
|
- единый жизненный цикл add/remove/reconcile;
|
||||||
|
- подписка компонентов на события;
|
||||||
|
- безопасный snapshot для background loops;
|
||||||
|
- backfill existing namespaces on startup;
|
||||||
|
- восстановление после restart.
|
||||||
|
|
||||||
|
Рабочее имя этой сущности: `NamespaceManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Какую проблему он решает
|
||||||
|
|
||||||
|
Сейчас логика размазана по нескольким слоям одновременно:
|
||||||
|
|
||||||
|
1. `NamespaceResolver` хранит registry.
|
||||||
|
2. watcher-ы executor/router/buildermgr сами решают, как регистрировать namespace.
|
||||||
|
3. components сами придумывают свой dedup.
|
||||||
|
4. часть background loops читают namespace snapshot.
|
||||||
|
5. provisioning SA/RBAC живёт как side effect watcher-а.
|
||||||
|
|
||||||
|
Из-за этого нет одного ответа на вопросы:
|
||||||
|
|
||||||
|
1. Когда namespace считается «принятым» системой?
|
||||||
|
2. Когда он считается «удалённым»?
|
||||||
|
3. Что должно происходить при restart компонента?
|
||||||
|
4. Кто отвечает за cleanup?
|
||||||
|
5. Кто отвечает за reconcile при расхождении локального и фактического состояния?
|
||||||
|
|
||||||
|
`NamespaceManager` нужен именно для того, чтобы эти вопросы получили один общий ответ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Какие свойства должны быть у новой подсистемы
|
||||||
|
|
||||||
|
### 1. Один вход для namespace lifecycle
|
||||||
|
|
||||||
|
Все namespace-ы, независимо от того, пришли они:
|
||||||
|
|
||||||
|
- из env на старте;
|
||||||
|
- из уже существующих labeled namespaces;
|
||||||
|
- из нового namespace event;
|
||||||
|
- из relabel existing namespace;
|
||||||
|
|
||||||
|
должны проходить через один и тот же pipeline.
|
||||||
|
|
||||||
|
### 2. Явный state machine
|
||||||
|
|
||||||
|
Нельзя больше жить в модели «namespace либо есть в map, либо нет». Нужны как минимум фазы:
|
||||||
|
|
||||||
|
- discovered;
|
||||||
|
- registering;
|
||||||
|
- active;
|
||||||
|
- deregistering;
|
||||||
|
- removed;
|
||||||
|
- failed.
|
||||||
|
|
||||||
|
Не обязательно все эти фазы сразу экспонировать наружу, но внутренняя модель должна понимать, на каком этапе lifecycle находится namespace.
|
||||||
|
|
||||||
|
### 3. Разделение ответственности
|
||||||
|
|
||||||
|
Нужно развести по слоям:
|
||||||
|
|
||||||
|
1. Discovery — кто узнал о namespace.
|
||||||
|
2. Registry — текущее состояние namespace в памяти процесса.
|
||||||
|
3. Reconcile — как довести локальное состояние до желаемого.
|
||||||
|
4. Subscription — как сообщить executor/router/buildermgr о событии.
|
||||||
|
5. Provisioning — отдельные side effects вроде SA/RBAC.
|
||||||
|
|
||||||
|
### 4. Thread-safe чтение и запись
|
||||||
|
|
||||||
|
Любой компонент должен иметь один безопасный способ получить:
|
||||||
|
|
||||||
|
- snapshot namespace-ов;
|
||||||
|
- текущее состояние конкретного namespace;
|
||||||
|
- stream событий.
|
||||||
|
|
||||||
|
### 5. Symmetry startup vs runtime
|
||||||
|
|
||||||
|
Если namespace был известен на старте или пришёл позже, конечный набор действий должен быть одинаковым.
|
||||||
|
|
||||||
|
Именно этот пункт был нарушен в `newdeploy`, и именно он должен стать жёстким архитектурным правилом нового дизайна.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что не должно быть в новой модели
|
||||||
|
|
||||||
|
### 1. Прямых чтений глобальной map из произвольных мест
|
||||||
|
|
||||||
|
Любой код, который напрямую читает внутреннюю структуру namespace registry, должен считаться legacy и подлежать выносу.
|
||||||
|
|
||||||
|
### 2. Глобального dedup вместо локального lifecycle
|
||||||
|
|
||||||
|
Global registry отвечает только на вопрос «namespace известен системе». Он не должен автоматически означать «каждый компонент уже подключил все свои informers».
|
||||||
|
|
||||||
|
### 3. Неявных side effects в watcher callback
|
||||||
|
|
||||||
|
Watcher должен сообщать о факте, а не выполнять пол-процесса orchestration сам по себе.
|
||||||
|
|
||||||
|
### 4. Скрытой зависимости от порядка вызовов
|
||||||
|
|
||||||
|
Сейчас уже был пойман дефект, когда второй компонент не регистрировался, потому что первый успел пометить namespace как «уже обработанный». Новая модель должна быть инвариантна к порядку subscriber-ов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Предлагаемая модель данных
|
||||||
|
|
||||||
|
Ниже не обязательно точный конечный код, но это целевая форма.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type NamespacePhase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NamespacePhaseDiscovered NamespacePhase = "discovered"
|
||||||
|
NamespacePhaseRegistering NamespacePhase = "registering"
|
||||||
|
NamespacePhaseActive NamespacePhase = "active"
|
||||||
|
NamespacePhaseDeregistering NamespacePhase = "deregistering"
|
||||||
|
NamespacePhaseRemoved NamespacePhase = "removed"
|
||||||
|
NamespacePhaseFailed NamespacePhase = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamespaceRecord struct {
|
||||||
|
Name string
|
||||||
|
Source NamespaceSource
|
||||||
|
Labels map[string]string
|
||||||
|
Phase NamespacePhase
|
||||||
|
LastError string
|
||||||
|
Generation int64
|
||||||
|
UpdatedAt time.Time
|
||||||
|
RegisteredParts map[string]NamespacePartState
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamespacePartState struct {
|
||||||
|
State string
|
||||||
|
LastError string
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Важная идея: manager должен знать не только список namespace-ов, но и состояние регистрации по частям.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
- executor.poolmgr: active
|
||||||
|
- executor.newdeploy: active
|
||||||
|
- router: active
|
||||||
|
- buildermgr.env: active
|
||||||
|
- buildermgr.pkg: failed
|
||||||
|
- provisioning.fetcher-sa: active
|
||||||
|
|
||||||
|
Это критично для reconcile. Иначе при частичном падении система знает только «namespace есть», но не знает, что именно недорегистрировано.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Источники namespace-ов
|
||||||
|
|
||||||
|
Нужен явный тип источника, чтобы не смешивать namespace-ы с разным происхождением.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type NamespaceSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NamespaceSourceEnv NamespaceSource = "env"
|
||||||
|
NamespaceSourceWatcher NamespaceSource = "watcher"
|
||||||
|
NamespaceSourceBackfill NamespaceSource = "backfill"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему это важно:
|
||||||
|
|
||||||
|
1. Проще расследовать состояние системы.
|
||||||
|
2. Проще логировать, откуда namespace попал в менеджер.
|
||||||
|
3. Проще понять, что именно должно переживать restart и что должно исчезать при relabel/delete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Предлагаемый API NamespaceManager
|
||||||
|
|
||||||
|
Ниже не «идеальный forever API», а минимально полезный контракт.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type NamespaceManager interface {
|
||||||
|
Snapshot() []string
|
||||||
|
SnapshotRecords() []NamespaceRecord
|
||||||
|
Get(name string) (NamespaceRecord, bool)
|
||||||
|
|
||||||
|
RegisterDesired(ctx context.Context, event NamespaceEvent) error
|
||||||
|
DeregisterDesired(ctx context.Context, name string, reason string) error
|
||||||
|
|
||||||
|
Subscribe(name string, subscriber NamespaceSubscriber)
|
||||||
|
Start(ctx context.Context)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
И ещё важнее — не только sync API, но и события.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type NamespaceEventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NamespaceEventAdd NamespaceEventType = "add"
|
||||||
|
NamespaceEventUpdate NamespaceEventType = "update"
|
||||||
|
NamespaceEventRemove NamespaceEventType = "remove"
|
||||||
|
NamespaceEventResync NamespaceEventType = "resync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NamespaceEvent struct {
|
||||||
|
Type NamespaceEventType
|
||||||
|
Name string
|
||||||
|
Labels map[string]string
|
||||||
|
Source NamespaceSource
|
||||||
|
ObservedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamespaceSubscriber interface {
|
||||||
|
Name() string
|
||||||
|
OnNamespaceAdd(ctx context.Context, ns NamespaceRecord) error
|
||||||
|
OnNamespaceRemove(ctx context.Context, ns NamespaceRecord) error
|
||||||
|
OnNamespaceResync(ctx context.Context, ns NamespaceRecord) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как должен работать startup
|
||||||
|
|
||||||
|
Это один из самых важных разделов. Сейчас именно startup/runtime symmetry остаётся центральным требованием.
|
||||||
|
|
||||||
|
### Текущий анти-pattern
|
||||||
|
|
||||||
|
Сначала что-то строится по env namespaces, потом dynamic path делает другой набор действий отдельно.
|
||||||
|
|
||||||
|
### Целевой startup
|
||||||
|
|
||||||
|
При старте процесса manager должен:
|
||||||
|
|
||||||
|
1. Собрать namespaces из env.
|
||||||
|
2. Сделать backfill всех существующих namespaces с label `fission.io/managed=true`.
|
||||||
|
3. Нормализовать список без дублей.
|
||||||
|
4. Сформировать initial desired set.
|
||||||
|
5. Пропустить весь этот set через тот же reconcile pipeline, что и поздние события.
|
||||||
|
6. Только потом считать manager готовым.
|
||||||
|
|
||||||
|
Иначе говоря:
|
||||||
|
|
||||||
|
startup — это просто массовый initial reconcile, а не отдельная логика «в обход».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как должен работать runtime add
|
||||||
|
|
||||||
|
Когда watcher видит новый namespace или relabel в `managed=true`, он не должен сам лезть во все компоненты.
|
||||||
|
|
||||||
|
Он должен только отправить event в manager:
|
||||||
|
|
||||||
|
```go
|
||||||
|
RegisterDesired(NamespaceEvent{Type: Add, Name: ns, Source: Watcher, Labels: ...})
|
||||||
|
```
|
||||||
|
|
||||||
|
Дальше manager:
|
||||||
|
|
||||||
|
1. Обновляет/создаёт `NamespaceRecord`.
|
||||||
|
2. Ставит phase `registering`.
|
||||||
|
3. По подписчикам запускает reconcile `OnNamespaceAdd`.
|
||||||
|
4. Фиксирует state каждой части.
|
||||||
|
5. Если все обязательные части успешны, переводит namespace в `active`.
|
||||||
|
6. Если часть упала, переводит в `failed` с возможностью повторной reconcile.
|
||||||
|
|
||||||
|
Это важно: add должен быть idempotent и retry-friendly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как должен работать runtime remove
|
||||||
|
|
||||||
|
Это следующий большой пробел в текущем Layer 1.
|
||||||
|
|
||||||
|
Нужен единый remove path для двух случаев:
|
||||||
|
|
||||||
|
1. namespace удалён;
|
||||||
|
2. label `fission.io/managed=true` снят.
|
||||||
|
|
||||||
|
Пайплайн должен быть таким:
|
||||||
|
|
||||||
|
1. Watcher сообщает `remove` event.
|
||||||
|
2. Manager помечает namespace как `deregistering`.
|
||||||
|
3. Вызывает `OnNamespaceRemove` у подписчиков.
|
||||||
|
4. Каждый подписчик:
|
||||||
|
- останавливает локальные informers;
|
||||||
|
- удаляет namespace из локальных lister maps;
|
||||||
|
- очищает связанный cache state.
|
||||||
|
5. После успешного снятия подписок manager переводит namespace в `removed` или удаляет запись полностью.
|
||||||
|
|
||||||
|
Главная причина делать это централизованно:
|
||||||
|
|
||||||
|
если remove semantics будут разъезжаться по компонентам, получится новая версия текущей проблемы, только уже в lifecycle удаления.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как должен работать reconcile
|
||||||
|
|
||||||
|
Remove/add недостаточно. Нужен ещё reconcile.
|
||||||
|
|
||||||
|
Причины:
|
||||||
|
|
||||||
|
1. Компонент мог стартовать позже manager-а.
|
||||||
|
2. Подписчик мог упасть на середине регистрации namespace.
|
||||||
|
3. Restart процесса может привести к тому, что локальная память пуста, а кластерное состояние уже существует.
|
||||||
|
|
||||||
|
Поэтому manager должен уметь периодически или по событию заново прогонять namespace через subscriber-ов.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```go
|
||||||
|
OnNamespaceResync(ctx, ns)
|
||||||
|
```
|
||||||
|
|
||||||
|
Или через тот же `OnNamespaceAdd`, если он строго idempotent.
|
||||||
|
|
||||||
|
Инженерно я бы предпочёл следующее правило:
|
||||||
|
|
||||||
|
1. `OnNamespaceAdd` и `OnNamespaceResync` могут быть одной реализацией.
|
||||||
|
2. Но семантически различать их всё равно полезно для логов и метрик.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Кто должен быть subscriber-ами
|
||||||
|
|
||||||
|
### 1. Executor subscriber
|
||||||
|
|
||||||
|
Внутри него можно уже вызывать внутренние add/remove/resync по типам:
|
||||||
|
|
||||||
|
- poolmgr
|
||||||
|
- newdeploy
|
||||||
|
- container
|
||||||
|
|
||||||
|
Но для manager это один subscriber уровня executor.
|
||||||
|
|
||||||
|
Почему это лучше:
|
||||||
|
|
||||||
|
1. Manager не должен знать детали каждого executor type.
|
||||||
|
2. Executor сам лучше знает, что для него является complete registration.
|
||||||
|
|
||||||
|
### 2. Router subscriber
|
||||||
|
|
||||||
|
Отвечает за:
|
||||||
|
|
||||||
|
- func informer;
|
||||||
|
- trigger informer;
|
||||||
|
- resolver informer registry;
|
||||||
|
- invalidate/rebuild path.
|
||||||
|
|
||||||
|
### 3. BuilderMgr subscriber
|
||||||
|
|
||||||
|
Но внутри него стоит сделать внутреннее разделение частей:
|
||||||
|
|
||||||
|
- env watcher part;
|
||||||
|
- pkg watcher part.
|
||||||
|
|
||||||
|
Именно потому, что на этом месте уже был пойман баг локального dedup.
|
||||||
|
|
||||||
|
### 4. Provisioning subscriber
|
||||||
|
|
||||||
|
Отдельный subscriber для:
|
||||||
|
|
||||||
|
- `fission-fetcher` SA;
|
||||||
|
- возможно builder SA;
|
||||||
|
- связанных Role/RoleBinding path.
|
||||||
|
|
||||||
|
Почему это должен быть отдельный subscriber:
|
||||||
|
|
||||||
|
сейчас provisioning встроен как side effect watcher-а, а это делает sequencing слишком хрупким и плохо наблюдаемым.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Почему provisioning нужно вынести отдельно
|
||||||
|
|
||||||
|
Сейчас логика «namespace зарегистрирован» и логика «в namespace создан нужный service account + RBAC» слишком слеплены.
|
||||||
|
|
||||||
|
Это вредно по нескольким причинам:
|
||||||
|
|
||||||
|
1. Трудно диагностировать, что именно сломалось: discovery, informer wiring или RBAC provisioning.
|
||||||
|
2. Нельзя отдельно повторить provisioning без повторного полного namespace registration.
|
||||||
|
3. Нельзя нормально отслеживать частичный success.
|
||||||
|
|
||||||
|
Целевой дизайн:
|
||||||
|
|
||||||
|
- manager знает, что provisioning — это отдельная обязательная или полуобязательная часть namespace lifecycle;
|
||||||
|
- provisioning subscriber отдаёт свой статус отдельно;
|
||||||
|
- при необходимости его можно повторно reconcile без переинициализации router/executor/buildermgr.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Нужен ли новый объект вместо NamespaceResolver
|
||||||
|
|
||||||
|
Да, но не обязательно удалять `NamespaceResolver` в один момент.
|
||||||
|
|
||||||
|
Реалистичная стратегия:
|
||||||
|
|
||||||
|
### Этап A
|
||||||
|
|
||||||
|
Сделать `NamespaceResolver` внутренней реализацией snapshot/compat layer.
|
||||||
|
|
||||||
|
### Этап B
|
||||||
|
|
||||||
|
Поверх него построить `NamespaceManager` как orchestration layer.
|
||||||
|
|
||||||
|
### Этап C
|
||||||
|
|
||||||
|
Постепенно вычистить прямые зависимости компонентов от `NamespaceResolver` и перевести их на manager/subscriber contract.
|
||||||
|
|
||||||
|
Почему так, а не сразу delete old resolver:
|
||||||
|
|
||||||
|
1. Слишком много мест уже используют текущие helper-ы.
|
||||||
|
2. Нужен период совместного существования старого snapshot API и нового orchestration API.
|
||||||
|
3. Иначе blast radius снова станет слишком большим.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Минимальный состав внутренних методов manager-а
|
||||||
|
|
||||||
|
Ниже не внешний API, а то, что почти наверняка понадобится внутри.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (m *manager) upsertRecord(event NamespaceEvent) NamespaceRecord
|
||||||
|
func (m *manager) markPartState(ns string, subscriber string, state NamespacePartState)
|
||||||
|
func (m *manager) markPhase(ns string, phase NamespacePhase, err error)
|
||||||
|
func (m *manager) snapshotActiveNamespaces() []string
|
||||||
|
func (m *manager) emit(event internalEvent)
|
||||||
|
func (m *manager) reconcileNamespace(ctx context.Context, name string)
|
||||||
|
func (m *manager) removeNamespace(ctx context.Context, name string)
|
||||||
|
```
|
||||||
|
|
||||||
|
Причина: если manager не умеет хранить part-level state, он снова выродится в glorified map.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Какой порядок вызовов нужен при add
|
||||||
|
|
||||||
|
Не просто «вызвать всех subscriber-ов подряд». Нужна осознанная последовательность.
|
||||||
|
|
||||||
|
Один из возможных вариантов:
|
||||||
|
|
||||||
|
1. Provisioning subscriber
|
||||||
|
2. BuilderMgr subscriber
|
||||||
|
3. Executor subscriber
|
||||||
|
4. Router subscriber
|
||||||
|
|
||||||
|
Но это не единственный вариант. Важно другое: порядок должен быть явным и объяснимым.
|
||||||
|
|
||||||
|
Почему provisioning логично раньше:
|
||||||
|
|
||||||
|
если namespace ещё не имеет нужного service account, часть runtime path может не подняться корректно.
|
||||||
|
|
||||||
|
Почему router можно позже:
|
||||||
|
|
||||||
|
он меньше зависит от SA provisioning, чем runtime execution path.
|
||||||
|
|
||||||
|
Но я бы не жёстко кодировал этот порядок как случайную последовательность callback-ов. Лучше иметь явно заданную subscriber order policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как manager должен вести себя при частичном падении
|
||||||
|
|
||||||
|
Это одна из самых важных деталей, потому что сейчас система часто мыслит бинарно: success/fail.
|
||||||
|
|
||||||
|
Нужно поведение такого типа:
|
||||||
|
|
||||||
|
1. Executor зарегистрировался успешно.
|
||||||
|
2. Router зарегистрировался успешно.
|
||||||
|
3. BuilderMgr не зарегистрировался.
|
||||||
|
4. Namespace получает phase `failed` или `active-with-errors`.
|
||||||
|
5. В record фиксируется, что именно сломалось.
|
||||||
|
6. Reconcile можно повторить только для buildermgr part.
|
||||||
|
|
||||||
|
Именно это позволит избегать режимов «namespace вроде есть, но реально не полностью обслуживается, а система этого не видит».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Метрики и логирование
|
||||||
|
|
||||||
|
Без этого новый manager будет трудно отлаживать.
|
||||||
|
|
||||||
|
Нужно как минимум:
|
||||||
|
|
||||||
|
### Метрики
|
||||||
|
|
||||||
|
- число active namespaces;
|
||||||
|
- число failed namespaces;
|
||||||
|
- число reconcile attempts;
|
||||||
|
- число add/remove events;
|
||||||
|
- количество ошибок по subscriber-ам.
|
||||||
|
|
||||||
|
### Логи
|
||||||
|
|
||||||
|
На каждое важное событие должны быть логи такого класса:
|
||||||
|
|
||||||
|
- namespace discovered;
|
||||||
|
- namespace registration started;
|
||||||
|
- subscriber registration succeeded;
|
||||||
|
- subscriber registration failed;
|
||||||
|
- namespace active;
|
||||||
|
- namespace deregistering;
|
||||||
|
- namespace removed;
|
||||||
|
- resync started/completed.
|
||||||
|
|
||||||
|
Без этого следующая стадия дебага снова упрётся в разрозненные логи компонентов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Тестовая стратегия для нового этапа
|
||||||
|
|
||||||
|
Нельзя ограничиться только unit tests отдельных helper-ов.
|
||||||
|
|
||||||
|
Нужны как минимум четыре слоя проверок.
|
||||||
|
|
||||||
|
### 1. Unit tests manager state machine
|
||||||
|
|
||||||
|
- add нового namespace;
|
||||||
|
- повторный add идемпотентен;
|
||||||
|
- remove переводит в нужную фазу;
|
||||||
|
- partial failure отражается в part states.
|
||||||
|
|
||||||
|
### 2. Unit tests subscriber ordering / reconcile
|
||||||
|
|
||||||
|
- add вызывает всех нужных subscriber-ов;
|
||||||
|
- failure одного subscriber-а не портит состояние других;
|
||||||
|
- повторный resync догоняет незарегистрированную часть.
|
||||||
|
|
||||||
|
### 3. Component tests
|
||||||
|
|
||||||
|
- buildermgr add/remove;
|
||||||
|
- router add/remove;
|
||||||
|
- newdeploy add parity;
|
||||||
|
- executor resync.
|
||||||
|
|
||||||
|
### 4. End-to-end tests
|
||||||
|
|
||||||
|
- startup with existing managed namespaces;
|
||||||
|
- late namespace add;
|
||||||
|
- relabel add;
|
||||||
|
- label removal;
|
||||||
|
- namespace delete;
|
||||||
|
- process restart;
|
||||||
|
- burst onboarding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как бы я разбил реализацию следующего этапа на коммиты
|
||||||
|
|
||||||
|
Это очень важно: не повторять ошибку большого rewrite.
|
||||||
|
|
||||||
|
### Commit A
|
||||||
|
|
||||||
|
Добавить скелет `NamespaceManager` и in-memory record model без подключения компонентов.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
|
||||||
|
- новый тип существует;
|
||||||
|
- есть unit tests state model;
|
||||||
|
- legacy path ещё не тронут.
|
||||||
|
|
||||||
|
### Commit B
|
||||||
|
|
||||||
|
Подключить discovery path: env + namespace watcher events начинают идти в manager.
|
||||||
|
|
||||||
|
Но subscribers пока можно ограничить одним compatibility subscriber.
|
||||||
|
|
||||||
|
### Commit C
|
||||||
|
|
||||||
|
Сделать provisioning отдельным subscriber-ом.
|
||||||
|
|
||||||
|
### Commit D
|
||||||
|
|
||||||
|
Перевести buildermgr на manager/subscriber contract.
|
||||||
|
|
||||||
|
Почему именно buildermgr первым:
|
||||||
|
|
||||||
|
там уже был пойман реальный dedup defect, и логика явно просит более чистый lifecycle.
|
||||||
|
|
||||||
|
### Commit E
|
||||||
|
|
||||||
|
Перевести router на manager/subscriber contract.
|
||||||
|
|
||||||
|
### Commit F
|
||||||
|
|
||||||
|
Перевести executor subscriber.
|
||||||
|
|
||||||
|
### Commit G
|
||||||
|
|
||||||
|
Добавить remove/relabel/delete lifecycle.
|
||||||
|
|
||||||
|
### Commit H
|
||||||
|
|
||||||
|
Вычистить legacy прямые обращения к resolver там, где это уже возможно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что можно оставить совместимым на переходный период
|
||||||
|
|
||||||
|
Не всё нужно ломать сразу.
|
||||||
|
|
||||||
|
Можно временно оставить:
|
||||||
|
|
||||||
|
1. `Snapshot()` API у `NamespaceResolver` как compatibility layer.
|
||||||
|
2. Часть существующих helper-ов для informer factory creation.
|
||||||
|
3. Отдельные component-specific `AddNamespace()` методы, но вызывать их уже через manager subscriber.
|
||||||
|
|
||||||
|
Это позволит переподключать компоненты последовательно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Какие риски у самого NamespaceManager rewrite
|
||||||
|
|
||||||
|
Нужно честно фиксировать и риски новой архитектуры.
|
||||||
|
|
||||||
|
### 1. Over-centralization
|
||||||
|
|
||||||
|
Если сделать manager слишком умным, он начнёт знать внутренности каждого компонента, и получится новый монолит уже поверх старого.
|
||||||
|
|
||||||
|
Поэтому manager должен оркестрировать lifecycle, но не содержать доменную логику executor/router/buildermgr.
|
||||||
|
|
||||||
|
### 2. Deadlocks или долгие lock sections
|
||||||
|
|
||||||
|
Если state manager будет держать lock во время вызова subscriber-ов, это плохой дизайн.
|
||||||
|
|
||||||
|
Нужно правило:
|
||||||
|
|
||||||
|
- lock только на обновление внутреннего state;
|
||||||
|
- вызовы subscriber-ов делать вне глобального lock.
|
||||||
|
|
||||||
|
### 3. Excessive retries
|
||||||
|
|
||||||
|
Если reconcile не ограничить и не сделать наблюдаемым, можно получить noisy system с бесконечными повторными попытками.
|
||||||
|
|
||||||
|
### 4. Confused ownership
|
||||||
|
|
||||||
|
Если не определить, кто отвечает за remove/reconcile конкретной части, получится новая версия старой размазанной логики.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что я считаю правильным следующим шагом после этого документа
|
||||||
|
|
||||||
|
Не сразу кодить full manager.
|
||||||
|
|
||||||
|
Сначала нужен ещё один маленький подготовительный шаг:
|
||||||
|
|
||||||
|
1. Добавить новый package или файл со skeleton model `NamespaceRecord`, `NamespacePhase`, `NamespaceEvent`.
|
||||||
|
2. Покрыть его unit tests.
|
||||||
|
3. Не подключать пока к production lifecycle.
|
||||||
|
|
||||||
|
Почему:
|
||||||
|
|
||||||
|
это даст опорную модель данных, вокруг которой уже можно строить manager, не смешивая сразу storage, watchers и subscribers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
|
||||||
|
Целевой `NamespaceManager` для Layer 1 — это не «один общий namespace» и не «ещё один helper над map`ой`».
|
||||||
|
|
||||||
|
Это должен быть orchestration слой с пятью обязательными свойствами:
|
||||||
|
|
||||||
|
1. единый lifecycle add/remove/resync;
|
||||||
|
2. state model с phase и part-level status;
|
||||||
|
3. подписчики-компоненты вместо хаотичных side effects;
|
||||||
|
4. symmetry startup и runtime onboarding;
|
||||||
|
5. безопасный reconcile после ошибок и restart.
|
||||||
|
|
||||||
|
Только после этого можно сказать, что Layer 1 действительно перестал быть монопользовательским Fission с набором динамических заплаток и стал многопользовательским control-plane слоем с понятным жизненным циклом.
|
||||||
Reference in New Issue
Block a user