Runtime фонового процесса на Python
Реализуй небольшой переиспользуемый runtime, который управляет процессом и задачами, но не знает, какую прикладную работу они выполняют.
Порядок работы
- Изучи требования к процессу, существующий composition root и lifecycle ресурсов.
- Зафиксируй обязательные задачи, ресурсы, heartbeat, readiness, сигналы и правила завершения.
- Отдели runtime от технологических клиентов и прикладных операций.
- Реализуй immutable-конфигурацию, контекст ресурсов, спецификации задач и runner.
- Добавь unit-тесты с управляемыми событиями и временем.
- Проверь fail-fast, освобождение ресурсов и все пути завершения.
Не определяй отсутствующие продуктовые требования. Уточняй только решения, без которых невозможно безопасно собрать процесс.
Контракт runtime
- Передавай фабрику асинхронного контекста ресурсов и неизменяемую последовательность спецификаций задач.
- Спецификация содержит техническое имя и асинхронный callable.
- Задача получает готовый runtime-контекст и общий stop event.
- Callable задачи является долгоживущим и сам владеет своим циклом до установки
stop event. Runtime не вызывает задачи повторно и не создаёт общий цикл по их
списку.
- Запускай задачи только после успешной подготовки всех обязательных ресурсов.
- Не передавай в runtime внешний settings-объект целиком. Принимай минимальные типизированные параметры с явными единицами измерения.
- Используй композицию. Не создавай иерархию
BackgroundBaseWorker -> TechnologyWorker.
Пример формы контракта приведён в runtime-design.md.
Стандартная структура и именование
В новом сервисе воспроизводить этот каркас без альтернативных имён:
presentation/background/
├── runtime.py
├── signals.py
├── <operation_name>.py
└── <technology>/<process_name>.py
entrypoints/<process_name>.py
runtime.py: BackgroundRuntime, BackgroundRunner, RuntimeOptions,
WorkerTaskSpec, TaskStoppedError, ShutdownTimeoutError.
signals.py: FileHeartbeat, FileReadiness, FileHealthcheck и
ReadinessState для HTTP-процесса, когда соответствующие сигналы требуются.
<technology>/<process_name>.py: <ProcessName>Process, immutable
<ProcessName>Context, публичный build_runtime() и resources().
<operation_name>.py: <OperationName>Task с публичным run().
entrypoints/<process_name>.py: main(), загружающий settings, выполняющий
preflight и запускающий результат process.build_runtime() через общий runner.
Связывать роли только композицией. Не наследовать конкретные процессы или задачи
от общего worker-класса. В существующем проекте сохранять эквивалентные имена,
если их переименование не входит в задачу.
Управление задачами
- Используй только
asyncio.TaskGroup.
- Не поддерживай параллельно ручной реестр задач с
create_task, cancel и gather.
- Неожиданное исключение или преждевременное завершение обязательной задачи завершает runtime и отменяет соседние задачи.
- Не подавляй
CancelledError.
- Обрабатывай
ExceptionGroup один раз на внешней границе процесса.
- Не допускай фоновых задач, жизненный цикл которых не принадлежит
TaskGroup.
- Запускай каждую независимую долгоживущую задачу отдельным элементом
TaskGroup; не выполняй их последовательно внутри общего рабочего цикла.
Ресурсы
- Управляй ресурсами через
AsyncExitStack.
- Создавай ресурсы фабриками, переданными из composition root.
- Храни готовые зависимости в неизменяемом runtime-контексте.
- Закрывай ресурсы в обратном порядке, включая случай частично успешного запуска.
- Не запускай задачи при неуспешной подготовке хотя бы одного обязательного ресурса.
Остановка процесса
- Обработчики
SIGTERM и SIGINT устанавливает runner, а не runtime-контекст и не прикладная задача.
- Первый сигнал инициирует graceful shutdown через stop event.
- Повторный сигнал инициирует немедленную отмену.
- Ограничивай graceful shutdown отдельным timeout.
- Восстанавливай прежние signal handlers после завершения.
- В тестах инициируй остановку через событие без настоящих OS-сигналов.
- Runner возвращает код завершения; глубокие функции не вызывают
sys.exit.
Рекомендуемая семантика:
- успешная остановка и первый
SIGTERM — код 0;
- ошибка запуска или обязательной задачи — ненулевой код;
- повторный сигнал или превышение shutdown timeout — ненулевой код.
Ожидания и время
- Не используй обычный
asyncio.sleep() для интервалов управляемых задач.
- Используй stop-aware ожидание, которое немедленно завершается при установке stop event.
- Для измерения длительностей используй монотонные часы.
- Не смешивай интервалы рабочего расписания, retry backoff и shutdown timeout.
- Позволяй подменить часы или ожидание в тестах.
Heartbeat и readiness
- Различай периодический liveness heartbeat и progress heartbeat после завершения
рабочей итерации.
- Периодический heartbeat реализуй отдельной обязательной задачей в том же
TaskGroup только когда это задано требованиями.
- Progress heartbeat обновляет владеющая рабочим циклом задача; runtime лишь
передаёт ей технический порт через готовый контекст.
- Записывай heartbeat через порт; runtime и задача не должны знать, файл это,
метрика или другой механизм.
- Запускай heartbeat только после готовности ресурсов.
- Остановка или ошибка обязательной задачи прекращает принадлежащий процессу
heartbeat вместе со всем runtime.
- Readiness отделяй от heartbeat. Если готовность зависит от последовательности
исходов рабочей итерации, её обновляет владеющая итерацией задача через
технический порт по согласованной политике.
- Для headless worker-а без HTTP использовать стандартные файловые сигналы:
heartbeat с timestamp и отдельный readiness marker. Readiness probe считать
успешной только при наличии marker-а и свежем heartbeat.
- Создавать readiness marker только после подготовки обязательных ресурсов;
удалять до startup, при снятии готовности и в
finally shutdown.
- Предоставлять import-safe
entrypoints/healthcheck.py, который загружает config,
проверяет liveness или readiness и возвращает код 0 либо 1.
- Не проверять из probe PostgreSQL/NATS напрямую: это не подтверждает движение
рабочей задачи и создаёт отдельную нагрузку на зависимости.
Логирование
Применяй python-service-logging-writing и профиль процесса. Runtime владеет
только lifecycle-записями: готовностью обязательных ресурсов, началом и
завершением процесса, сигналом остановки, startup failure и shutdown timeout.
- Связывай общий process context до запуска обязательных задач.
- Записывай успешный startup только после подготовки ресурсов и установки
начальной readiness.
- Записывай остановку до закрытия operation context, а завершение — после
освобождения ресурсов согласно требованиям.
- Переход readiness записывает компонент, который принимает решение о переходе:
runtime — для startup/shutdown, конкретная task — для исходов её итераций.
- Обрабатывай
ExceptionGroup и фатальный исход один раз на внешней границе
runner-а. Не повторяй stack trace уже записанной task-ошибки.
- Не логируй каждый heartbeat и успешный healthcheck, если профиль явно этого не
требует.
- Не закрепляй собственную универсальную схему полей поверх контракта сервиса.
Границы
В область скила входят:
TaskGroup, stop event и stop-aware ожидание;
- параллельный lifecycle независимых долгоживущих задач с собственными циклами;
AsyncExitStack и runtime-контекст;
- task specifications;
- heartbeat-порт и readiness;
- runner, сигналы, shutdown timeout и exit codes;
- тесты перечисленного поведения.
Не входят:
- расписание прикладной операции;
- NATS, PostgreSQL и другие технологические клиенты;
- payload, сообщения, ACK/NAK/TERM;
- application- и domain-логика;
- outbox, Unit of Work, HTTP и миграции.
Проверка результата
- Все обязательные задачи принадлежат одному
TaskGroup.
- Ошибка любой обязательной задачи останавливает процесс.
- Частично созданные ресурсы закрываются в обратном порядке.
- Первый и повторный сигналы имеют разную семантику.
- Общий цикл по всем рабочим задачам отсутствует; каждая задача владеет своим
расписанием и состоянием итераций.
- Периодический и progress heartbeat не смешаны; выбранный режим соответствует
требованиям.
- Readiness не выводится из heartbeat и изменяется только согласованным владельцем.
- Файловая readiness наблюдаема снаружи и невозможна при устаревшем heartbeat.
- Интервалы прерываются остановкой.
- Lifecycle-события и общий process context соответствуют logging-профилю, а
task-события не дублируются runtime-ом.
- Unit-тесты покрывают запуск, fail-fast, остановку, timeout, сигналы, heartbeat,
ExceptionGroup, отмену и exit codes.
- Новый сервис следует стандартной структуре и имеет отдельный entrypoint.
1---2name: python-background-worker-runtime-writing3description: Используй при реализации или правке общего runtime фоновых процессов на Python: lifecycle ресурсов и его логирование, asyncio.TaskGroup, fail-fast задач, stop event, heartbeat, readiness, обработка SIGTERM/SIGINT, graceful shutdown и коды завершения. Не применять для расписания конкретной операции, NATS, HTTP, application/domain-логики и технологических адаптеров.4---56# Runtime фонового процесса на Python78Реализуй небольшой переиспользуемый runtime, который управляет процессом и задачами, но не знает, какую прикладную работу они выполняют.910## Порядок работы11121. Изучи требования к процессу, существующий composition root и lifecycle ресурсов.132. Зафиксируй обязательные задачи, ресурсы, heartbeat, readiness, сигналы и правила завершения.143. Отдели runtime от технологических клиентов и прикладных операций.154. Реализуй immutable-конфигурацию, контекст ресурсов, спецификации задач и runner.165. Добавь unit-тесты с управляемыми событиями и временем.176. Проверь fail-fast, освобождение ресурсов и все пути завершения.1819Не определяй отсутствующие продуктовые требования. Уточняй только решения, без которых невозможно безопасно собрать процесс.2021## Контракт runtime2223- Передавай фабрику асинхронного контекста ресурсов и неизменяемую последовательность спецификаций задач.24- Спецификация содержит техническое имя и асинхронный callable.25- Задача получает готовый runtime-контекст и общий stop event.26- Callable задачи является долгоживущим и сам владеет своим циклом до установки27 stop event. Runtime не вызывает задачи повторно и не создаёт общий цикл по их28 списку.29- Запускай задачи только после успешной подготовки всех обязательных ресурсов.30- Не передавай в runtime внешний settings-объект целиком. Принимай минимальные типизированные параметры с явными единицами измерения.31- Используй композицию. Не создавай иерархию `BackgroundBaseWorker -> TechnologyWorker`.3233Пример формы контракта приведён в [runtime-design.md](references/runtime-design.md).3435## Стандартная структура и именование3637В новом сервисе воспроизводить этот каркас без альтернативных имён:3839```text40presentation/background/41├── runtime.py42├── signals.py43├── <operation_name>.py44└── <technology>/<process_name>.py45entrypoints/<process_name>.py46```4748- `runtime.py`: `BackgroundRuntime`, `BackgroundRunner`, `RuntimeOptions`,49 `WorkerTaskSpec`, `TaskStoppedError`, `ShutdownTimeoutError`.50- `signals.py`: `FileHeartbeat`, `FileReadiness`, `FileHealthcheck` и51 `ReadinessState` для HTTP-процесса, когда соответствующие сигналы требуются.52- `<technology>/<process_name>.py`: `<ProcessName>Process`, immutable53 `<ProcessName>Context`, публичный `build_runtime()` и `resources()`.54- `<operation_name>.py`: `<OperationName>Task` с публичным `run()`.55- `entrypoints/<process_name>.py`: `main()`, загружающий settings, выполняющий56 preflight и запускающий результат `process.build_runtime()` через общий runner.5758Связывать роли только композицией. Не наследовать конкретные процессы или задачи59от общего worker-класса. В существующем проекте сохранять эквивалентные имена,60если их переименование не входит в задачу.6162## Управление задачами6364- Используй только `asyncio.TaskGroup`.65- Не поддерживай параллельно ручной реестр задач с `create_task`, `cancel` и `gather`.66- Неожиданное исключение или преждевременное завершение обязательной задачи завершает runtime и отменяет соседние задачи.67- Не подавляй `CancelledError`.68- Обрабатывай `ExceptionGroup` один раз на внешней границе процесса.69- Не допускай фоновых задач, жизненный цикл которых не принадлежит `TaskGroup`.70- Запускай каждую независимую долгоживущую задачу отдельным элементом71 `TaskGroup`; не выполняй их последовательно внутри общего рабочего цикла.7273## Ресурсы7475- Управляй ресурсами через `AsyncExitStack`.76- Создавай ресурсы фабриками, переданными из composition root.77- Храни готовые зависимости в неизменяемом runtime-контексте.78- Закрывай ресурсы в обратном порядке, включая случай частично успешного запуска.79- Не запускай задачи при неуспешной подготовке хотя бы одного обязательного ресурса.8081## Остановка процесса8283- Обработчики `SIGTERM` и `SIGINT` устанавливает runner, а не runtime-контекст и не прикладная задача.84- Первый сигнал инициирует graceful shutdown через stop event.85- Повторный сигнал инициирует немедленную отмену.86- Ограничивай graceful shutdown отдельным timeout.87- Восстанавливай прежние signal handlers после завершения.88- В тестах инициируй остановку через событие без настоящих OS-сигналов.89- Runner возвращает код завершения; глубокие функции не вызывают `sys.exit`.9091Рекомендуемая семантика:9293- успешная остановка и первый `SIGTERM` — код `0`;94- ошибка запуска или обязательной задачи — ненулевой код;95- повторный сигнал или превышение shutdown timeout — ненулевой код.9697## Ожидания и время9899- Не используй обычный `asyncio.sleep()` для интервалов управляемых задач.100- Используй stop-aware ожидание, которое немедленно завершается при установке stop event.101- Для измерения длительностей используй монотонные часы.102- Не смешивай интервалы рабочего расписания, retry backoff и shutdown timeout.103- Позволяй подменить часы или ожидание в тестах.104105## Heartbeat и readiness106107- Различай периодический liveness heartbeat и progress heartbeat после завершения108 рабочей итерации.109- Периодический heartbeat реализуй отдельной обязательной задачей в том же110 `TaskGroup` только когда это задано требованиями.111- Progress heartbeat обновляет владеющая рабочим циклом задача; runtime лишь112 передаёт ей технический порт через готовый контекст.113- Записывай heartbeat через порт; runtime и задача не должны знать, файл это,114 метрика или другой механизм.115- Запускай heartbeat только после готовности ресурсов.116- Остановка или ошибка обязательной задачи прекращает принадлежащий процессу117 heartbeat вместе со всем runtime.118- Readiness отделяй от heartbeat. Если готовность зависит от последовательности119 исходов рабочей итерации, её обновляет владеющая итерацией задача через120 технический порт по согласованной политике.121- Для headless worker-а без HTTP использовать стандартные файловые сигналы:122 heartbeat с timestamp и отдельный readiness marker. Readiness probe считать123 успешной только при наличии marker-а и свежем heartbeat.124- Создавать readiness marker только после подготовки обязательных ресурсов;125 удалять до startup, при снятии готовности и в `finally` shutdown.126- Предоставлять import-safe `entrypoints/healthcheck.py`, который загружает config,127 проверяет `liveness` или `readiness` и возвращает код `0` либо `1`.128- Не проверять из probe PostgreSQL/NATS напрямую: это не подтверждает движение129 рабочей задачи и создаёт отдельную нагрузку на зависимости.130131## Логирование132133Применяй `python-service-logging-writing` и профиль процесса. Runtime владеет134только lifecycle-записями: готовностью обязательных ресурсов, началом и135завершением процесса, сигналом остановки, startup failure и shutdown timeout.136137- Связывай общий process context до запуска обязательных задач.138- Записывай успешный startup только после подготовки ресурсов и установки139 начальной readiness.140- Записывай остановку до закрытия operation context, а завершение — после141 освобождения ресурсов согласно требованиям.142- Переход readiness записывает компонент, который принимает решение о переходе:143 runtime — для startup/shutdown, конкретная task — для исходов её итераций.144- Обрабатывай `ExceptionGroup` и фатальный исход один раз на внешней границе145 runner-а. Не повторяй stack trace уже записанной task-ошибки.146- Не логируй каждый heartbeat и успешный healthcheck, если профиль явно этого не147 требует.148- Не закрепляй собственную универсальную схему полей поверх контракта сервиса.149150## Границы151152В область скила входят:153154- `TaskGroup`, stop event и stop-aware ожидание;155- параллельный lifecycle независимых долгоживущих задач с собственными циклами;156- `AsyncExitStack` и runtime-контекст;157- task specifications;158- heartbeat-порт и readiness;159- runner, сигналы, shutdown timeout и exit codes;160- тесты перечисленного поведения.161162Не входят:163164- расписание прикладной операции;165- NATS, PostgreSQL и другие технологические клиенты;166- payload, сообщения, ACK/NAK/TERM;167- application- и domain-логика;168- outbox, Unit of Work, HTTP и миграции.169170## Проверка результата171172- Все обязательные задачи принадлежат одному `TaskGroup`.173- Ошибка любой обязательной задачи останавливает процесс.174- Частично созданные ресурсы закрываются в обратном порядке.175- Первый и повторный сигналы имеют разную семантику.176- Общий цикл по всем рабочим задачам отсутствует; каждая задача владеет своим177 расписанием и состоянием итераций.178- Периодический и progress heartbeat не смешаны; выбранный режим соответствует179 требованиям.180- Readiness не выводится из heartbeat и изменяется только согласованным владельцем.181- Файловая readiness наблюдаема снаружи и невозможна при устаревшем heartbeat.182- Интервалы прерываются остановкой.183- Lifecycle-события и общий process context соответствуют logging-профилю, а184 task-события не дублируются runtime-ом.185- Unit-тесты покрывают запуск, fail-fast, остановку, timeout, сигналы, heartbeat, `ExceptionGroup`, отмену и exit codes.186- Новый сервис следует стандартной структуре и имеет отдельный entrypoint.