peer

package
v1.3.1 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

Package peer — носитель полосы ответа соседа: ОДНО место, где чужой отказ классифицируется, и ОДНО, где он превращается в наш ответ.

Зачем носитель

Когда сервис зовёт соседа и тот отказывает, вызывающий обязан различить ПОЛОСЫ: чужого объекта нет у владельца · объект есть, но состояние не позволяет · владелец недоступен · нам отказано в правах · владелец счёл ссылку негодной. От полосы зависит код ответа, право на повтор и то, что увидит арендатор (api-conventions.md §By-lane code-split).

До носителя это читалось руками: каждый клиент писал свой `switch status.Code(err)`, и наборы разошлись. Разошлись они молча — по таблице `api-conventions.md` §«gRPC-код → HTTP» и INVALID_ARGUMENT, и FAILED_PRECONDITION дают 400, поэтому ни REST-клиент, ни e2e-утверждение о статусе перехода не заметят разницы.

Что здесь запрещено по построению

  • Корзины «прочее» нет. Код соседа, которому не назначена полоса, даёт OutcomeUnclassified — это СОСТОЯНИЕ («не смог классифицировать»), а не молчаливый выбор политики повтора. Оно терминально и выходит наружу фиксированным INTERNAL.
  • Отказ в правах (OutcomeDenied) НЕ временный. Повтор идентичного запроса не пройдёт; трактовать его как временный значит вечно держать голову очереди (data-integrity.md §«Межсервисное намерение»: очередь, у которой за всю жизнь не доехало ни одной строки, выглядела исправной).
  • Проза, утверждающая отсутствие НЕНАЗВАННОГО ресурса, невыразима: идентификатор подставляет носитель, и на пустом идентификаторе он подставляет не пустое место, а говорит, что ссылка пуста (реальный дефект `resolveVipSources`: `"subnet not found"` — утверждение об отсутствии того, чего вызывающий не называл).
  • Машинный признак едет вместе с кодом: и то и другое берётся у полосы github.com/PRO-Robotech/kacho/pkg/errors.Reason, собрать их порознь нельзя. Клиент ключуется на токен; тон прозы остаётся контрактом и принадлежит вызывающему.

Граница пакета

Носитель судит ОТВЕТ соседа. Он не владеет соединением, сроком вызова и повтором — эта половина (`Conn`/`Open`/`Do` из §4.7 приёмки XC-7) в дереве пока отсутствует, и её отсутствие названо, а не замаскировано: сроки и повторы сегодня живут у каждого клиента.

Внешние системы (хранилище прав, провайдер токенов, реестр образов) — ИНАЯ полоса: там нет ни нашего каталога, ни наших токенов, и носитель их не судит.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClassifiedCodes

func ClassifiedCodes() map[codes.Code]Outcome

ClassifiedCodes — коды, которым носитель назначил полосу (копия, чтобы вызывающий не правил канон). Нужен проверкам, утверждающим свойство НАБОРА: без него они перечисляли бы коды рукой и молчали бы ровно о том, который забыли дописать.

func PeerCode

func PeerCode(err error) codes.Code

PeerCode — код, которым ответил сосед. ТОЛЬКО для журнала и метрик.

Существует потому, что у непонятого ответа обязана оставаться диагностика: без кода в журнале ошибка маршрутизации («стучимся на тот адрес, но не на тот листенер») теряется немо, а полоса `unclassified` сама по себе не говорит, что именно ответил сосед.

НЕ для решений: любое ветвление по этому значению воссоздаёт рукописный разбор кодов, ради снятия которого заведён носитель. Решение принимают Classify, Outcome.Transient и Outcome.RefusedReference. Не-статус и `nil` дают codes.Unknown — «сосед кодом не отвечал».

func PeerMessage

func PeerMessage(err error) string

PeerMessage — проза, которой ответил сосед.

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

Граница, которую обязан помнить вызывающий: на OutcomeUnclassified эта проза НЕ имеет права уехать арендатору — там она может нести host/port и текст драйвера (security.md §Hardening #1). Для той полосы носитель и отдаёт фиксированный текст сам (Outcome.Status).

Types

type Outcome

type Outcome uint8

Outcome — исход обращения к соседу. Закрытый набор БЕЗ корзины «прочее».

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

const (
	// OutcomeUnclassified — сосед ответил кодом, которому полоса не назначена.
	// СОСТОЯНИЕ, а не политика: терминально (повтор идентичного запроса не
	// изменит непонятного ответа) и наружу — фиксированный INTERNAL без прозы
	// соседа. Никогда не «временно»: молчаливый повтор непонятого отказа и есть
	// та самая корзина «прочее», только спрятанная в политику.
	OutcomeUnclassified Outcome = iota

	// OutcomeOK — сосед ответил.
	OutcomeOK

	// OutcomeMissing — у владельца нет названного ресурса. Терминально.
	OutcomeMissing

	// OutcomeStateRefused — ресурс у владельца есть, состояние не позволяет.
	// Терминально.
	OutcomeStateRefused

	// OutcomeDenied — владелец отказал В ПРАВАХ. ТЕРМИНАЛЬНО: повтор
	// идентичного запроса под той же личностью не пройдёт никогда.
	//
	// Наружу отдаётся полосой промаха (см. [Outcome.Reason]) — арендатору не
	// сообщают, что отказ был про права: иначе по коду отличали бы «ресурса
	// нет» от «есть, но не виден», то есть оракул существования. Внутри полоса
	// остаётся отдельной, потому что решения о повторе и о наблюдаемости у неё
	// свои.
	OutcomeDenied

	// OutcomeMalformed — владелец счёл ссылку негодной по форме. Терминально.
	//
	// Отдельная от [OutcomeMissing] полоса, хотя наружу они совпадают: чужой id
	// у нас формату не поверяется (api-conventions.md B4 — чужой префикс не наш
	// словарь), поэтому «негоден» говорит владелец, и в журнале это отличимо от
	// «не резолвится».
	OutcomeMalformed

	// OutcomeUnavailable — владелец не дал ответа, на который можно опереться.
	// ЕДИНСТВЕННАЯ временная полоса; на мутации — fail-closed (непроверяемое
	// предусловие не считается выполненным).
	OutcomeUnavailable
)

func AllOutcomes

func AllOutcomes() []Outcome

AllOutcomes — набор исходов целиком, в том же назначении, что и ClassifiedCodes.

func Classify

func Classify(err error) Outcome

Classify — ЕДИНСТВЕННОЕ место, где ответ соседа превращается в полосу.

Разбирается обёрнутая ошибка тоже (`status.FromError` разворачивает цепочку `%w`), поэтому клиент, добавивший контекст к ответу соседа, не теряет полосу.

Соответствие кодов — закрытое. Код, которого здесь нет, даёт OutcomeUnclassified, и это осознанно шире, чем кажется: `ABORTED`, `RESOURCE_EXHAUSTED`, `INTERNAL`, `UNKNOWN`, `UNIMPLEMENTED` от СОСЕДА означают, что мы не понимаем его ответ, — а не что можно повторить.

func (Outcome) CallerIndependent

func (o Outcome) CallerIndependent() bool

CallerIndependent сообщает, зависит ли исход от ТОГО, КТО СПРОСИЛ.

Отделено от Outcome.RefusedReference намеренно, и разница не косметическая. `RefusedReference` отвечает на вопрос «показывать ли арендатору отказ ссылки» — и правильно объединяет промах, отказ в правах и негодный идентификатор: их различимость снаружи и есть оракул существования. Но тот же ответ нельзя класть в кэш, ключ которого личности не содержит: отказ в правах вычислен ДЛЯ ВЫЗЫВАЮЩЕГО, и, сохранённый под ключом «идентификатор ресурса», он будет отдан другому — то есть один арендатор получит решение, вынесенное не ему.

Реальный случай (найден адверсарной проверкой этой же работы): клиент проекта в vpc стал принимать `RefusedReference` за «проекта нет», а кэш положил результат на окно TTL под ключом без личности. Штатное окно отказа на своём свежем ресурсе — материализация прав идёт eventually-consistent, и 403/404 на первом обращении объявлены нормой — фиксировалось как «проекта нет» для ВСЕХ.

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

func (Outcome) NamesResource

func (o Outcome) NamesResource() bool

NamesResource сообщает, УТВЕРЖДАЕТ ли полоса что-либо о чужом ресурсе — и потому вправе называть его в тексте.

Истинно для промаха, состояния, отказа в правах и негодной ссылки: владелец ответил про конкретный ресурс, и текст обязан сказать, про какой. Их проза несёт ровно один глагол, который заполняется идентификатором из Ref.

Ложно для недоступности и непонятого ответа: владелец не ответил вовсе, и называть чужой ресурс не в чем — ни утверждения, ни знания о нём нет. Проза этих полос глагола не несёт (так объявлено в Prose), поэтому подстановка в неё печаталась бы форматтером служебным мусором `%!(EXTRA string=…)` — и идентификатор всё-таки называла, вопреки замыслу полосы.

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

func (Outcome) Reason

func (o Outcome) Reason() kerrors.Reason

Reason — полоса ответа, как её увидит клиент: код и машинный признак берутся вместе и разойтись не могут.

OutcomeOK и OutcomeUnclassified полосы контракта не имеют и возвращают необъявленное значение: у первого нет отказа, у второго нет утверждения, которое мы вправе сделать.

func (Outcome) RefusedReference

func (o Outcome) RefusedReference() bool

RefusedReference отвечает на вопрос вызывающего, который собирает ответ НЕ здесь: УСТАНОВИЛ ли владелец, что ссылка не годится.

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

Ложно для недоступности и непонятого ответа: там владелец не установил НИЧЕГО, и выдавать это за «ссылки нет» значит утверждать за него — ровно так временный перебой у соседа превращался в терминальный отказ арендатору, а наша неверная настройка — в «повтори позже».

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

func (Outcome) Status

func (o Outcome) Status(ref Ref, p Prose) error

Status — ЕДИНСТВЕННОЕ место, где исход превращается в наш ответ: код и машинный признак берутся у полосы, проза — у вызывающего.

Четыре исхода, пятого нет:

  • OutcomeOK → nil;
  • полоса контракта и НЕПУСТОЙ идентификатор → код полосы + проза с подставленным идентификатором + машинный признак в details;
  • полоса контракта и ПУСТОЙ идентификатор (при Opaque=false) → код и признак те же, но проза говорит, что ссылка пуста. Это не косметика: «<ресурс> не найден» с пустым местом — утверждение об отсутствии того, чего вызывающий не называл, и оно уводит отладку к владельцу, у которого всё в порядке;
  • OutcomeUnclassified → INTERNAL с фиксированным текстом. Проза вызывающего отбрасывается: она описывает полосу, о которой мы ничего не установили, то есть была бы ложью.

func (Outcome) String

func (o Outcome) String() string

String — имя полосы для журнала. Неизвестное значение не притворяется полосой.

func (Outcome) Transient

func (o Outcome) Transient() bool

Transient отвечает на ЕДИНСТВЕННЫЙ вопрос, ради которого полосу и различают в очередях: пройдёт ли повтор идентичного запроса.

Истинно ровно для OutcomeUnavailable. Отказ в правах, промах, негодная ссылка и непонятый ответ — терминальны: дренаж, считающий любой из них временным, держит голову своей партиции всё окно повторов, и очередь при этом выглядит живой (реальный инцидент — 198 строк, ни одной доставленной).

type Prose

type Prose struct {
	// Missing — проза трёх наружу-неразличимых полос: промах, отказ в правах,
	// негодная по мнению владельца ссылка.
	Missing string
	// State — проза полосы «ресурс есть, состояние не позволяет».
	State string
	// Unavailable — проза полосы недоступности.
	Unavailable string
	// Opaque — идентификатор не называется НИГДЕ: ни подстановкой в текст, ни в
	// метаданных. Ставится там, где раскрытие само по себе есть утечка
	// (принадлежность и размещение чужого адреса).
	//
	// Отдельный признак нужен потому, что «намеренно не называем» и «потеряли»
	// обязаны выглядеть в коде по-разному: за первое отвечает автор, за второе
	// краснеет проба. При Opaque тексты берутся дословно, глагол в них не
	// ожидается.
	Opaque bool
}

Prose — тексты полос. Принадлежат ВЫЗЫВАЮЩЕМУ: тон сообщений — часть контракта Kachō, полоса добавляет к сказанному только код и машинный признак.

Missing и State несут РОВНО ОДИН глагол `%s` — его заполняет носитель идентификатором из Ref. Идентификатор не передаётся отдельным аргументом намеренно: пара «формат + аргумент» позволяет потерять аргумент, пара «формат + Ref» — нет.

Unavailable глагола не несёт: полоса недоступности ничего не утверждает о чужом ресурсе, поэтому и называть его ей нечем.

Пустое поле означает «своей прозы у этой полосы нет» и заменяется нейтральной формой (см. Outcome.Status). Это не умолчание ради краткости: полоса, до которой на этом ребре не доходит (владелец Geography не отвечает FAILED_PRECONDITION), не должна требовать мёртвого текста — а дойдя однажды, обязана ответить чем-то честным, а не пустой строкой.

type Ref

type Ref struct {
	Service      string
	ResourceType string
	ResourceID   string
}

Ref — координата ответа: чей это отказ и о чём он.

Service — НАШ сервис, тот, что отвечает арендатору (из него собирается `ErrorInfo.domain`), а не сосед: домен называет источник ответа.

ResourceID пуст там, где полоса намеренно не подтверждает существование чужого ресурса, — и там, где вызывающий его потерял. Носитель разводит эти случаи явно: замысел объявляется признаком Prose.Opaque, потеря отвечает прозой «ссылка пуста» (см. Outcome.Status).

Jump to

Keyboard shortcuts

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