Documentation
¶
Overview ¶
Package quotaread — арендаторское чтение квот: ОДНО тело на всех владельцев.
Почему это общий пакет, а не пятая копия ¶
Число, из которого вычитают, остаётся в базе владельца ресурса — списание идёт в транзакции вставки, и распределённой транзакции в стеке нет. Но ЧТЕНИЕ доменно-независимо целиком: одна и та же таблица учёта с точностью до имени схемы, одно и то же правило «ответ никогда не бывает пустым», один и тот же порядок и один и тот же разбор промаха.
Написанное пять раз порознь, оно разошлось бы там, где расхождение не видно ни одной из сторон: на промахе, то есть на проекте, который ещё ничего не создавал. Каждая копия по отдельности выглядела бы верной, а арендатор получал бы в разных доменах разный ответ на один и тот же вопрос.
Чего здесь нет и почему ¶
Здесь нет ни pgx, ни сгенерённых стабов: пакет стоит в слое use-case, и оператор чтения живёт у адаптера владельца (`pkg/quota.ListStates`), а перевод в контракт — у транспорта (`pkg/quota/quotapb`). Разделение не косметическое: полоса обязана быть проверяемой без базы и без соседа, иначе её собственные пробы становятся интеграционными и перестают писаться.
Здесь нет и ЗАПИСИ. Резолв на промахе ничего не материализует — в отличие от `Admit`, где заведение строк есть часть предмета (место надо занимать). Предмет чтения иной: показать. Материализация на чтении означала бы, что список квот меняет состояние базы, и тогда «посмотреть» стало бы мутацией, которую никто не просил, а на fail-closed пути ещё и отказом там, где спрашивали только показать.
Index ¶
Constants ¶
const CarrierProject = "project"
CarrierProject — носитель учёта «корень аренды».
Объявлен ЗДЕСЬ, потому что и полоса, и её вызывающие называют одно и то же значение: пять литералов `"project"` разошлись бы молча ровно один раз, и это был бы последний раз, когда кто-то смог прочитать свои квоты.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Band ¶
type Band struct {
// contains filtered or unexported fields
}
Band — полоса чтения квот проекта.
func NewBand ¶
NewBand собирает полосу чтения.
Отказывает при сборке, а не при первом вызове: полоса без строк или без владельца величин отвечала бы отказом на каждое чтение, оставаясь на вид собранной. Пустое имя каталога хуже — оно спрашивает у соседа виды сервиса с пустым именем и получает пустой набор, то есть «квот нет» вместо «не собрано».
func (*Band) States ¶
States отдаёт квоты НАЗВАННОГО носителя: предел, потребление и источник победившей величины по каждому виду, который в этом носителе считается.
ОТВЕТ НИКОГДА НЕ БЫВАЕТ ПУСТЫМ. Носитель, у которого ещё ничего не создавали, строк учёта не имеет by construction — их заводит первая мутация. Вернуть на это пустой набор значило бы сказать «квот нет», то есть «предела нет»: ровно обратное действительности, и прочитано оно будет как разрешение (`api-conventions.md` §«Пустое значение обязано означать „пусто“»).
ПУСТО ПОСЛЕ ОТБОРА — ТОЖЕ НЕ ОТВЕТ, А ОТКАЗ (#705). Отбор оставляет виды ЭТОГО носителя; если не осталось ни одного, вернуть пустой набор значило бы выдать отобранное за полный перечень. Действительность при этом обратная: вид без назначенной величины отвергается на каждой вставке («потолок не назван» есть отказ, а не бесконечность), — то есть пустой ответ сообщал бы противоположное тому, что произойдёт. Отказ называет, у каких носителей величины ЕСТЬ, чтобы «пределов нет» было отличимо от «этот вид сюда не попадает».
ЦЕНА НАЗВАНА: чтение квот носителя, у которого ещё ничего не создавали, зависит от доступности владельца величин и отвечает отказом, когда тот недоступен. После первой мутации строки есть, и чтение локально.
type Carrier ¶
type Carrier struct {
// Type — тип носителя: корень аренды (`project`) либо двухчастный токен
// родительского типа (`vpc.network`).
Type string
// ID — идентификатор носителя: тот объект, чьё потребление считается.
ID string
// ScopeID — область, на которой владелец величин разрешает величины для этого
// носителя.
//
// Отдельным полем, а не «то же, что ID», и различие несущее: у вложенного
// носителя величина назначается на ПРОЕКТ («по скольку детей на родителя в
// этом проекте»), а считается в родителе. Свести их в одно поле значило бы
// спрашивать у соседа про идентификатор сети как про область — и получать
// пустой набор, то есть «пределов нет» вместо величины.
ScopeID string
}
Carrier — носитель учёта, О КОТОРОМ СПРАШИВАЮТ.
Почему носитель — параметр, а не константа полосы ¶
Полоса спрашивала ровно один тип и остальные отбрасывала. Следствие для арендатора: предел, считаемый в родительском ресурсе («сколько подсетей в ОДНОЙ сети»), увидеть было нельзя вовсе — узнать о нём получалось только из текста отказа, то есть уже упёршись в него (#705). Витрина при этом показывала перечень, из которого арендатор вправе заключить, что других пределов у него нет.
Приписать все виды одному ответу нельзя: у семейств РАЗНЫЕ носители, и вид, считаемый в родителе, единственного значения на уровне проекта не имеет by construction. Значит спрашивающий обязан назвать, про КОГО он спрашивает, а полоса — отвечать ровно про него.
func ProjectCarrier ¶
ProjectCarrier — носитель «корень аренды». Область и носитель здесь совпадают: величина проектного вида назначается на тот же проект, в котором считается.
type Limits ¶
type Limits interface {
Resolve(ctx context.Context, scopeID, service string) ([]ResolvedLimit, error)
}
Limits — владелец величин. Старшинство областей (PROJECT > ACCOUNT > DEFAULT) разрешается У НЕГО и только у него.
type Posture ¶
type Posture struct {
// contains filtered or unexported fields
}
Posture — как ЭТА установка объявила домен величин, доехавшее до витрины.
Тип, а не булев параметр у вызывающего: на месте вызова `false` не читается никак, а пять владельцев написали бы пять разных его прочтений. Поля неэкспортируемые — собрать посадку, которой не бывает, за пределами пакета нельзя.
НУЛЕВОЕ ЗНАЧЕНИЕ — «домен объявлен адресом», то есть прежнее поведение. Выбрано намеренно: владелец, ещё не научившийся называть посадку, остаётся при верном ответе для СВОЕГО случая — непровязанная полоса при объявленном адресе и есть дефект сборки. Обратное умолчание объявляло бы отсутствие потолков за оператора, причём молча.
func AbsentAuthority ¶
AbsentAuthority — домен величин объявлен отсутствующим этой установкой.
Имя сервиса берётся параметром, а не выводится: у балансировщика каталог величин знает виды как `loadbalancer`, а ответ на проводе обязан называть источником `nlb`, — вывести одно из другого нельзя.
func AuthorityDeclared ¶
func AuthorityDeclared() Posture
AuthorityDeclared — домен величин объявлен адресом.
func (Posture) AuthorityIsAbsent ¶
AuthorityIsAbsent — объявлен ли домен величин отсутствующим.
func (Posture) Refusal ¶
Refusal — отказ витрины на объявленном отсутствии домена величин; `nil`, когда домен объявлен адресом.
Почему ОТКАЗ, а не пустой ответ и не строки с нулями ¶
Пустой набор контракт делать запрещает: он читается как «ограничений нет» (`Band.States`), а полный перечень видов приходит даже проекту, в котором ещё ничего не создано. Строки же показать нечем: поле предела — `int64`, у него нет представления «величины не существует», а ноль есть ЗАКОННОЕ и осмысленное значение («этого вида создавать нельзя»), прямо оговорённое контрактом. Нарисовать ноль значило бы сказать арендатору обратное действительности: в этой установке не запрещено НИЧЕГО.
Отсюда следует и то, чего здесь сделать НЕЛЬЗЯ без правки контракта: показать одно лишь занятое. «Занятое» живёт у владельца ресурса и остаётся, но выдать его без предела нечем — предел не пропустишь. Это отдельный предмет и отдельное решение о контракте, а не умолчание этой функции.
Код ¶
`FAILED_PRECONDITION` — тот же, что у соседней полосы «величины не назначены» (`notStated`): вопрос арендатора корректен, а состояние установки не позволяет на него ответить. Полосы различаются машинным признаком, а не кодом: клиент ключуется на признак, потому что тон сообщений — часть контракта и меняется осознанно.
Что говорит проза ¶
Она обязана восстанавливать следующий шаг, а следующий шаг здесь — БЕЗДЕЙСТВИЕ: потолков нет, и ни одна мутация по количеству отвергнута не будет. Отказ, сказавший только «не могу ответить», отправил бы арендатора в поддержку за пределом, которого в этой установке не бывает.
type ResolvedLimit ¶
type ResolvedLimit struct {
// Kind — вид ресурса точечным токеном платформы.
Kind string
// Value — разрешённая величина.
Value int64
// Carrier — ГДЕ этот вид считается: корень аренды (`project`) либо
// родительский тип у вложенного вида.
//
// Приезжает ОТВЕТОМ и не выводится из формы токена: `iam.project` двухчастен
// и считается в АККАУНТЕ. Догадка здесь не отказывает громко — она называет
// носителем то, чем вид не считается.
Carrier string
// SourceScope — область, на которой величина победила.
SourceScope string
// SourceScopeID — объект победившей области; пуст, когда победило умолчание.
SourceScopeID string
}
ResolvedLimit — одна разрешённая величина, как её отдаёт владелец величин.
ЕДИНСТВЕННЫЙ экземпляр этого типа на все домены: у каждого владельца он объявлен псевдонимом сюда. Пять независимых структур с одинаковыми полями компилировались бы одинаково и разошлись бы при первом же новом поле — добавленном одному, а нужном всем.
Ревизии здесь нет, и это НЕ упущение, а факт контракта: `Resolve` её не несёт. Настоящую проставляет синхронизатор дельты.
type Rows ¶
type Rows interface {
// ListStates отдаёт строки учёта носителя, упорядоченные по виду.
//
// Пустой срез означает «строк учёта ещё нет», а НЕ «квот нет»: различать эти
// два состояния обязана полоса, и она это делает.
ListStates(ctx context.Context, carrierType, carrierID string) ([]State, error)
}
Rows — строки учёта владельца, как их видит его собственная база.
type State ¶
type State struct {
// Kind — вид ресурса точечным токеном платформы (`vpc.network`).
Kind string
// Limit — разрешённая величина на момент снимка.
Limit int64
// Used — сколько занято.
Used int64
// SourceScope — область, на которой величина победила.
SourceScope string
// SourceScopeID — объект победившей области; пуст, когда победило умолчание.
SourceScopeID string
// CarrierType, CarrierID — носитель учёта.
CarrierType string
CarrierID string
}
State — строка учёта, какой её ВИДИТ арендатор.
Состав повторяет `kacho.cloud.quota.v1.Quota`: тип заводится под один контракт, и расхождение состава обязано быть видно сразу. Писателя у него нет ни одного — `used` пишет только триггер базы, и невыразимость «записать used» на Go-стороне и есть то, чем счётчик защищён от расхождения с реальностью.