Перейти к основному содержимому

Background Job: SessionRetentionJob

Назначение

SessionRetentionJob — фоновый maintenance-воркер модуля аутентификации identity, который физически удаляет отработавшие строки двух сущностей user-session.md по регламенту ретенции (раздел «Партиционирование / retention»):

  1. SignInConfirmationRequest (далее SICR) — короткоживущие запросы кодов: удалить записи старше SicrRetention (30 дней) по CreatedAt (индекс ix_sicr_created_at), независимо от Status (Confirmed/Expired/Exhausted/Superseded/Pending — все инертны спустя 30 дней; TTL самого кода 5 мин).
  2. RefreshSession — удалить терминальные сессии (Status ∈ {Rotated, Revoked, Expired}) старше RefreshTerminalRetention (90 дней) по UpdatedAt (момент терминализации, дальше не меняется). Живые (Active) не трогаются вне зависимости от возраста.

Retention — про очистку хранилища и PII: PhoneNumber/UserAgent/IpAddress в этих строках покидают БД не позже порога ретенции + один прогон. Аудит-историю сессий это не рушит: переходы уже отражены статусами до удаления, витрина user-activity.sessions.* живёт своей проекцией.

Отличать от соседних воркеров (README «jobs: expire-sessions, retention, access fan-out»):

  • expire-sessions (expire-sessions.md) — смена статуса Active → Expired (материализация TTL), запись остаётся в БД. Здесь — обратный конец жизненного цикла: удаление уже терминальной строки. Retention никогда не меняет статус.
  • access fan-out / expire-role-assignments (expire-role-assignments.md) — работают с RoleAssignment/правами, не с сессиями.

Metadata

ПолеЗначение
Статус документаdraft
Owning teamPIN Platform Core
Область действияконтекст identity, таблицы identity.sign_in_confirmation_requests / identity.refresh_sessions
Assumptions/ограниченияПороги — регламент хранения (SICR 30 дней, refresh-терминалы 90 дней), не security-контроль; при объёмах ADR-0056 (1–10k активных) партиционирования нет — удаление батчами. refresh_sessions не имеет выделенного retention-индекса (в отличие от ix_sicr_created_at): скан терминалов идёт по Status-guard + UpdatedAt, порционно по PK; при росте объёма — партиционирование (отложено). Self-FK ParentSessionId (Restrict) требует удаления цепочки ротаций «от листа к корню» (см. Алгоритм шаг 4)
ЗависимостиPostgreSQL (schema identity). Шина событий не используется (событий нет). Redis не нужен (ADR-0056)

Классификация

ПолеЗначение
Сервисidentity
Тип jobScheduled (фоновый воркер, суточный таймер, ночное окно)
Triggerвнутренний таймер каждые RetentionInterval (~24 ч, ночное окно), конфиг
Критичностьlow (housekeeping; деградация — рост таблиц и задержка вывода PII, не влияет на вход/авторизацию — они не зависят от наличия старых строк)

Расписание и параллелизм

АспектПравило
Scheduleкаждые RetentionInterval (default 24 ч; UTC), запуск в ночное окно (NightWindowUtc, default 02:00–05:00) для минимизации конкуренции с онлайном; лаг вывода PII ≤ RetentionInterval + длительность прогона
Параллелизмнесколько экземпляров разбирают непересекающиеся порции (взятая порция блокируется, остальные её пропускают); повторное удаление невозможно (строки уже нет); отдельная координация не требуется
Batch sizeBatchSize строк на порцию (default 5000); прогон повторяет выборку+удаление, пока порция полна — большой backlog разгребается за несколько итераций, длинные удаления не держат таблицу
Max durationMaxRunDuration (default 30 мин) — предохранитель; при превышении прогон завершается штатно, остаток берёт следующая ночь

Входная модель SessionRetentionOptions (config)

Внешнего runtime-входа нет; поведение задаётся конфигом. Актор — система (воркер); контекст вне tenant (сущности не tenanted, изоляция record-level по UserIdADR-0052, user-session.md): скан по всем строкам под предикат ретенции, cross-owner maintenance.

ПолеТипОбязательностьИсточникОписание
SicrRetentionTimeSpanДаconfig (регламент)Возраст SICR к удалению по CreatedAt (> 0, default 30 дней).
RefreshTerminalRetentionTimeSpanДаconfig (регламент)Возраст терминальных refresh_sessions к удалению по UpdatedAt (> 0, default 90 дней).
RetentionIntervalTimeSpanДаconfigПериод запуска (~24 ч, default 24 ч).
BatchSizeintДаconfigРазмер порции удаления (> 0, default 5000).
MaxRunDurationTimeSpanДаconfigВерхняя граница одного прогона (default 30 мин).

Алгоритм

Фазы независимы (нет связи между sign_in_confirmation_requests и refresh_sessions), порядок не важен. now фиксируется на старте прогона; отсечки cutoffSicr = now − SicrRetention, cutoffRefresh = now − RefreshTerminalRetention.

  1. Контекст. Актор = система, correlationId = jobRunId (на прогон), контекст вне tenant (cross-owner). Ключ идемпотентности не нужен: физическое удаление идемпотентно по определению — повторный проход не находит уже удалённых строк.
  2. Что читаем (порциями под блокировкой). Берётся только порция Id строк-кандидатов:
    • Фаза A — SICR: записи с CreatedAt < cutoffSicr, порядок по CreatedAt (индекс ix_sicr_created_at). Статус в предикат не входит — удаляются любые.
    • Фаза B — refresh-терминалы: Status ∈ {Rotated, Revoked, Expired} и UpdatedAt < cutoffRefresh, порядок по UpdatedAt. Active не матчится ни при каком возрасте. Параллельные экземпляры пропускают строки, взятые другим экземпляром/предыдущей незавершённой порцией.
  3. Правила. Переходов состояния нет — retention работает только с уже терминальными (refresh) или инертными (SICR) строками и удаляет их. Домен-инвариантов к проверке нет (дом — user-session.md); guard возраста/статуса целиком в предикате шага 2.
  4. Удаление. На порцию — удаление по набору Id из шага 2 (число удалённых = метрика прохода). Порядок с учётом FK:
    • Фаза B, self-FK ParentSessionId (Restrict): родителя нельзя удалить, пока на него ссылается живая дочерняя строка. В предикат добавлен анти-ссылочный guard — удаляются только «листья» терминального под-леса (нет ссылающегося потомка). Родитель, чей потомок ещё не устарел, забирается следующим прогоном (когда потомок тоже станет терминальным и старым). Дренаж цепочки сходится за несколько ночей; при объёмах ADR-0056 цепочки короткие.
    • Фаза A: на SICR никто не ссылается — простое батч-удаление (связь SICR → users — reference, удаление SICR родителя не затрагивает; user не удаляется).
  5. Согласованность. Каждая порция — своё атомарное изменение. Кросс-фазового/кросс-порционного изменения нет — фазы и порции независимы, частичный прогресс валиден и не откатывается.
  6. События. Нет. Удаление отработавших строк «тихое», side effects отсутствуют (в отличие от SessionRevokedEvent/UserSignedInKafkaEvent, рождающихся на Revoke/вход). Потребители (user-activity) уже получили терминальные статусы ранее — удаление старой строки их проекции не меняет.
  7. Результат. Наружу — метрики прогона (sicrDeleted, refreshDeleted, durationMs, backlogRemaining). Пустой проход — no-op success. Инфраструктурный сбой на порции — лог + переход к следующей фазе/тику; остаток берёт следующая ночь (guard делает повтор идемпотентным).

Согласованность изменения

ЭлементПравило
Границана порцию удаления; фазы A и B независимы
Сохраняемое состояниеудаление строк sign_in_confirmation_requests / refresh_sessions; иных таблиц не касается (никаких статусов)
Внешние вызовынет — только PostgreSQL. Шина событий/Redis не задействованы
Частичная ошибкаcontinue: сбой на порции/фазе логируется и не откатывает уже зафиксированные порции; строки остаются и берутся следующей ночью

Идемпотентность и retry

СценарийПравило
Job restartCheckpoint не нужен — «прогресс» это факт отсутствия строки; рестарт заново фильтрует по cutoff, удалённые не находятся
Duplicate itemНевозможно: удалённая строка не матчит предикат повторно; повторный проход даёт 0
Параллельные экземплярыБезопасны: построчная/попорционная блокировка разводит порции; одну строку удаляет ровно один экземпляр
Гонка с онлайномRetention трогает только терминалы (refresh) / записи старше 30 дней (SICR) — их не изменяют живые операции (confirm/refresh работают по Pending/Active); пересечения предикатов нет
Self-FK ParentSessionIdАнти-ссылочный guard + Restrict исключают удаление родителя при живом потомке; цепочка дренится «от листа» за несколько прогонов, без FK-нарушений
Повторяемая ошибкаИнфраструктурный сбой — порция не удалена, следующий tick повторяет; без бесконечного retry внутри прогона

Пред/постусловия

  • До прогона. Могут существовать SICR старше SicrRetention (любого статуса) и терминальные RefreshSession (Status ∈ {Rotated, Revoked, Expired}) старше RefreshTerminalRetention, удерживающие PII (PhoneNumber/UserAgent/IpAddress).
  • После прогона. Такие строки физически удалены, кроме родителей цепочки ротаций, заблокированных живым/молодым потомком (self-FK Restrict — дренаж «от листа» за несколько ночей), и строк неудавшейся порции. Active-сессии не тронуты при любом возрасте; статусы не менялись; событий нет; PII удалённых строк выведена из БД (регламент хранения, compliance). Воркер ничего не ожидает и не предоставляет соседним сервисам — потребители (user-activity) уже получили терминальные статусы ранее, удаление старой строки их проекцию не меняет.

Наблюдаемость

ТипПравило
Logsjob started (jobRunId, cutoffSicr, cutoffRefresh); batch deleted (phase, deletedCount); phase failed (phase, exception); job completed (sicrDeleted, refreshDeleted, durationMs, backlogRemaining). PhoneNumber/IpAddress/UserAgent в логи не пишутся (PII — их же и выводим)
Metricsidentity_session_retention_sicr_deleted_total / _refresh_deleted_total (counter); identity_session_retention_run_duration_ms (histogram); identity_session_retention_backlog (gauge — строки под предикатом ретенции, не удалённые на конец прогона, по таблицам); identity_session_retention_errors_total
Tracesspan на прогон с jobRunId, атрибуты phase, batchSize, deletedCount
Alertsнет успешного прогона > 2 × RetentionInterval (воркер стоит → PII задерживается сверх регламента — compliance-алерт); identity_session_retention_backlog устойчиво растёт (не успевает разгребать); identity_session_retention_errors_total растёт

SLO ретенции

ПоказательЦельОбоснование
Вывод PII в срок99% строк старше порога удалены за ≤ RetentionInterval + один прогон (SICR ≤ ~31 день, refresh-терминалы ≤ ~91 день от порога)регламент хранения; проверяется метрикой backlog
Отсутствие «залежавшихся» строкнет SICR старше SicrRetention + 2 × RetentionInterval; нет терминальных refresh старше RefreshTerminalRetention + 2 × RetentionInterval (кроме заблокированных живым потомком)алерт + метрика backlog
Идемпотентность0 повторных/ошибочных удалений при рестартах и N экземплярахпредикат + попорционная блокировка; удаление идемпотентно
FK-целостность0 нарушений self-FK ParentSessionId при удалении цепочеканти-ссылочный guard (шаг 4)

Связанный runbook

СценарийRunbook
Backlog растёт (воркер не успевает)увеличить BatchSize/частоту либо сузить окно; проверить блокировки PostgreSQL и план по ix_sicr_created_at; при устойчивом росте — рассмотреть партиционирование
Воркер стоит (нет прогонов)проверить сервис/pod, конфиг RetentionInterval/NightWindowUtc; compliance-риск — PII задерживается, но вход/авторизация не деградируют
Рост errors_totalпроверить доступность PostgreSQL/schema и FK-нарушения (self-FK цепочек); строки останутся и удалятся после восстановления

Тесты

ТестОжидание
Empty batchнет строк под предикатом — no-op success, 0 удалений, событий нет
SICR старше 30 днейудаляются независимо от статуса (Confirmed/Expired/Exhausted/Superseded/Pending); строки ≤ 30 дней остаются
SICR-статус не влияетConfirmed-запрос возрастом 40 дней удалён так же, как Expired
Терминальный refresh старше 90 днейRotated/Revoked/Expired с UpdatedAt < cutoff удалены; Active любого возраста — остаётся
Refresh младше порогатерминальный Revoked возрастом 10 дней — не удаляется
Цепочка ротаций (self-FK)родитель с живым/молодым потомком не удаляется (без FK-нарушения); удаляется после устаревания потомка
Идемпотентность/рестартповторный прогон по тем же данным удаляет 0 строк
Параллельные экземплярыдве копии воркера не удаляют одну строку/порцию дважды
Нет событий/статусовни одного события; ни один Status не изменён
Cancellationостановка между порциями — graceful stop, зафиксированное сохранено

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Дом полей/инвариантов/индексов/ретенции: ../domain/user-session.md (SignInConfirmationRequest/RefreshSession, статусы, ix_sicr_created_at, self-FK ParentSessionId, раздел «Партиционирование / retention»), ../domain/user.md.
  • Обзор/эксплуатация: ../README.md («jobs», «Alerts»); соседний воркер — expire-sessions.md (терминализация статуса, без удаления), expire-role-assignments.md.
  • Витрина сессий пользователя (проекция статусов): ../../user-activity/README.md (ADR-0054).
  • Решения: ADR-0052 (tenancy, record-level), ADR-0056 (simplicity-first, PostgreSQL-only, polling/batch, без Redis).