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 ¶
func ToProto(op *operations.Operation) *operationpb.Operation
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 ¶
func (h *Handler) Cancel(ctx context.Context, req *operationpb.CancelOperationRequest) (*operationpb.Operation, error)
Cancel отменяет операцию — только её владельцу и АТОМАРНО.
Отмена идёт одним `CancelOwned`: предикат владения и смена состояния — один оператор, терминальное состояние приходит в RETURNING, поэтому отдельного перечитывания после отмены не нужно.
Одна из семи сведённых сюда копий была написана иначе: «прочитать своё (`GetOwned`) → отменить НЕСУЖЕННО → перечитать несуженно». Это форма check-then-act, запрещённая ban #10, и она снята — но сказать про неё «между проверкой и мутацией не держалось ничего» было бы НЕВЕРНО, и здесь это говорится прямо, чтобы следующий читатель не переизобрёл довод. Держалась НЕИЗМЕНЯЕМОСТЬ: колонки принципала не меняются, поэтому решение о владении не устаревало между двумя операторами, и чужую операцию отменить было нельзя ни при какой гонке. Снято здесь не окно эксплуатации, а ФОРМА: инвариант, выраженный software-проверкой вместо оператора БД, переживает первое же изменение того, что делало его верным, — и переживает молча.
func (*Handler) Get ¶
func (h *Handler) Get(ctx context.Context, req *operationpb.GetOperationRequest) (*operationpb.Operation, error)
Get возвращает операцию по id — только её владельцу.