Help us shape the future of Go! Take the 2026 Go Developer Survey

peer

package
v1.16.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 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"` — утверждение об отсутствии того, чего вызывающий не называл).
  • Машинный признак едет вместе с кодом: и то и другое берётся у полосы `Reason` соседнего пакета фундамента `errors`, собрать их порознь нельзя. Клиент ключуется на токен; тон прозы остаётся контрактом и принадлежит вызывающему.

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

Носитель судит ОТВЕТ соседа. Он не владеет соединением, сроком вызова и повтором — эта половина (`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