doc(audit): mark stuck-failed as CLOSED after 919e8439 (RunReconciler + error propagation)

Updated FORENSIC_ARCHITECTURE_AUDIT.md to reflect the fix from commit 919e8439:

Status table:
- 'Stuck-failed namespace без auto-recovery':  ОТКРЫТ →  ЗАКРЫТ (919e8439)
- 'No Explicit State Machine':  ОТКРЫТ → ⚠️ СМЯГЧЕНО (stuck-failed закрыт;
  явная state machine остаётся в backlog)

Sections updated:
- §1.4: полное описание что было (void-функции, мёртвый reconciler) и что
  сделано (error propagation chain, end-to-end flow retry)
- Fragile Components: Stuck Failed Phase — вычеркнуто как закрытое
- Risky Decisions table: No Explicit State Machine → частично закрыто
- Lifecycle Management: добавлено что auto-recovery работает через RunReconciler
- Operational Burden: убрано упоминание stuck-failed как активной проблемы
- Maintainability/Production-Grade: обновлены под текущее состояние
- §2 Stuck Failed Accumulation:  ЗАКРЫТ
- §3 P1 Reconcile-очередь:  ЗАКРЫТ
- §5 Sharded mutex вердикт: убрано упоминание stuck-failed
This commit is contained in:
“Naeel”
2026-05-18 11:32:13 +04:00
parent 919e84396c
commit f5b57173f5
+28 -21
View File
@@ -14,9 +14,9 @@
| Executor dedup dirty state при re-add NS | ✅ ЗАКРЫТ | `4eedf95f` |
| Синхронный subscriber dispatch (onboarding latency) | ✅ ЗАКРЫТ | предыдущая сессия |
| `DefaultNSResolver` только append (нет RemoveNamespace) | ✅ ЗАКРЫТ | предыдущая сессия |
| Stuck-failed namespace без auto-recovery | ❌ ОТКРЫТ | |
| Stuck-failed namespace без auto-recovery | ✅ ЗАКРЫТ | `919e8439` |
| AdoptExistingResources race при rolling update | ❌ ОТКРЫТ | — |
| No explicit state machine (implicit phase transitions) | ❌ ОТКРЫТ | — |
| No Explicit State Machine (implicit phase transitions) | ⚠️ СМЯГЧЕНО | `919e8439` |
| Sharded mutex (bottleneck при >500 concurrent tenant) | ⏳ BACKLOG | не актуально при текущей нагрузке |
---
@@ -67,7 +67,7 @@
- **RBAC/SA Drift**: Неконсистентность между созданием и удалением SA/ролей при сбое в `deregisterNamespace`.
- **Cache Invalidation**: `FunctionServiceCache` не очищается при `RemoveNamespace` — расчёт на `idleObjectReaper`. При высоком churn rate может накапливать stale записи быстрее, чем reaper убирает.
- **Adoption Race**: `AdoptExistingResources` vs `namespace_subscriber` — активная проблема (§1.3).
- **Stuck Failed Phase**: Namespace в `failed` не восстанавливается без рестарта — активная проблема (§1.4).
- **Stuck Failed Phase**: ~~Namespace в `failed` не восстанавливается без рестарта~~закрыто: `RunReconciler` + полная цепочка error propagation (§1.4).
---
@@ -96,7 +96,7 @@
| Informer Lifecycle Management | ✅ Hardened | per-NS context cancel + RemoveNamespace во всех компонентах |
| Centralized Mutex | ⚠️ Приемлемо | sharding в backlog, не актуально до >500 NS |
| Manual Adoption | ❌ Активная проблема | race при rolling update |
| No Explicit State Machine | ❌ Активная проблема | stuck-failed без retry |
| No Explicit State Machine | ✅ Частично закрыто | stuck-failed закрыт (`919e8439`); явная state machine в backlog |
| Eventual Consistency | ⚠️ Смягчено | параллельный dispatch уменьшает окно, но не устраняет |
---
@@ -105,10 +105,10 @@
- **Multi-Tenancy**: Label-based discovery, каждый tenant — отдельный namespace, изоляция на уровне k8s.
- **Isolation Model**: Namespace-level isolation, per-NS SA/RBAC, per-NS informer factory.
- **Lifecycle Management**: Фазы (discovered → registering → active → deregistering → removed / failed) реализованы, но без явной state machine и без auto-recovery из failed.
- **Lifecycle Management**: Фазы (discovered → registering → active → deregistering → removed / failed) реализованы. Auto-recovery из failed работает через `RunReconciler`. Явная state machine в backlog.
- **State Handling**: Глобальный `DefaultNSResolver` + локальные lister-ы. После `RemoveNamespace` — оба синхронизованы. После re-add — оба корректно инициализируются заново.
- **Reconciliation Logic**: Каждый компонент через subscribe. Отсутствует reconcile-очередь для failed state.
- **Operational Burden**: Средний — goroutine leak устранён, stale informer устранён. Требуется мониторинг: stuck-failed накопление, orphaned SA/RBAC при неудачном deregister.
- **Operational Burden**: Средний — goroutine leak устранён, stale informer устранён, stuck-failed закрыт. Требуется мониторинг: orphaned SA/RBAC при неудачном deregister.
---
@@ -116,8 +116,8 @@
- **Maturity**: Архитектурно зрелый, хорошо документированный, с явным reasoning и поэтапным внедрением.
- **Complexity**: Высокая в синхронизации и lifecycle. Снижена за счёт формализации `RemoveNamespace` контракта.
- **Maintainability**: Среднесрочная — без явной state machine и auto-recovery возможны stuck state при API нестабильности.
- **Production-Grade**: Близко — informer lifecycle корректен, dispatch параллелен, cleanup симметричен. Основной gap: stuck-failed и AdoptExistingResources race.
- **Maintainability**: Среднесрочная — без явной state machine сложность будет расти. Auto-recovery из failed работает.
- **Production-Grade**: Близко — informer lifecycle корректен, dispatch параллелен, cleanup симметричен, stuck-failed закрыт. Основной gap: AdoptExistingResources race.
---
@@ -170,20 +170,29 @@
---
### 1.4 No Explicit State Machine — ❌ АКТИВНАЯ ПРОБЛЕМА
### 1.4 No Explicit State Machine — ✅ ЗАКРЫТ (коммит `919e8439`)
**Сценарий: stuck в `failed` без auto-recovery**
1. `registerNamespace()` вызывает `EnsureNamespaceSA()` — Kubernetes API возвращает 503.
2. Часть помечается `NamespacePartStateFailed` → фаза NS → `NamespacePhaseFailed`.
3. **Нет reconcile-цикла**: фаза остаётся `failed` до рестарта процесса.
4. Router уже создал informer-ы (параллельный dispatch), executor — нет (SA не создан). Dirty state: router видит NS активным, executor — нет. Вызовы → 503.
**Что было:** `EnsureNamespaceSA` и `registerNamespace` были void-функциями — ошибки только логировались, до `MarkPartFailed` не доходили. Executor subscriber всегда возвращал nil → namespace никогда не попадал в `NamespacePhaseFailed``RunReconciler` для executor был мёртвым кодом.
**Накопление при churn:** 10 ns/час × 1% API error rate = ~2.4 failed NS/сутки. За 30 дней = ~72 "мёртвых" записи. `Snapshot()` возвращает их в `idleObjectReaper` → лишние LIST запросы к k8s.
**Что сделано:**
- `setupSAAndRoleBindings` → возвращает `error`
- `EnsureNamespaceSA` → возвращает `error`, пробрасывает
- `registerNamespace` → возвращает `error` (SA + executorTypes) с `fmt.Errorf` wrapping
- Executor `AddFunc`/`ResyncFunc` → пробрасывают ошибку вместо `return nil`
- `RunReconciler` → принимает `*zap.Logger`, логирует каждый retry и исход
**Предложение (не реализовано):** reconcile-очередь в `inMemoryNamespaceManager` — см. FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §3 для кода.
**End-to-end flow:**
1. `EnsureNamespaceSA` fails (k8s 503) → `registerNamespace` returns error
2. Executor AddFunc returns error → `dispatch()``MarkPartFailed("executor")`
3. `deriveNamespacePhase``NamespacePhaseFailed`
4. `RunReconciler` tick (30s) находит namespace → `DispatchResync` → retry
5. Если API восстановился: `MarkPartActive``NamespacePhaseActive` → лог `resync succeeded`
**Статус:** не исправлено.
**Накопление при churn:** ликвидировано — failed NS автоматически выходят из этой фазы при восстановлении API.
**Ограничение:** нет max-retries. Namespace, у которого SA создать принципиально невозможно (например, удалённый k8s namespace), будет ретраиться вечно. Приемлемо на текущем масштабе.
---
@@ -229,11 +238,9 @@
## 3. Рекомендации (приоритизированные)
### P1 — Reconcile-очередь для failed NS
### P1 — Reconcile-очередь для failed NS — ✅ ЗАКРЫТ (`919e8439`)
Минимальное изменение без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §3`. Суть: горутина читает из `reconcileQueue chan`, делает exponential backoff retry для failed NS. Max 5 попыток.
**Влияние:** устраняет stuck-failed накопление, dirty state между router и executor.
Error propagation исправлена во всей цепочке: `setupSAAndRoleBindings``EnsureNamespaceSA``registerNamespace` → executor subscriber. `RunReconciler` логирует retry и исход.
### P2 — AdoptExistingResources после BootstrapAndDispatch
@@ -266,4 +273,4 @@
**Технически реализуемо** без breaking interface change. Полный код — в `FORENSIC_ARCHITECTURE_AUDIT_LEGACY_2026-05.md §5`.
**Вердикт:** не оправдано при текущей нагрузке. Реальные bottleneck — AdoptExistingResources race и stuck-failed, не mutex. Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.
**Вердикт:** не оправдано при текущей нагрузке. Реальный bottleneck — AdoptExistingResources race, не mutex. Sharded mutex — в backlog, актуально при >500 concurrent tenant с >1 onboarding/sec.