retention

package
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

defaults.go — величины ПЕТЛИ (не пороги) в единственном экземпляре.

Package retention — ОДНА фоновая петля уборки таблиц, чей рост задаёт внешний.

Почему петля живёт в общем фундаменте, а не у владельца

Таблица операций заведена у ВОСЬМИ владельцев, очереди дренажа — у семи (задачи #1360 и #1361). Восемь расписаний уборки и тринадцать расписаний очередей разошлись бы молча, а разойдясь — дали бы разный порог у разных владельцев при ОДНОМ контракте. Поэтому петля, партии, потолок проходов и наблюдаемость заводятся здесь ровно один раз.

Что заводится порознь — и почему это НЕ отступление от «один раз»

Порознь заводится только ПРЕДИКАТ, потому что предикат есть свойство СХЕМЫ таблицы, а она у семей разная: у операций признак терминальности `done`, у очереди дренажа — отметка доставки `sent_at`. Один предикат на обе семьи был бы либо неразбираемым (колонки нет — отказ `42703` на каждом проходе), либо холостым (колонка есть, писателей нет — снимает ноль молча). Оба отказа наблюдались в дереве, и оба живые.

Отсюда раскладка: петля — здесь; предикат операций — в `pkg/operations`, рядом с репозиторием и реконсайлером, владеющими `done`; предикат очереди — в `pkg/outbox`, рядом с дренажем, владеющим `sent_at` и `attempt_count`.

Порог — функция предиката ЧИТАТЕЛЯ, а не свойство колонки срока

Строку позволено снять не раньше момента, после которого НИ ОДИН читатель не изменил бы из-за неё своего исхода. Порог есть ПАРА «величина + источник часов»: уборщик и читатель обязаны судить одними часами, а разница источников входит в порог отдельным слагаемым и берётся из объявленной величины, а не выписывается числом.

Часы уборки — БАЗЫ: момент времени в сигнатуру уборщика не входит, предикат целиком в SQL. Это идиома дерева, а не изобретение — все уборщики, у которых вызывающий есть, приняли ровно эту форму; входом момент принимали ровно два, и это были те самые два, у которых вызывающего не было.

Форма петли (партии, потолок проходов, запись «РЕПЛИКИ:», наблюдаемость) задана приёмкой `services/iam/docs/engineering/acceptance/ retention-sweep-has-a-caller.md` §3.1–§3.4 и здесь не пересказывается: два места об одном предмете разошлись бы молча.

Index

Constants

View Source
const (
	// DefaultInterval — как часто идёт проход.
	//
	// Задаёт не пропускную способность, а задержку замечания: догон внутри
	// прохода обеспечивает DefaultMaxBatchesPerPass, а интервал определяет лишь,
	// насколько поздно уборка узнает о том, что появилось после прошлого прохода.
	DefaultInterval = 5 * time.Minute

	// DefaultBatch — сколько строк снимает один оператор.
	//
	// На порядок ниже потолка MaxBatch: партия задаёт длину одного DELETE, а тот
	// держит строки, которые в этот момент читает путь запроса. Уборка не вправе
	// быть причиной отказа на пути запроса.
	DefaultBatch = 1000

	// DefaultMaxBatchesPerPass — потолок партий за проход.
	//
	// Даёт скорость догона DefaultBatch × DefaultMaxBatchesPerPass за интервал —
	// двадцать тысяч строк за пять минут, то есть около 66 строк в секунду
	// УСТОЙЧИВО. Величина названа здесь затем, чтобы «догоняет ли уборка внешний
	// темп» был вопросом с числом, а не предположением: темп записи выше этого
	// означает, что догон не обеспечен, и это видно сравнением двух чисел.
	DefaultMaxBatchesPerPass = 20
)

Величины петли уборки по умолчанию.

Это величины РАСПИСАНИЯ, а не пороги: порог отвечает на вопрос «когда строку позволено снять» и выводится из предиката читателя своей таблицы, а эти три — на вопрос «как быстро уборка догоняет накопленное, не мешая пути запроса». Поэтому они общие для всех предметов и объявлены здесь один раз: восемь расписаний об одном предмете разошлись бы молча.

View Source
const MaxBatch = 10_000

MaxBatch — потолок величины партии.

Он не про вкус: партия задаёт длину одного оператора DELETE, а тот держит строки, которые в этот момент читает путь запроса. Величина выбрана на порядок выше рабочей (1000) — ручка остаётся ручкой, но «убрать всё одним оператором» ею не выражается.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Interval — как часто идёт проход.
	Interval time.Duration
	// Batch — сколько строк снимает один оператор.
	//
	// Партии, а не «удалить всё»: первый прогон после выкатки встречает всё,
	// что накопилось за жизнь стенда, и DELETE без предела на такой таблице —
	// длинная транзакция, удерживающая строки, которые горячий путь читает в
	// тот же момент. Уборка не вправе быть причиной отказа на пути запроса.
	Batch int
	// MaxBatchesPerPass — потолок числа партий за проход.
	//
	// Одна партия за тик даёт скорость догона `партия / интервал`, и если темп
	// записи выше — уборщик не догонит НИКОГДА, оставаясь зелёным по всякой
	// проверке «вызвался ли». Проход повторяет партию, пока она уходит полной,
	// но не более потолка: тогда длительность прохода ограничена сверху, а
	// догон меряется проходом, а не тиком.
	MaxBatchesPerPass int
}

Config — величины уборки. ОБЪЯВЛЯЮТСЯ в конфигурации сервиса и проверяются при старте: величина, уезжающая в SQL мимо объявления, невидима оператору.

Порогов здесь нет намеренно — они не настраиваются, а вычисляются из объявленных величин своего пакета. Настраиваемый порог был бы ручкой, которой его молча разводят с предикатом читателя, то есть ручкой заведения дефекта.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig — величины петли по умолчанию.

Отдельная функция, а не литерал у каждого вызывающего: литерал, скопированный в композиционный корень каждого владельца, и есть та копия, которая расходится молча.

func (Config) Validate

func (c Config) Validate() error

Validate отвергает величины, при которых уборка не работает либо работает неограниченно долго.

Отдельный экспортированный метод, потому что страж старта сервиса обязан звать ТОТ ЖЕ предикат, что и построитель: две проверки об одном предмете разошлись бы молча.

type Counts

type Counts struct {
	// Passes — сколько проходов исполнено. Ноль здесь отличает «убирать нечего»
	// от «петля не идёт вовсе».
	Passes int64
	// Removed — снято строк по каждому предмету за всё время.
	Removed map[string]int64
	// Failures — отказов по каждому предмету за всё время.
	Failures map[string]int64
}

Counts — накопленное за жизнь процесса. Читается сборщиком метрик: накопитель без читателя считает в никуда, и его ноль не утверждает ничего.

type PassResult

type PassResult struct {
	// Removed — снято строк по каждому предмету. Ключ есть у КАЖДОГО предмета
	// реестра, даже когда снято ноль.
	Removed map[string]int64
	// Batches — сколько партий ушло по каждому предмету.
	Batches map[string]int
	// Errs — отказ по каждому предмету, у которого он был.
	Errs map[string]error
}

PassResult — исход одного прохода, ПО КАЖДОМУ ПРЕДМЕТУ ОТДЕЛЬНО.

Раздельность не украшение: ноль по одному предмету означает либо «убирать нечего», либо «уборка не доходит до этой записи реестра», и общая величина не различает эти состояния.

func (PassResult) Err

func (r PassResult) Err() error

Err — объединённый отказ прохода; nil, если ни один предмет не отказал.

type Subject

type Subject struct {
	// Name — имя предмета. Совпадает с именем таблицы: имя попадает в отчёт
	// прохода и в метку метрики, и оператор обязан узнавать в нём таблицу.
	Name string
	// Grace — слагаемое порога. ВЫЧИСЛЯЕТСЯ из объявленных величин, а не
	// выписывается длительностью: копия разошлась бы с политикой молча и в
	// ОПАСНУЮ сторону.
	Grace time.Duration
	// Sweep — сам уборщик.
	Sweep SweepFunc
}

Subject — запись реестра: предмет, порог и уборщик.

type SweepFunc

type SweepFunc func(ctx context.Context, grace time.Duration, batch int) (removed int64, full bool, err error)

SweepFunc — один проход уборщика по одному предмету.

`grace` — слагаемое порога: уборщик снимает строки, чей срок истёк РАНЬШЕ чем `now() − grace` часами БАЗЫ. Момент времени параметром не приходит намеренно.

Возвращает число снятых строк и признак «партия ушла полной». Признак — не удобство: без него проход не отличает «убрал всё, что было» от «упёрся в партию», и уборка со скоростью одна партия за тик не догоняла бы внешний темп НИКОГДА, оставаясь зелёной по всякой проверке «вызвался ли».

type Sweeper

type Sweeper struct {
	// contains filtered or unexported fields
}

Sweeper — фоновая уборка по реестру предметов.

func New

func New(cfg Config, subjects []Subject, log *slog.Logger) (*Sweeper, error)

New собирает уборщика. Отказывает на негодных величинах: уборка, собранная с нулевой партией, исполняется и не убирает ничего — то есть выглядит работающей, будучи мёртвой.

func (*Sweeper) Pass

func (s *Sweeper) Pass(ctx context.Context) PassResult

Pass — один проход по всему реестру.

Экспортирован НАМЕРЕННО: «сборщик работает» обязано быть проверяемо без ожидания тикера. Довод не изобретён здесь — его говорит шапка живого уборщика дерева (`gateway/internal/idempotencypg/store.go`, `Store.Reap`), и он же снимает нужду подавать уборщику часы входом ради проверяемости.

func (*Sweeper) Start

func (s *Sweeper) Start(ctx context.Context)

Start поднимает фоновую петлю уборки.

Первый проход идёт СРАЗУ, не через интервал: он встречает всё, что накопилось за жизнь стенда, и откладывать его на интервал значило бы держать накопленное ещё и это время.

РЕПЛИКИ: на-реплику — уборка есть условный оператор `DELETE … WHERE <срок> < now() − порог` с пределом партии и клеймом строк (`FOR UPDATE SKIP LOCKED`). Строки заперты самим оператором, поэтому вторая реплика уносит только остаток, а на пустой выборке не делает ничего; к соседям проход не ходит. Общий замок не нужен: он купил бы отсутствие дубля ценой одиночной точки, у которой свой отказ.

func (*Sweeper) Stats

func (s *Sweeper) Stats() Counts

Stats — накопленное за жизнь процесса.

func (*Sweeper) Wait

func (s *Sweeper) Wait(d time.Duration) bool

Wait ждёт завершения петли после отмены контекста и отвечает, дождался ли.

Нужен пробе останова и останову процесса: петля, которую никто не дожидается, оставляет незавершённый проход в неопределённом состоянии для наблюдателя — хотя частично применённой партии не бывает by construction, партия есть один оператор.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL