operationspb

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

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