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
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<!--
CEO plan E2 (Tiered perf): per-dictionary Redis projection feature flag.
`redis_projection_enabled` BOOLEAN на dictionary_definitions:
- false (default) — projection-writer пропускает Kafka events этого
dict'а, Redis для него не материализуется. read-api продолжает
читать из PG read replica (current path).
- true — projection-writer пишет updates в Redis. read-api в будущем
сможет route'ить queries в Redis для 10k+ RPS dicts. Read-api
routing — отдельная feature, не часть этой migration.
Default false — backward-compat: сейчас НИ ОДИН dict не помечен → Redis
становится пустым после deploy + первого refresh cycle. Это OK
потому что read-api сегодня НЕ читает из Redis (просто dead writes).
-->
<changeSet id="0017-redis-projection-flag" author="ordinis">
<comment>Per-dict Redis projection feature flag (CEO plan E2)</comment>
<addColumn tableName="dictionary_definitions">
<column name="redis_projection_enabled" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
<createIndex indexName="idx_dict_def_redis_proj"
tableName="dictionary_definitions">
<column name="redis_projection_enabled"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -26,5 +26,6 @@
<include file="changes/0014-webhook-deliveries.xml" relativeToChangelogFile="true"/>
<include file="changes/0015-backfill-latlon-geometry.xml" relativeToChangelogFile="true"/>
<include file="changes/0016-record-dependents-index.xml" relativeToChangelogFile="true"/>
<include file="changes/0017-redis-projection-flag.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>