Цель
Зафиксировать инженерный контракт: отдельное вычисление в системе (job) всегда является субъектным актом, а не «законом природы».
1) Базовый инвариант
J0: каждая запись в jobs принадлежит ровно одному owner_id.
Из J0 напрямую следуют инфраструктурные свойства:
— доступ к статусу и результатам определяется владельцем;
— дедупликация вычисляется в границах владельца;
— квоты, rate-limit, биллинг и приоритизация считаются по владельцу;
— удаление, отмена и GC артефактов привязаны к владельцу;
— аудит становится определённым: кто инициировал, повторил, форсировал, остановил.
Это не доменная опция, а минимальная онтология ответственности для многосубъектной системы.
2) Ключевое разделение уровней
2.1 Универсальный исполнитель (worker loop)
Исполнитель может быть полностью нейтральным:
— взять job из очереди;
— поставить lease;
— обновить статус;
— передать handler.
Это слой механизма.
2.2 Job как акт
job — не механизм, а заявка/билет: инициированное действие с ресурсной стоимостью и последствиями.
Это слой ответственности.
Формула:
— worker универсален;
— job субъектна.
Универсальность исполнителя не отменяет субъектность записи в jobs.
3) Почему «owner только в handler» недостаточен
Если owner_id отсутствует в jobs, а проверяется только в обработчиках, инфраструктура теряет управляемость:
— нельзя надёжно ограничить создание/выполнение задач по субъекту на уровне очереди;
— нельзя честно считать нагрузку и стоимость по субъекту;
— нельзя корректно дедупить в границах субъекта;
— нельзя быстро сделать kill all jobs for owner;
— нельзя жёстко гарантировать tenant-изоляцию в самом ядре очереди.
Итог: субъектность уезжает в «внешнюю логику», где неизбежны рассинхроны.
4) Минимальная модель сущностей
Для универсальной системы вычислений в jobs должны быть:
— owner_id — ось доступа и ответственности;
— dedup_key (job_key) — ось идентичности работы;
— pipeline_version — ось версионности результата.
Это инфраструктурные поля вычислительного акта, а не поля конкретного домена.
5) Контракт истинности owner
Допустимы два корректных режима.
Вариант A (нормализация)
jobs.owner_id— единственная истина;transcriptions.owner_idотсутствует;- owner производной сущности вычисляется через
...job_id -> jobs.owner_id.
Вариант B (практичный денорм)
jobs.owner_id— единственная истина;transcriptions.owner_id— кэш/производное поле;- приложение не принимает
owner_idизвне при создании/обновлении производной сущности; - БД запрещает рассинхрон
transcriptions.owner_id != jobs.owner_id.
Наличие двух колонок допустимо только при наличии одной истины и одной производной.
6) Инварианты БД
jobs.owner_id—NOT NULL+FK owners(id).- Идемпотентность в пределах субъекта:
UNIQUE (owner_id, kind, pipeline_version, dedup_key)
WHERE dedup_key IS NOT NULL
- (Для варианта B) запрет рассинхрона owner между
jobsи производными таблицами.
7) Контракт сервисного слоя
- Создание job всегда субъектно:
create_job(owner_id, kind, pipeline_version, dedup_key, payload...).- Получение статуса/результата всегда проверяет владение:
job.owner_id == requester.owner_id.- Воркеры могут оставаться универсальными, но учёт ресурса/лимитов/аудита идёт по
owner_id.
8) Миграционный маршрут без «взрыва базы»
- Добавить
jobs.owner_idкак временно nullable. - Выполнить backfill из связанных таблиц (
transcriptionsи др.). - Перевести
jobs.owner_idвNOT NULL. - Добавить
pipeline_version,dedup_key, partial unique index. - Закрыть рассинхрон для денормированных owner-полей (триггер/constraint + код).
9) Быстрый архитектурный тест
Если архитектура верна, на уровне jobs можно ответить на вопросы без «магии» в handler:
— кто инициировал и кто платит;
— сколько ресурсов потратил конкретный субъект;
— какие задачи нужно остановить у конкретного субъекта;
— кто имеет право видеть/повторять/удалять результат.
Если на эти вопросы нельзя ответить через jobs, субъектность в ядре ещё не зафиксирована.
10) Принципиальный вывод
owner_id в jobs — не усложнение, а возвращение ответственности в структуру системы.
Система может иметь универсальный механизм исполнения, но каждый вычислительный акт внутри него должен иметь владельца. Иначе возникает «анонимная зона» — место утечки границ, лимитов и ответственности.
11) Онтологическое уточнение: субъект не эпифеномен
Ключевая фиксация:
Субъект не должен появляться «где-то в середине». Он не производный от процесса, а инициатор действия.
Если owner_id хранится только в payload-таблицах, схема неявно утверждает ложную причинность: будто сначала есть процесс/содержание, и лишь потом «внутри» обнаруживается субъект.
Правильная причинная ось обратная:
— субъект инициирует акт;
— акт фиксируется как job;
— payload описывает материал/параметры акта.
Иными словами:
— job — событие ответственности;
— payload — аргументы события;
— owner_id — атрибут акта, а не атрибут материала.
Поэтому owner_id должен быть в корневой сущности jobs, а не в «сменной плоти» payload-структур.
12) Практическая формула архитектуры
Рекомендуемая форма:
Job(owner_id, kind, status, schedule, lease, pipeline_version, dedup_key, ...)
и отдельно:
JobPayload(job_id, ...)
Что это даёт сразу:
— единая и быстрая выборка «все мои задачи» по индексу jobs.owner_id;
— естественная политика доступа (job.owner_id как первичный предикат);
— чистая идемпотентность в границах субъекта (owner_id + ключ идентичности);
— нормальные операции retention, cleanup, rate limiting, abuse detection, audit без зоопарка join-веток по типам payload.
Это не просто «удобнее хранить». Это возвращение субъектности в начало причинной цепи системы.