quotaread

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: 9 Imported by: 0

Documentation

Overview

Package quotaread — арендаторское чтение квот: ОДНО тело на всех владельцев.

Почему это общий пакет, а не пятая копия

Число, из которого вычитают, остаётся в базе владельца ресурса — списание идёт в транзакции вставки, и распределённой транзакции в стеке нет. Но ЧТЕНИЕ доменно-независимо целиком: одна и та же таблица учёта с точностью до имени схемы, одно и то же правило «ответ никогда не бывает пустым», один и тот же порядок и один и тот же разбор промаха.

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

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

Здесь нет ни pgx, ни сгенерённых стабов: пакет стоит в слое use-case, и оператор чтения живёт у адаптера владельца (`pkg/quota.ListStates`), а перевод в контракт — у транспорта (`pkg/quota/quotapb`). Разделение не косметическое: полоса обязана быть проверяемой без базы и без соседа, иначе её собственные пробы становятся интеграционными и перестают писаться.

Здесь нет и ЗАПИСИ. Резолв на промахе ничего не материализует — в отличие от `Admit`, где заведение строк есть часть предмета (место надо занимать). Предмет чтения иной: показать. Материализация на чтении означала бы, что список квот меняет состояние базы, и тогда «посмотреть» стало бы мутацией, которую никто не просил, а на fail-closed пути ещё и отказом там, где спрашивали только показать.

Index

Constants

View Source
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

func NewBand(rows Rows, limits Limits, limitService, errorDomain string) (*Band, error)

NewBand собирает полосу чтения.

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

func (*Band) States

func (b *Band) States(ctx context.Context, c Carrier) ([]State, error)

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

func ProjectCarrier(projectID string) Carrier

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

func AbsentAuthority(serviceDomain string) Posture

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

Имя сервиса берётся параметром, а не выводится: у балансировщика каталог величин знает виды как `loadbalancer`, а ответ на проводе обязан называть источником `nlb`, — вывести одно из другого нельзя.

func AuthorityDeclared

func AuthorityDeclared() Posture

AuthorityDeclared — домен величин объявлен адресом.

func (Posture) AuthorityIsAbsent

func (p Posture) AuthorityIsAbsent() bool

AuthorityIsAbsent — объявлен ли домен величин отсутствующим.

func (Posture) Refusal

func (p Posture) Refusal() error

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-стороне и есть то, чем счётчик защищён от расхождения с реальностью.

Jump to

Keyboard shortcuts

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