ADR-0058: Unified retention BC + monthly partitioning
Status: accepted Date: 2026-05-15 Deciders: Maxim Belkanov
Контекст
Storage roof: laptop 30-40 GB, prod 500 GB. До этого цикла retention
fragmented: 3 ticker’a (connector_events / canonical_events /
debug_artifacts) + 3 resource без cleanup (ingestion_job_runs,
offer_observations, offer_characteristic_raw). Hot tables
(observations ~125 MB/день; connector_events ~750 MB/день) растут
unbounded; DELETE-induced bloat + autovacuum lag.
Решение
Единый BC core/observability/retention владеющий cleanup для 8 PG
resources + MinIO RAW bucket. Hot tables — declarative monthly
partitioning + DROP PARTITION; lower-volume — batched DELETE с LIMIT.
MonthlyPartitionManagergeneric создаёт future partitions (prealloc N months) + drops outdated. Naming:<table>_YYYY_MM.- Migration 0136 connector_events + 0137 offer_observations конвертят existing tables в partitioned (rename → _legacy + new partitioned shell + default partition + current month).
- Per-resource
Cleanerimpl вinfra/pg/cleaners/. Cycle isolation: ошибка одного cleaner не abort’ит cycle. RetentionService.RunCycle()orchestration сpg_try_advisory_lock('retention_cleanup')для multi-replica safety.RetentionTickerс configurable interval (15m local / 1h prod).- Admin endpoint
GET /api/v1/admin/retention/statusпод AdminPathGate. - Config ENV-driven (
RETENTION_*namespace) с backward-compat aliases дляCONNECTOR_EVENTS_*,CANONICAL_EVENTS_RETENTION_*,S3_RAW_*. - Tier matrix через
.env.local-prod.example(aggressive) +deploy/.env.prod.template(relaxed). cmd/canonical-events-retention-workerdeprecated (logic переехала в RetentionService); compose service закомментирован.observability/connector_events_cleanup.goудалён (logic в newcleaners/connector_events.go).
Последствия
Плюсы
- Bounded growth для всех high-churn resources.
- DROP PARTITION instant (~50ms) vs DELETE минуты для high-volume tables.
- Один config namespace, один ticker, один advisory lock.
- Admin /retention/status даёт visibility на cleanup state.
Минусы
- +1 BC, +2 partitioning migrations.
- _legacy tables живут 30d/3d до natural выселения; следующая migration drops их.
- Default partition catches edge-case writes; operator alert через
tracium_retention_default_partition_rowsgauge (когда metrics добавятся в follow-up).
Нейтральные последствия
- Backward-compat ENV aliases читаются 1 cycle с warning’ами; убираются в следующий cycle.
Рассмотренные альтернативы
A — Distributed retention (per-BC ticker)
Каждый BC сам владеет cleanup. Текущий fragmented pattern. Отвергнут — ENV namespace fragmentation persists; 8 ticker’ов overhead.
B — PG pg_cron extension
Retention через SQL functions + scheduled jobs в БД. Opaque scheduling от Prometheus/observability, новая dependency, не fit для Go-first backend.
C — TimescaleDB hypertables
Native time-series solution. Install extension, change base image, migrate tables. Overkill для текущего volume (<5M rows/30d per table).
Ссылки
- Spec:
docs/superpowers/specs/2026-05-15-storage-retention-refactor-design.md - Plan:
docs/superpowers/plans/2026-05-15-storage-retention-refactor.md - Runbook:
docs/docs/40-operations/runbooks/retention.md - Migrations:
backend/migrations/0136_*.sql,0137_*.sql - BC:
backend/internal/core/observability/retention/