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