quota

package
v1.3.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: 15 Imported by: 0

Documentation

Overview

Package quota держит ОБЩИЕ части учёта потолков: то, что у всех владельцев одинаково, и ровно это.

Что здесь есть и чего здесь нет

Число, из которого вычитают, остаётся в базе владельца ресурса — списание обязано идти в транзакции вставки, а распределённой транзакции в стеке нет. Поэтому здесь НЕТ ни таблицы учёта, ни списания, ни отказа.

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

Почему это не «ещё один слой», а устранение шестой копии

Синхронизатор — предмет, которого не существовало вовсе: столбцы под него (`limit_revision`, `synced_at`) были заведены в пяти схемах, индекс под них создан, а писателя не было ни одного. Написанный пять раз порознь, он разошёлся бы на правиле старшинства — то есть там, где расхождение не видно ни одной из сторон: каждая копия по отдельности верна, а администратор получает разный эффект от одного и того же действия в разных доменах.

Index

Constants

View Source
const NotDeployed = "not-deployed"

NotDeployed — значение ручки, объявляющее, что домена величин в этой установке нет.

То же написание служит и состоянием курсора синхронизации (AuthorityAbsent): одно написание в двух ролях, а не два написания одного смысла. Разойтись они не могут by construction — второе выведено из первого.

Variables

This section is empty.

Functions

func ExpectedRefusalBodies

func ExpectedRefusalBodies(o RefusalOwner) (map[string]string, error)

ExpectedRefusalBodies — тела, какими их рендерит шаблон для этого владельца.

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

func ListStates

func ListStates(
	ctx context.Context, q Querier, schema, carrierType, carrierID string,
) ([]quotaread.State, error)

ListStates отдаёт строки учёта носителя из названной схемы.

`ORDER BY kind` — по КОЛОНКЕ ПОРЯДКА, а не по времени вставки. Строки заводит материализация одной транзакцией, поэтому метка времени у них совпадает, и сортировка по ней разрешалась бы идентификатором, то есть случайной строкой: ответ переставлял бы виды от прогона к прогону, а клиент, ведущий состояние по индексу, читал бы перестановку как изменение (`api-conventions.md` §«Порядок повторяющегося поля — часть контракта либо его нет»).

Пустой срез означает «строк учёта ещё нет» и НИЧЕГО не говорит о пределах: различать это состояние обязана полоса чтения (`quotaread.Band`). Здесь сообщается только то, что видно в таблице.

func ReadPosture

func ReadPosture(a Authority, serviceDomain string) quotaread.Posture

ReadPosture — объявление домена величин, каким его обязана назвать ВИТРИНА.

ЕДИНСТВЕННЫЙ перевод объявления в посадку витрины, и он здесь намеренно. Владельцев пятеро; написанный у каждого, этот `if` был бы пятью копиями одного решения — и разошёлся бы там, где расхождение не видит ни один из пяти: у арендатора ОДНОГО домена витрина говорила бы про установку, у соседнего — про поломку, при одной и той же посадке.

Перевод стоит у объявления, а не у витрины, потому что предмет здесь — объявление: витрина о ручке не знает и знать не должна. serviceDomain — имя сервиса, называющее источник ответа в машинном признаке. Приходит параметром, а не полем объявления: у балансировщика каталог величин знает виды как `loadbalancer`, а ответ на проводе обязан называть источником `nlb`, — вывести одно из другого нельзя, и объявление домена величин об этом различии не знает.

func RefusalFunctionBodies

func RefusalFunctionBodies(sqlText string) map[string]string

RefusalFunctionBodies извлекает тела функций отказа из текста SQL.

Один извлекатель на обе стороны сравнения — и на рендер шаблона, и на дерево, — чтобы разбор не мог разойтись сам с собой. Отсюда же требование к вызывающему: сторона-эталон обязана дать ПОЛНЫЙ набор непустых тел, иначе «совпало» означало бы, что извлечение не состоялось у обеих сторон сразу.

Возвращает последнее определение каждой функции в переданном тексте: в файле их не бывает двух, а по цепи побеждает позднейшее, и склейка текстов в порядке версий даёт ровно это.

func RefusalFunctionNames

func RefusalFunctionNames() []string

RefusalFunctionNames — имена, чьи тела сверяются. Копия, а не сам срез.

func RenderRefusalMigration

func RenderRefusalMigration(o RefusalOwner) (string, error)

RenderRefusalMigration отдаёт содержимое файла миграции владельца.

Рендер — единственный способ получить этот текст: и генератор, и гейт зовут ЭТУ функцию, поэтому «что сгенерировано» и «что проверяется» не могут разойтись. Второй рендерер сделал бы гейт проверкой самого себя.

func StartLimitSync

func StartLimitSync(
	ctx context.Context,
	db Execer,
	authority Authority,
	src Source,
	schema string,
	cfg Config,
	logger *slog.Logger,
) (func(), error)

StartLimitSync заводит фонового тянущего величины СТРОГО по объявлению домена величин и возвращает его останов.

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

src обязан быть согласован с объявлением, и рассогласование — отказ при сборке, а не предпочтение одной из сторон:

  • домен объявлен адресом, источника нет ⇒ подъём исполнялся бы по расписанию и не делал ничего, оставаясь на вид работающим;
  • домен объявлен отсутствующим, а источник есть ⇒ потребитель дозвонился туда, куда по объявлению обращаться не должен. Решение принято дважды, и второе место невидимо оператору.

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

Types

type Authority

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

Authority — разрешённое объявление домена величин.

Тип суммы, а не строка: значение «не развёрнут» не может быть случайно использовано как адрес, потому что адреса у него нет вовсе.

func ResolveAuthority

func ResolveAuthority(d Declaration) (Authority, error)

ResolveAuthority разбирает объявление и проверяет ОБЕ его половины разом.

Одним вызовом намеренно: две отдельные проверки — форма объявления и наличие удостоверения — дают потребителю возможность позвать первую и забыть вторую, а забытая половина и есть предмет KAN-Q1-05. Забыть здесь нечего: проверка одна.

func (Authority) Deployed

func (a Authority) Deployed() bool

Deployed — развёрнут ли домен величин в этой установке.

func (Authority) Endpoint

func (a Authority) Endpoint() string

Endpoint — адрес домена величин. Пуст, когда домен объявлен отсутствующим.

func (Authority) State

func (a Authority) State() AuthorityState

State — наблюдаемое состояние, которое доедет до строки курсора.

type AuthorityState

type AuthorityState string

AuthorityState — наблюдаемое состояние домена величин, каким его объявил оператор этому потребителю.

Живёт в строке курсора синхронизации и существует ради РАЗЛИЧИМОСТИ: после снятия авторитета «ни одна строка не синхронизирована за всё время» становится штатным и вечным, поэтому нулевые накопительные счётчики перестают что-либо означать. Причина обязана НАЗЫВАТЬСЯ, а не выводиться из нуля.

const (
	// AuthorityUnknown — объявления ещё не было ни от одного подъёма. Умолчание
	// столбца: между применением миграции и первым стартом процесса не сказано
	// ничего, и это третье состояние, а не одно из двух законных.
	AuthorityUnknown AuthorityState = "unknown"
	// AuthorityPresent — домен величин объявлен адресом.
	AuthorityPresent AuthorityState = "deployed"
	// AuthorityAbsent — домен величин объявлен отсутствующим.
	AuthorityAbsent AuthorityState = AuthorityState(NotDeployed)
)

func AuthorityStates

func AuthorityStates() []AuthorityState

AuthorityStates — закрытый словарь состояний, признаваемых столбцом курсора.

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

type Change

type Change struct {
	// Kind — вид ресурса точечным токеном платформы.
	Kind string
	// Scope / ScopeID — где величина назначена. У умолчания ScopeID пуст.
	Scope   Scope
	ScopeID string
	// Value — назначенная величина. На отзыве не применяется (см. Withdrawn).
	Value int64
	// Revision — глобально возрастающий номер изменения у владельца величин. Он
	// же курсор дельты и он же то, с чем сверяется снимок строки.
	Revision int64
	// Withdrawn — величина снята. Строка проекции убирается, и следующее
	// списание разрешит потолок заново, получив следующую по старшинству
	// область.
	Withdrawn bool
}

Change — одно изменение величины, каким его видит владелец проекции.

Нейтрально к транспорту НАМЕРЕННО: пакет не импортирует сгенерённые стабы, поэтому синхронизатор проверяем без соседа и без сети, а владелец волен приносить дельту чем угодно.

func (Change) Validate

func (c Change) Validate() error

Validate отвергает изменение, которое нельзя применить не догадываясь.

Отвергается ИМЕННО ЗДЕСЬ, а не в SQL: оператор, получивший неизвестную область, тихо не совпал бы ни с одной строкой и отчитался бы успехом — «применено 0», неотличимое от «нечего применять».

type Config

type Config struct {
	// Interval — как часто тянуть дельту. Ноль означает умолчание.
	Interval time.Duration
	// PageSize — размер страницы дельты. Ноль означает умолчание.
	PageSize int32
	// PullTimeout — срок одного прохода. Ноль означает умолчание.
	PullTimeout time.Duration
}

Config — настройки синхронизатора.

type Declaration

type Declaration struct {
	// Knob — имя ручки, объявляющей домен величин.
	Knob string
	// Value — её сырое значение, как оно пришло из посадки.
	Value string
	// TransportKnob — имя ручки удостоверения, которым потребитель
	// представляется домену величин.
	TransportKnob string
	// TransportRequired — обязано ли удостоверение быть объявлено на ЭТОЙ
	// посадке.
	//
	// Посадочное правило у ребра величин ТО ЖЕ, что у остальных рёбер службы:
	// проверяемый транспорт требуется в боевом режиме. Заводить ребру величин
	// собственную строгость значило бы завести различие, которое пришлось бы
	// помнить, — и первым же следствием стал бы неподнимаемый локальный стенд
	// там, где все прочие рёбра ходят открытым текстом законно.
	//
	// Незаданное ОБЪЯВЛЕНИЕ (поле Value) режимом не смягчается никогда: оно
	// означает не «транспорт попроще», а «оператор не выбрал между „потолки
	// действуют" и „потолков нет"», и подставить за него разумное умолчание
	// нельзя ни в каком режиме.
	TransportRequired bool
	// TransportDeclared — объявлено ли это удостоверение.
	//
	// Половина пары хуже отсутствия обеих, потому что ВЫГЛЯДИТ настроенной:
	// адрес есть, обращение уходит, сосед отвечает «требуется сертификат
	// клиента», а наш потребитель классифицирует это как недоступность соседа.
	// Со стороны — «сосед недоступен», хотя сосед исправен и говорит ровно то,
	// что должен (`security.md` §«Контроль, у которого нет МЕХАНИЗМА
	// исполниться»).
	TransportDeclared bool
}

Declaration — сырое объявление домена величин, как его прочитал потребитель из своей посадки.

Имена ручек здесь не для красоты: они попадают в текст отказа старта, а он обязан восстанавливать следующий шаг оператора. Без имени ручки стенд не поднять — это одно из трёх мест, прямо выведенных из-под запрета `security.md` §«Публичные артефакты».

type Execer

type Execer interface {
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

Execer — то единственное, что нужно проекции от базы: два глагола. Транзакция, соединение из пула — подходит любое.

Формы драйвера взяты как есть, без своих обёрток: обёртка добавила бы место, где адаптер может оказаться СНИСХОДИТЕЛЬНЕЕ настоящего носителя, а проверяемость от неё не зависит — `pgconn.CommandTag` есть структура, `pgx.Row` есть интерфейс, и подставной носитель выражается обоими без единой строки перехода.

type Health

type Health struct {
	// Pulls / Rows — сколько проходов состоялось и сколько строк снимка они
	// затронули за всю жизнь процесса.
	Pulls, Rows int64
	// LastSuccess — момент последнего состоявшегося прохода; нулевой, если его
	// не было ни разу.
	LastSuccess time.Time
	// NeverSynchronised — за всю жизнь не состоялось НИ ОДНОГО прохода.
	//
	// Отдельным полем, а не выводом «Pulls == 0» у каждого читателя: вывод,
	// сделанный в двух местах, разойдётся в третьем.
	NeverSynchronised bool
}

Health — состояние тянущего, каким его читает наблюдатель.

Тип заведён потому, что у накопительных счётчиков ДОЛЖЕН быть читатель: величина, которую никто не читает, ничего не делает заметным. Читатель в прод-коде — сам `Run`: он называет эти поля в строке отказа и в строке останова.

type PgProjection

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

PgProjection — проекция поверх таблицы `project_resource_quotas` названной схемы.

func NewPgProjection

func NewPgProjection(db Execer, schema string) (*PgProjection, error)

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

func (*PgProjection) ApplyChange

func (p *PgProjection) ApplyChange(ctx context.Context, ch Change) (int64, error)

ApplyChange правит строки снимка, адресуя их по столбцам строки учёта.

Почему отзыв УДАЛЯЕТ строку

Отозванная величина не имеет значения, которым можно заменить снимок: верным становится следующий по старшинству потолок, а знает его владелец величин. Строка снимается, и ближайшее списание разрешит потолок заново. Потребление при этом не теряется — материализация затравляет строку фактом, а не нулём.

func (*PgProjection) ClaimPass

func (p *PgProjection) ClaimPass(ctx context.Context, lease time.Duration) (string, bool, error)

ClaimPass берёт проход на одну реплику и отдаёт курсор, с которого он продолжит.

Почему клейм СТРОКИ, а не замок прохода

Строка курсора одна на владельца и по построению есть предмет взаимного исключения: тот, кто её взял, и есть тот, кто тянет дельту. Условная правка — один оператор, поэтому конкурирующая реплика не «увидела свободно и взяла», а не совпала с условием: второй писатель ждёт снятия блокировки строки, после чего Postgres перепроверяет предикат и не находит совпадения (ban #10 — инвариант держит оператор базы, а не проверка в коде).

Что это чинит

Проход ходит к ВЛАДЕЛЬЦУ ВЕЛИЧИН по сети. Без развода N реплик тянут одну и ту же дельту одновременно: сосед получает N-кратную подачу ровно тогда, когда автомасштабирование подняло реплики, то есть под нагрузкой. Плюс курсор двигали N писателей без условия, поэтому отставший возвращал его НАЗАД, а накопительные счётчики — то самое, чем «ни одной строки за всё время» отличается от «нечего применять», — считали одну и ту же работу N раз.

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

lease — сколько проход считается занятым с момента последней отметки. Меньше периода тика он быть не должен: тогда две реплики с разной фазой успевали бы взять проход в одном окне.

func (*PgProjection) Heartbeat

func (p *PgProjection) Heartbeat(ctx context.Context) error

Heartbeat отмечает состоявшийся проход, не трогая ни курсор, ни счётчик работы.

Существует ради различимости двух картин, которые иначе выглядят одинаково: «синхронизатор жив, менять нечего» и «синхронизатор не работает». Первая двигает отметку времени, вторая — нет.

func (*PgProjection) LoadAuthority

func (p *PgProjection) LoadAuthority(ctx context.Context) (AuthorityState, error)

LoadAuthority читает объявленное состояние домена величин, ничего не занимая.

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

func (*PgProjection) LoadCursor

func (p *PgProjection) LoadCursor(ctx context.Context) (string, error)

LoadCursor читает курсор дельты, ничего не занимая.

Не участвует в проходе (тот берёт курсор клеймом) и существует ради наблюдаемости и проб: «на чём остановились» обязано быть доступно, не двигая отметку занятости.

func (*PgProjection) RecordAuthority

func (p *PgProjection) RecordAuthority(ctx context.Context, state AuthorityState) error

RecordAuthority записывает в строку курсора то, что оператор объявил этому потребителю о домене величин.

Зачем состояние, если есть накопительные счётчики

Счётчики заведены затем, чтобы тянущий, не применивший ни строки, не выглядел здоровым. После снятия авторитета «ни строки не применено» становится штатным и ВЕЧНЫМ: сигнал застоя срабатывал бы всегда, а проверку, кричащую на нормальной работе, перестают читать вместе с настоящими находками. Поэтому причина НАЗЫВАЕТСЯ, а не выводится из нуля.

Чего оператор НЕ трогает — и это несущее

Ни курсор, ни оба накопительных счётчика, ни отметку занятости прохода. Первые три — потому что перевод объявления в «не развёрнут» не есть работа тянущего: обнулить их значило бы стереть свидетельство того, что он работал (KAN-Q4-13). Отметку — потому что она служит арендой прохода: сдвинув её на подъёме, мы отложили бы первый проход после возврата авторитета на целый срок аренды, причём у КАЖДОЙ реплики.

func (*PgProjection) SaveCursor

func (p *PgProjection) SaveCursor(ctx context.Context, cursor string, appliedRows int64) error

SaveCursor двигает курсор и прибавляет применённые строки к накопительному счётчику.

Счётчик накопительный НАМЕРЕННО: он и есть то, чем «ни одна строка не синхронизирована за всё время» отличается от «сейчас нечего применять». Без него мёртвый синхронизатор выглядит точно так же, как живой на неизменной конфигурации.

type Projection

type Projection interface {
	// ClaimPass берёт проход на ОДНУ реплику и отдаёт курсор, с которого он
	// продолжит. Пустой курсор означает «с начала времён» — ровно то, что нужно
	// проекции, ни разу не тянувшей дельту.
	//
	// Второе значение говорит, достался ли проход нам. Проигрыш — штатный исход,
	// а не отказ: дельта идемпотентна по ревизии, пропущенный тик ничего не
	// теряет, следующий догонит.
	//
	// lease — сколько проход считается занятым с момента последней отметки.
	ClaimPass(ctx context.Context, lease time.Duration) (cursor string, ok bool, err error)
	// ApplyChange правит строки снимка, адресуя их по столбцам, и отдаёт число
	// затронутых строк.
	ApplyChange(ctx context.Context, ch Change) (int64, error)
	// SaveCursor двигает курсор и прибавляет число применённых строк к
	// накопительному счётчику. Зовётся ТОЛЬКО когда есть что записать.
	SaveCursor(ctx context.Context, cursor string, appliedRows int64) error
	// Heartbeat отмечает состоявшийся проход, ничего больше не меняя.
	//
	// Отдельный глагол, а не побочный эффект сохранения курсора: иначе «жив» и
	// «работал» слились бы в одно число, и различить остановившийся
	// синхронизатор от живого на неизменной конфигурации стало бы нечем —
	// оба применяют ноль строк.
	Heartbeat(ctx context.Context) error
}

Projection — проекция величин у владельца ресурса.

type Querier

type Querier interface {
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}

Querier — то единственное, что нужно чтению от носителя: один глагол. Транзакция, соединение из пула — подходит любое.

Отдельный интерфейс, а не расширение `Execer`: у `Execer` есть свои реализации, и добавление третьего метода потребовало бы от них глагола, которым они не пользуются, — то есть заставило бы дописать код ради формы.

type RefusalOwner

type RefusalOwner struct {
	// Service — каталог сервиса под `services/`.
	Service string
	// Schema — имя схемы, в которой живёт таблица учёта.
	Schema string
	// Migration — имя файла миграции без каталога, ЛИБО пусто.
	//
	// Пусто означает: цепь этого владельца СВЕДЕНА, и отказ живёт в её первичной
	// миграции, а не в отдельном отрендеренном файле. Это не послабление и не
	// исключение из проверки — сверка тела функции (см. `RefusalFunctionBodies`)
	// идёт у ВСЕХ владельцев одинаково; отпадает только сверка файла целиком,
	// которой у сведённой цепи нет предмета by construction: рендерить файл в
	// такую цепь значило бы завести миграцию с версией НИЖЕ уже применённой
	// первичной, то есть версию, которую мигратор не применит вовсе.
	Migration string
}

RefusalOwner — владелец, которому рендерится файл.

func RefusalOwners

func RefusalOwners() []RefusalOwner

RefusalOwners отдаёт перечень владельцев в устойчивом порядке.

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

func (RefusalOwner) RendersOwnFile

func (o RefusalOwner) RendersOwnFile() bool

RendersOwnFile говорит, рендерится ли этому владельцу отдельный файл миграции.

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

type Scope

type Scope string

Scope — область, на которой величина назначена. Порядок старшинства — PROJECT > ACCOUNT > DEFAULT — выражен методом AtLeast, а не сравнением строк в месте применения: правило одно, и жить оно обязано в одном месте.

const (
	ScopeDefault Scope = "DEFAULT"
	ScopeAccount Scope = "ACCOUNT"
	ScopeProject Scope = "PROJECT"
)

func (Scope) AtLeast

func (s Scope) AtLeast(other Scope) bool

AtLeast отвечает, не ниже ли эта область, чем other.

func (Scope) Valid

func (s Scope) Valid() bool

Valid отвечает, знает ли платформа такую область.

Неизвестная область — НЕ «примем как умолчание»: это ровно тот случай, когда сосед сказал то, чего мы не понимаем, и применить это значило бы записать в проекцию догадку. Классификация чужого ответа без корзины «прочее».

type Source

type Source interface {
	// ListChangedSince отдаёт изменения с ревизией больше курсора и следующий
	// курсор. Следующий курсор возвращается ВСЕГДА, включая пустую страницу.
	ListChangedSince(ctx context.Context, cursor string, pageSize int32) ([]Change, string, error)
}

Source — откуда владелец берёт дельту. Реализуется клиентом к владельцу величин.

type Syncer

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

Syncer тянет дельту величин и правит проекцию.

func NewSyncer

func NewSyncer(src Source, proj Projection, cfg Config, log *slog.Logger) (*Syncer, error)

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

func (*Syncer) Health

func (s *Syncer) Health() Health

Health отдаёт накопленное вместе с признаком «ни одного прохода за всю жизнь».

func (*Syncer) Run

func (s *Syncer) Run(ctx context.Context)

Run гоняет проходы до отмены контекста.

Отказ прохода НЕ фатален и НЕ тих: он логируется и повторяется следующим тиком. Величина — не путь запроса, недоступность соседа здесь не должна ронять сервис; но и «молча продолжаем» тоже не годится, поэтому каждый отказ называется, а «ни одной строки за всё время» видно по накопительному счёту.

РЕПЛИКИ: клейм — проход берёт одна реплика условной правкой строки курсора (Projection.ClaimPass); проигравший пропускает тик.

func (*Syncer) RunAfterInterval

func (s *Syncer) RunAfterInterval(ctx context.Context)

RunAfterInterval — то же, что Run, но первый проход откладывается на один тик.

Зовётся тем, кто проход УЖЕ СДЕЛАЛ сам (подъём). Прежде такого глагола не было, и подъём вместе с циклом давали ровно ДВА прохода на старт с разницей в десятки миллисекунд — оба про одно событие, оба в загрузочную бурю, когда сосед и без того на пределе (#666).

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

func (*Syncer) RunOnce

func (s *Syncer) RunOnce(ctx context.Context) (int64, bool, error)

RunOnce делает один догоняющий проход и отдаёт число затронутых строк снимка.

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

func (*Syncer) RunOnceBudgeted

func (s *Syncer) RunOnceBudgeted(ctx context.Context) (int64, bool, error)

RunOnceBudgeted делает один проход ПОД СВОИМ СРОКОМ.

Единственная точка, где срок прохода назначается. Прежде его ставил цикл, а подъём звал `RunOnce` голым контекстом процесса — то есть первый проход, тот самый, что случается в загрузочную бурю, шёл без срока вовсе, и замолчавший сосед держал бы подъём сервиса сколько угодно (#666). Владеть сроком обязано одно место, иначе следующий вызывающий снова его забудет.

Directories

Path Synopsis
Package quotadetail — величины отказа учёта на пути от производителя к клиенту.
Package quotadetail — величины отказа учёта на пути от производителя к клиенту.
Package quotaread — арендаторское чтение квот: ОДНО тело на всех владельцев.
Package quotaread — арендаторское чтение квот: ОДНО тело на всех владельцев.

Jump to

Keyboard shortcuts

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