feat(redis-projection): per-dict feature flag (CEO plan E2 tiered perf)

Per-dictionary opt-in для Redis projection materialization. Default false
— projection-writer пропускает event'ы того dict'а, никаких dead writes
в Redis. Включается админ'ом через UI checkbox в Metadata tab.

Migration 0017:
- ALTER TABLE dictionary_definitions ADD COLUMN redis_projection_enabled
  BOOLEAN NOT NULL DEFAULT false.
- Index idx_dict_def_redis_proj для быстрого filtering в list queries.

Backend:
- DictionaryDefinition entity: new field + getter/setter.
- DictionaryResponse DTO: returns flag в response.
- CreateDictionaryRequest DTO: optional Boolean (null/missing = no change).
- DictionaryDefinitionService.create + updateSchema: применяет flag из request.
- ProjectionWriter (projection-writer service):
  * upsert() — early-return + skip counter если flag=false на dict.
  * delete() — symmetric: skip если flag=false (защита от stale keys
    после flag-flip; полный cleanup TODO Phase 2).
  * New metric: ordinis_projection_records_skipped_total.

Admin UI:
- DictionaryEditorDialog Metadata tab: checkbox "Включить Redis-проекцию"
  с описанием. Apply через CreateDictionaryRequest payload.
- DictionaryDefinition + CreateDictionaryRequest types updated.
- i18n RU/EN: schema.redisProjection.{label,hint}.

Что НЕ делается (deferred Phase 2):
- read-api Redis routing — read-api сейчас всегда идёт в PG read replica.
  Когда dict опт-инится в флаг, projection материализуется, но read-api
  ещё не использует. Read routing — отдельная feature с benchmark proof
  (через JMH + k6 SLO test).
- One-shot cleanup job для stale keys после flag-flip from true→false.

Behavior change в production:
- До patch: projection-writer пишет в Redis для ВСЕХ dict events (40
  dictionaries). Default flag=false после migration → projection-writer
  пропускает всё → Redis keys для existing dicts становятся stale.
  Это OK потому что read-api НИКОГДА не читал из Redis (writes были
  dead). Stale keys eventually evicted Redis maxmemory policy.
- После patch: только dicts с redis_projection_enabled=true получают
  материализацию. По default — нет. Admin opt-ins per-dict через UI.

Verify:
- mvn test (full reactor): SUCCESS, all green.
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS (migration 0017 applied).
- pnpm tsc --noEmit: clean.
- pnpm test (vitest): 89/89 PASS.
- pnpm build: clean.
This commit is contained in:
Zimin A.N.
2026-05-08 12:34:58 +03:00
parent c11044c32e
commit 0a8275ef4c
10 changed files with 117 additions and 1 deletions
+4
View File
@@ -192,6 +192,8 @@ i18n
'schema.tabs.schema': 'Поля',
'schema.tabs.preview': 'JSON',
'schema.tabs.events': 'События',
'schema.redisProjection.label': 'Включить Redis-проекцию для этого справочника',
'schema.redisProjection.hint': 'CEO plan E2 (tiered perf). По умолчанию выключено — read-api идёт в PG read replica. Включай когда dict упирается в RPS limit (целевые 10k+ RPS read через Redis hot cache). Projection-writer материализует updates per locale в Redis. Read-api routing появится отдельным feature — пока flag готовит почву.',
'schema.events.intro': 'Превью JSON событий, которые будут опубликованы в Kafka после save справочника. Данные подставлены на основе типов полей schema. Полезно интеграторам (Альтум, Геопортал) для понимания shape\'а до live-выкатки.',
'schema.events.topic': 'Topic',
'schema.events.disclaimer': 'Это client-side preview. Реальные события содержат actual data + traceId/spanId из request scope. Schema events ordinis-events-api/src/main/java/.../events/ — единственный source of truth.',
@@ -541,6 +543,8 @@ i18n
'schema.tabs.schema': 'Fields',
'schema.tabs.preview': 'JSON',
'schema.tabs.events': 'Events',
'schema.redisProjection.label': 'Enable Redis projection for this dictionary',
'schema.redisProjection.hint': 'CEO plan E2 (tiered perf). Off by default — read-api hits PG read replica. Turn on when this dict approaches RPS limit (target 10k+ RPS read through Redis hot cache). The projection writer materializes per-locale updates to Redis. Read-api routing will arrive in a separate feature; the flag prepares the ground.',
'schema.events.intro': 'Preview of JSON events that will be published to Kafka after the dictionary is saved. Data is filled in based on schema field types. Useful for integrators (Altum, Geoportal) to understand the shape before going live.',
'schema.events.topic': 'Topic',
'schema.events.disclaimer': 'This is a client-side preview. Real events contain actual data + traceId/spanId from request scope. The events ordinis-events-api/src/main/java/.../events/ is the single source of truth.',