# Python Background Worker Runtime Writing

> Используй при реализации или правке общего runtime фоновых процессов на Python: lifecycle ресурсов и его логирование, asyncio.TaskGroup, fail-fast задач, stop event, heartbeat, readiness, обработка SIGTERM/SIGINT, graceful shutdown и коды завершения. Не применять для расписания конкретной операции, NATS, HTTP, application/domain-логики и технологических адаптеров.

- Skill: `nemagu/python-background-worker-runtime-writing` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add nemagu/python-background-worker-runtime-writing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nemagu/python-background-worker-runtime-writing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Nemagu (https://skillmd.com/u/nemagu)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/nemagu/python-background-worker-runtime-writing

---


# Runtime фонового процесса на Python

Реализуй небольшой переиспользуемый runtime, который управляет процессом и задачами, но не знает, какую прикладную работу они выполняют.

## Порядок работы

1. Изучи требования к процессу, существующий composition root и lifecycle ресурсов.
2. Зафиксируй обязательные задачи, ресурсы, heartbeat, readiness, сигналы и правила завершения.
3. Отдели runtime от технологических клиентов и прикладных операций.
4. Реализуй immutable-конфигурацию, контекст ресурсов, спецификации задач и runner.
5. Добавь unit-тесты с управляемыми событиями и временем.
6. Проверь fail-fast, освобождение ресурсов и все пути завершения.

Не определяй отсутствующие продуктовые требования. Уточняй только решения, без которых невозможно безопасно собрать процесс.

## Контракт runtime

- Передавай фабрику асинхронного контекста ресурсов и неизменяемую последовательность спецификаций задач.
- Спецификация содержит техническое имя и асинхронный callable.
- Задача получает готовый runtime-контекст и общий stop event.
- Callable задачи является долгоживущим и сам владеет своим циклом до установки
  stop event. Runtime не вызывает задачи повторно и не создаёт общий цикл по их
  списку.
- Запускай задачи только после успешной подготовки всех обязательных ресурсов.
- Не передавай в runtime внешний settings-объект целиком. Принимай минимальные типизированные параметры с явными единицами измерения.
- Используй композицию. Не создавай иерархию `BackgroundBaseWorker -> TechnologyWorker`.

Пример формы контракта приведён в [runtime-design.md](references/runtime-design.md).

## Стандартная структура и именование

В новом сервисе воспроизводить этот каркас без альтернативных имён:

```text
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.

