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

operationspb

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: 8 Imported by: 1

Documentation

Overview

Package operationspb — арендаторская поверхность `OperationService`: перевод строки операции в контракт и обработчик, реализующий оба глагола.

Почему отдельный пакет, а не функции в pkg/operations

`pkg/operations` не импортирует proto-стабы и не должен: он владеет строкой операции и её хранением, а не тем, как она выглядит на проводе. Тот же шов проведён у величин пределов (`pkg/quota/quotapb`), и он здесь повторён, а не изобретён.

Почему один пакет, а не семь копий

`OperationService` — единственный сервис контракта, который несёт КАЖДЫЙ домен: любая мутация возвращает `Operation` (ban #9), и клиент поллит её у того же сервиса, который мутацию принял. До сведения реализация была объявлена семь раз, и семь копий давали семь поведений.

Расхождения были не косметические, и увидеть их можно было только сравнив копии: одна из них отменяла операцию НЕАТОМАРНО — проверяла владение отдельным чтением, а мутацию звала несуженную, то есть предикат владения уходил из мутации совсем (ban #10, software check-then-act). По диффу обе половины выглядят добросовестной работой.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ToProto

ToProto переводит строку операции в контракт.

Здесь два РЕШЕНИЯ, а не механика, и у каждого одно место:

  • усечение отметок времени до секунды — конвенция продукта для КАЖДОГО proto-ответа (`api-conventions.md`): БД хранит микросекунды, клиент их не видит. `Operation` — ответ каждой мутации, то есть самая частая поверхность утечки долей секунды;
  • охрана пустого значения: `nil` на входе даёт `nil` на выходе, а не панику. До сведения одна из копий этой охраны НЕ несла — расхождение, невидимое до первого пустого значения.

Тело сведено к версии БОЛЬШИНСТВА, а не написано заново. Числа ниже сняты на предшествующей ревизии, и единица счёта названа явно: прежние редакции этого абзаца смешивали «объявление» с «именем помощника» и потому не сходились ни с одним предикатом.

ОБЪЯВЛЕНИЙ было ДВЕНАДЦАТЬ. Четыре из них — обёртки nlb, делегировавшие в свой же общий помощник и собственного тела не имевшие; РЕАЛИЗАЦИЙ, стало быть, восемь. Все двенадцать несли все три поля принципала.

Расходились они тремя способами, и ни один не косметический:

  • УСЕЧЕНИЕ: четыре реализации звали именованный помощник (три РАЗНЫХ имени — `ts`, `TimestampProto`, `prototime.Truncate`), четыре усекали инлайн;
  • ОХРАНА ПУСТОГО ВХОДА (`op == nil`): её не несли ДВЕ реализации из восьми;
  • ОХРАНА ПУСТОГО МОМЕНТА ВРЕМЕНИ: её несли ТРИ объявления из двенадцати — те, что звали `TimestampProto` (iam, два места) и `prototime.Truncate` (registry). То есть два именованных помощника из трёх: if t.IsZero() { return nil }

Здесь принята версия МЕНЬШИНСТВА, и это решение, а не перенос. Довод: пустой момент означает «не задано», и отдавать вместо него `0001-01-01T00:00:00Z` значит выдавать умолчание за факт.

РЕГРЕССА БЫТЬ НЕ МОЖЕТ, НО ОСНОВАНИЕ НЕ ТО, КОТОРОЕ НАПРАШИВАЕТСЯ. Здесь стояло «обе колонки `NOT NULL DEFAULT now()`», и это неверно дважды: `DEFAULT` не срабатывает никогда, потому что вставка перечисляет `created_at` и `modified_at` ЯВНО (`pkg/operations/repo.go`, параметры `$3` и `$5`), а `NOT NULL` пустого времени не отсекает — `time.Time{}` пишется законным `0001-01-01T00:00:00Z`, на котором `IsZero()` истинно.

Непустоту гарантирует единственный чеканящий конструктор: `operations.New` ставит обе метки до вставки, и другого в дереве нет. Значит охрана в проде НЕДОСТИЖИМА — она страхует не сегодняшний путь, а завтрашний второй конструктор, и сказать это надо прямо: следующий, кто такой конструктор заведёт, прочитал бы гарантию, которой нет.

Types

type Handler

type Handler struct {
	operationpb.UnimplementedOperationServiceServer
	// contains filtered or unexported fields
}

Handler — арендаторская реализация `OperationService` для ЛЮБОГО домена.

Что здесь энфорсится и почему это не может жить у вызывающего

Владелец операции — принципал, её создавший. Без предиката владения caller, узнав чужой id, прочитал бы чужой ресурс (`Operation.response` несёт его целиком) или отменил бы чужую in-flight мутацию. Предикат уходит в SQL `WHERE` (`GetOwned`/`CancelOwned`), а чужой и несуществующий id отдают ОДИНАКОВЫЙ `NotFound`: «есть, но не твоя» обязано быть неотличимо от «нет такой».

Правильный вызов складывается из трёх шагов, и пропуск любого возвращает исходную дыру: получить ownership-апгрейд репозитория, вывести ключ владения ИМЕННО из ctx, и на отсутствие ключа отказать, а не продолжить. Тот же довод, что у `operations.ListForCaller`, и та же форма.

Ключ выводится `OwnerFromContext`, а НЕ `PrincipalFromContext`: последний на контексте без принципала отдаёт системную личность `{system, bootstrap}`, которая совпадает с предикатом владения на КАЖДОЙ системно записанной строке. То есть анонимный запрос стал бы владельцем всего.

Admin-обхода предиката здесь НЕТ, и держится это НЕ посадкой.

Прежняя редакция обосновывала отсутствие обхода тем, что `OperationService` выставляется только на публичном слушателе. Замер опроверг: geo, storage и registry регистрируют обработчик на ОБОИХ слушателях — шесть мест, по два на сервис. Довод, верный для одного сервиса, был схлопнут в общий и стал ложным для трёх; в файле, который читают семь доменов, это приглашение снять предикат на «доверенном» внутреннем (`security.md` §Hardening #5).

Настоящее основание — предикат в SQL: ключ владения выводится из ctx и уходит в `WHERE`, поэтому исход не зависит от того, каким слушателем пришёл запрос. Прежняя ветка «если админ — снять предикат» читала флаг из КЛИЕНТСКОГО заголовка, ретранслированного краем, то есть была не admin-функцией, а живым обходом владения.

func NewHandler

func NewHandler(repo operations.Repo) *Handler

NewHandler создаёт обработчик. В проде repo — pgRepo, реализующий `operations.OwnedOperationRepo`; если не реализует (ошибка провязки), ownership-вызовы отвечают INTERNAL — fail-closed, а не молчаливый обход.

func (*Handler) Cancel

Cancel отменяет операцию — только её владельцу и АТОМАРНО.

Отмена идёт одним `CancelOwned`: предикат владения и смена состояния — один оператор, терминальное состояние приходит в RETURNING, поэтому отдельного перечитывания после отмены не нужно.

Одна из семи сведённых сюда копий была написана иначе: «прочитать своё (`GetOwned`) → отменить НЕСУЖЕННО → перечитать несуженно». Это форма check-then-act, запрещённая ban #10, и она снята — но сказать про неё «между проверкой и мутацией не держалось ничего» было бы НЕВЕРНО, и здесь это говорится прямо, чтобы следующий читатель не переизобрёл довод. Держалась НЕИЗМЕНЯЕМОСТЬ: колонки принципала не меняются, поэтому решение о владении не устаревало между двумя операторами, и чужую операцию отменить было нельзя ни при какой гонке. Снято здесь не окно эксплуатации, а ФОРМА: инвариант, выраженный software-проверкой вместо оператора БД, переживает первое же изменение того, что делало его верным, — и переживает молча.

func (*Handler) Get

Get возвращает операцию по id — только её владельцу.

Jump to

Keyboard shortcuts

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