Задача Егора 2ч.

This commit is contained in:
2026-07-17 01:27:02 +03:00
parent 3f685d3e22
commit 3d798c13e3
20 changed files with 2003 additions and 275 deletions

View File

@@ -81,7 +81,7 @@ sequenceDiagram
| `TenantRoutingDataSource` | Маршрутизирует запросы и атомарно публикует неизменяемый снимок `TenantConfig + DataSource` |
| `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы |
| `TenantWebMvcConfig` | Регистрирует `TenantInterceptor` и `AuthorizationInterceptor` в MVC-слое |
| `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов |
| `TenantConfigWatcher` | Периодически перечитывает `tenants.json`, сравнивает полный нормализованный снимок и повторяет неудачную синхронизацию с ограниченным backoff |
| `TenantConfigStore` | Задаёт явные атомарные операции upsert/remove и условную компенсацию persisted-конфигурации |
| `KubernetesTenantSecretUpdater` | Реализует `TenantConfigStore` через GET и условный PUT Kubernetes Secret по `resourceVersion`, проверяя TLS по service-account CA |
| `TenantLifecycleService` | Сериализует prepare/validate/migrate/persist/swap/remove и выполняет компенсацию при отказе |
@@ -128,10 +128,11 @@ sequenceDiagram
### Жизненный цикл тенанта
1. **Добавление или обновление через API:** `TenantLifecycleService` нормализует конфигурацию,
создаёт непубликуемый HikariCP candidate, проверяет реальное соединение и выполняет
Flyway. Затем `TenantConfigStore` читает актуальный Secret и применяет к нему только
upsert запрошенного домена.
1. **Добавление или обновление через API:** перед мутацией `TenantConfigWatcher` разбирает,
нормализует и при необходимости полностью применяет текущую mounted-проекцию как baseline.
Ошибка подготовки baseline прерывает API-операцию. Затем `TenantLifecycleService` создаёт
непубликуемый HikariCP candidate, проверяет реальное соединение, выполняет Flyway, а
`TenantConfigStore` читает актуальный Secret и применяет к нему только upsert домена.
2. **Межподовая запись:** `KubernetesTenantSecretUpdater` выполняет `GET` текущего
`tenants-secret`, нормализует и сортирует список, после чего отправляет полный объект
условным `PUT` с прочитанным `metadata.resourceVersion`. При `409 Conflict` актуальное
@@ -140,6 +141,9 @@ sequenceDiagram
не отправляет лишний `PUT`.
3. **Атомарная локальная публикация:** только после успешной или подтверждённой повторным
чтением персистенции одной публикацией заменяется связка `TenantConfig + DataSource`.
Успешный lifecycle возвращает внутренний `TenantLifecycleMutationResult` с конфигурацией
и `TenantSecretUpdateReceipt`, содержащей признаки `persisted`/`changed`, а также
семантические снимки `previousTenants` и `committedTenants`.
4. **Безопасный отказ и компенсация:** ошибка credentials, соединения, Flyway или Secret
закрывает candidate, не меняя действующий route. Компенсация прежним снимком разрешена
только для подтверждённой записи и только пока текущий Secret сохраняет выданный ей
@@ -151,7 +155,10 @@ sequenceDiagram
а прежний Hikari pool закрывается после завершения активных подключений либо по истечении
настраиваемого grace timeout.
6. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 секунд проверяет смонтированный
`tenants.json` и применяет добавление/удаление под тем же локальным lifecycle-monitor.
`tenants.json` и сравнивает все поля нормализованного `TenantConfig`: `name`, `domain`,
`url`, `username` и `password`. Для нового или изменённого тенанта lifecycle без повторной
записи Secret выполняет prepare → проверку соединения → Flyway → атомарный swap; отсутствующие
домены удаляются под тем же локальным monitor.
7. **Удаление:** `DELETE /api/database/tenants/{domain}` применяет к актуальному Secret только
удаление указанного домена через тот же условный `PUT`, затем атомарно исключает tenant
из маршрутизации и передаёт pool на drain. Для отказа локального удаления действуют те же
@@ -171,13 +178,25 @@ monitor по-прежнему не считается межподовой бл
цепочки и обязательную проверку hostname. Trust-all fallback отсутствует: ошибка CA или TLS
завершает персистенцию безопасным отказом.
`TenantConfigWatcher` сравнивает содержимое смонтированного из Secret `tenants.json` по
SHA-256, а не по `String.hashCode()`, чтобы изменение файла не пропускалось из-за
32-битной коллизии. На текущем этапе watcher активирует новые домены и удаляет отсутствующие,
но ещё не заменяет подключение существующего домена при изменении его URL или credentials.
Полная синхронизация такого изменения относится к проблеме №13; до её выполнения новое
значение сбрасывает readiness этого tenant в состояние ожидания, а pod не должен считаться
готовым по старому подключению.
`TenantConfigWatcher` использует SHA-256 содержимого `tenants.json` как идентификатор файловой
ревизии, но решения о запаздывающих проекциях принимает по нормализованным семантическим
снимкам `name`/`domain`/`url`/`username`/`password`. Хеш становится последним применённым
только после успешного разбора и полного lifecycle sync. Ошибка сохраняет прежний хеш,
снимает readiness и запускает экспоненциальные повторы через 30300 секунд; другая неудачная
файловая ревизия получает собственную попытку без ожидания backoff предыдущей.
После API-операции snapshot fence устанавливается только если `TenantSecretUpdateReceipt`
подтверждает реальное общее изменение: `persisted=true` и `changed=true`. Снимок до записи
и ранее ожидавшиеся committed-снимки временно считаются deferred, а новый committed-снимок —
ожидаемым. Поэтому при быстрых мутациях H0 → H1 → H2 запаздывающие H0 и H1 не откатывают
runtime, а H2 полностью синхронизируется и снимает fence. Неизвестный объединённый снимок,
которого нет среди deferred-состояний, также немедленно проходит полный sync и не теряет
изменение другого pod. Локальная персистенция (`persisted=false`) и подтверждённый no-op
(`changed=false`) fence не создают.
Если ошибка readiness произошла уже после успешного swap, следующий watcher retry видит
совпадающую активную конфигурацию, проверяет действующее соединение и восстанавливает
readiness без повторной миграции и второго swap.
### Liveness и readiness