Компания в фоновом задании
Некоторые действия не укладываются в короткий HTTP-запрос. Выгрузка нескольких проектов ещё небольшая, но модель полезно построить так, чтобы она выдержала рост. Клиент создаёт задание, получает его идентификатор и позднее забирает результат. Фоновый процесс не имеет текущей вкладки пользователя, поэтому область компании нужно сохранить явно.
В «Пространстве» задание содержит организацию, инициатора, тип, состояние и срок аренды. Worker получает работу из общей очереди, затем открывает ограниченный сеанс данных этой компании. Он не использует случайно оставшийся tenant-контекст HTTP-запроса. Полный код ExportWorker.cs находится в архиве, приложение и фоновые циклы при подготовке не выполнялись.
Очередь хранит команду и её область
При создании экспорта API назначает organization_id из доверенного контекста и requested_by из личности. Клиент не передаёт эти поля в теле. Если он сможет выбрать компанию задания сам, worker станет способом читать соседние данные даже при правильно защищённых HTTP-списках.
Задание создаётся в той же транзакции, которая проверяет право инициатора и состояние компании. После commit API возвращает 202 и адрес проверки состояния. Возврат 200 с сообщением «экспорт готов» был бы неверным: работа только принята. В нашем небольшом примере это различие важнее фактической скорости выгрузки.
| Поле | Назначение |
|---|---|
organization_id |
Область данных и результата |
requested_by |
Кого проверить перед выполнением |
status |
queued, leased, completed или failed |
lease_token |
Идентификатор текущего права завершить работу |
lease_until |
Момент повторного допуска к очереди |
attempts |
Ограничение повторов после сбоев |
Полезная нагрузка не содержит сериализованный HttpContext или объект соединения. Эти объекты имеют короткое время жизни и не подходят для восстановления после перезапуска. В очереди сохраняются простые данные, из которых новый процесс сможет заново открыть сеанс и проверить условия.
Получение задания не означает вечного владения
Worker берёт строку через FOR UPDATE SKIP LOCKED, записывает состояние leased, новый token и срок. Другой worker пропускает уже заблокированную строку, а после истечения аренды сможет повторить работу. Этот механизм предполагает возможную повторную обработку; он не обещает выполнить каждую команду ровно один раз.
Ниже — фрагмент получения кандидата из ExportWorker. Команда выполняется в короткой транзакции очереди, после которой вычисление результата идёт отдельно. Полный UPDATE в архиве присваивает аренду и возвращает строку задания.
SELECT organization_id,id
FROM export_jobs
WHERE (status='queued' OR (status='leased' AND lease_until<now()))
AND attempts<3
ORDER BY created_at,id
FOR UPDATE SKIP LOCKED
LIMIT 1;
Если держать блокировку очереди на всё время выгрузки, другие процессы станут ждать завершения длинной задачи. Аренда делает владение явным без открытой транзакции на всё вычисление. При медленной работе срок потребуется продлевать; наш учебный экспорт мал, но границу этого упрощения нужно понимать до переноса механизма на большие файлы.
Случайный lease token защищает от запоздалого завершения. Старый worker мог зависнуть, потерять аренду и позже вернуться с результатом. UPDATE результата требует совпадения token, поэтому он не перезапишет результат новой попытки. Одного состояния leased недостаточно: обе попытки видят одно название состояния, но разные полномочия завершения.
Worker заново устанавливает компанию
Роль space_worker может читать очередь разных компаний через отдельную политику. Это не даёт ей безусловного чтения проектов. Для полезных данных worker начинает транзакцию, назначает app.organization_id из сохранённого задания и получает только строки этой области. Таким образом, исключение общей очереди не распространяется на весь сервис.
Перед экспортом проверяется актуальное членство инициатора. Если Веру удалили из компании после создания задания, задача не должна автоматически выдавать ей новый файл. В нашем контракте такой экспорт завершается отказом. Можно выбрать другой продуктовый контракт, например право владельца продолжить задание, но это должно быть явное правило, а не последствие отсутствующего HTTP-фильтра.
Состояние компании тоже перечитывается. Active и read_only допускают выгрузку, deleting и deleted прекращают новые пользовательские экспорты. Здесь read_only полезен: ограничения записи не мешают владельцу получить свои данные. Если общий код слепо требует active для любой операции, он случайно лишит клиента выгрузки именно в момент, когда она нужна.
Повторяемый результат и сбой процесса
Наш экспорт формирует JSON и сохраняет его в строке задания. Сохранение результата и переход в completed идут в одной транзакции с проверкой аренды. Если процесс завершится до commit, результат не будет объявлен готовым. Если после commit потеряется ответ worker, запись уже completed и повтор не создаст второй публичный результат.
Хранение JSON в базе выбрано для небольшого учебного проекта. Для больших архивов нужен отдельный объект хранения, частная ссылка и схема согласования объекта со строкой задания. Записать файл по пути /public/export.json недостаточно: такой путь способен обойти проверки компании. Более подробный экспорт разберём в следующем отдельном уроке.
Ограничение числа попыток переводит устойчивую ошибку в failed. Бесконечная очередь повторов скрыла бы проблему и расходовала ресурсы. В ответ клиенту возвращается предметный код, а технический журнал сохраняет причину. Не записывайте весь JSON результата в текст ошибки: диагностический канал тогда станет второй копией данных компании.
Для ручного наблюдения создайте задание от Анны и посмотрите ожидаемые переходы queued → leased → completed. Затем рассмотрите остановку worker после аренды: по истечении срока новая попытка получает другой token. Поздний UPDATE первой попытки должен затронуть ноль строк. Эти переходы в курсе описаны, но не выдаются за фактический запуск.
Не все повторы требуют нового задания
Повторная обработка арендованной строки сохраняет тот же jobId. Новое осознанное нажатие экспорта создаёт другой jobId и другой снимок. Эти случаи нужно различать в интерфейсе, иначе человек увидит несколько работ и не поймёт, какие возникли из его действий, а какие из восстановления процесса. Lease token является внутренним техническим значением и не возвращается как право скачивания. Для пользователя доступ определяется компанией и текущим членством.
Следующий урок оформляет аудит действий компании. Очередь уже хранит инициатора и область; аудит позволит объяснять завершённые изменения без превращения технических журналов в общий список клиентского содержимого.