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.

  1. MonthlyPartitionManager generic создаёт future partitions (prealloc N months) + drops outdated. Naming: <table>_YYYY_MM.
  2. Migration 0136 connector_events + 0137 offer_observations конвертят existing tables в partitioned (rename → _legacy + new partitioned shell + default partition + current month).
  3. Per-resource Cleaner impl в infra/pg/cleaners/. Cycle isolation: ошибка одного cleaner не abort’ит cycle.
  4. RetentionService.RunCycle() orchestration с pg_try_advisory_lock('retention_cleanup') для multi-replica safety.
  5. RetentionTicker с configurable interval (15m local / 1h prod).
  6. Admin endpoint GET /api/v1/admin/retention/status под AdminPathGate.
  7. Config ENV-driven (RETENTION_* namespace) с backward-compat aliases для CONNECTOR_EVENTS_*, CANONICAL_EVENTS_RETENTION_*, S3_RAW_*.
  8. Tier matrix через .env.local-prod.example (aggressive) + deploy/.env.prod.template (relaxed).
  9. cmd/canonical-events-retention-worker deprecated (logic переехала в RetentionService); compose service закомментирован.
  10. observability/connector_events_cleanup.go удалён (logic в new cleaners/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_rows gauge (когда 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/