yourmaze/background-job-bundle

Database-backed background jobs for Symfony

Maintainers

Package info

github.com/yourmaze/background-job-bundle

Type:symfony-bundle

pkg:composer/yourmaze/background-job-bundle

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-08-04 11:02 UTC

This package is auto-updated.

Last update: 2026-08-04 11:03:17 UTC


README

Самостоятельный Symfony Bundle для фоновых задач с очередью в PostgreSQL. Пакет рассчитан на PHP 8.4+, Symfony 7.4 LTS или 8.1, Doctrine DBAL 3.10 или 4.x, DoctrineBundle 2.13 или 3.x и Doctrine Migrations 3.9. Он не использует Symfony Messenger, RabbitMQ, Redis и не запускает дочерний PHP-процесс для каждой задачи.

Гарантии и модель исполнения

  • Одна таблица background_job обслуживает любые типы задач.
  • Доставка at-least-once: после потери lease задача может быть выполнена повторно. Handler обязан быть идемпотентным либо использовать собственный idempotency key/checkpoint.
  • Claim выполняется одним PostgreSQL statement через FOR UPDATE SKIP LOCKED и UPDATE ... RETURNING, поэтому workers безопасно работают параллельно.
  • Каждая попытка получает новый UUID v7 lease_token. Heartbeat, progress, complete и fail применяются только для текущего непросроченного token. Старый worker не может перезаписать результат новой попытки.
  • configuration_data неизменяемы: пакет не предоставляет update API, а миграция дополнительно устанавливает PostgreSQL trigger. execution_data предназначены для progress/checkpoint.
  • Нормальный возврат handle() означает completed. Любой Throwable повторяется с capped exponential backoff и jitter, пока не исчерпаны max_attempts. NonRetryableJobException сразу завершает задачу как failed.
  • Для выполняющейся задачи отмена кооперативна: выставляется cancellation_requested_at, а handler периодически вызывает isCancellationRequested(). После возврата handler инфраструктура выставляет cancelled.

Установка

composer require yourmaze/background-job-bundle

Если Symfony Flex не зарегистрировал bundle, добавьте его вручную:

// config/bundles.php
return [
    YourMaze\BackgroundJob\BackgroundJobBundle::class => ['all' => true],
];

Минимальная конфигурация использует default Doctrine connection, Symfony clock, logger и event dispatcher:

# config/packages/background_job.yaml
background_job:
    connection_service: doctrine.dbal.default_connection
    default_max_attempts: 3
    lease_seconds: 300
    retry_base_delay_seconds: 30
    retry_max_delay_seconds: 1800
    retry_jitter: 0.20
    poll_interval_seconds: 1.0

lease_seconds должен быть больше максимального интервала между вызовами JobExecution::heartbeat() в handler-е.

Миграция

Пакет поставляет Doctrine Migration в namespace YourMaze\BackgroundJob\Migrations. Подключите каталог в приложении:

# config/packages/doctrine_migrations.yaml
doctrine_migrations:
    migrations_paths:
        'YourMaze\BackgroundJob\Migrations': '%kernel.project_dir%/vendor/yourmaze/background-job-bundle/migrations'

Затем выполните обычную команду приложения:

bin/console doctrine:migrations:migrate

Для окружений без Doctrine Migrations эквивалентная схема находится в resources/schema.sql.

Handler

<?php

declare(strict_types=1);

namespace App\Job;

use YourMaze\BackgroundJob\Handler\AsJobHandler;
use YourMaze\BackgroundJob\Handler\JobHandlerInterface;
use YourMaze\BackgroundJob\Worker\JobExecution;

#[AsJobHandler(type: 'catalog.reindex')]
final readonly class ReindexCatalogHandler implements JobHandlerInterface
{
    public function handle(JobExecution $execution): void
    {
        $catalogId = (string) $execution->configurationData()['catalog_id'];
        $checkpoint = $execution->executionData();
        $offset = (int) ($checkpoint['offset'] ?? 0);

        foreach ($this->loadProducts($catalogId, $offset) as $product) {
            if ($execution->isCancellationRequested()) {
                return;
            }

            $this->indexProductIdempotently($product);
            ++$offset;
            $execution->checkpoint(['offset' => $offset]);
            $execution->heartbeat();
        }
    }
}

Attribute работает для autoconfigured services. При явной регистрации используется тот же tag:

services:
    App\Job\ReindexCatalogHandler:
        tags:
            - { name: background_job.handler, type: catalog.reindex }

Для постоянной ошибки, которую повторять бессмысленно:

throw new YourMaze\BackgroundJob\Exception\NonRetryableJobException('Catalog no longer exists.');

Создание задач

use YourMaze\BackgroundJob\Job\ScheduleOptions;
use YourMaze\BackgroundJob\Scheduler\JobScheduler;
use Symfony\Component\Uid\Uuid;

$groupId = (string) Uuid::v7();

$jobId = $scheduler->schedule(
    type: 'catalog.reindex',
    configurationData: ['catalog_id' => 'summer-2026'],
    options: new ScheduleOptions(
        name: 'Summer catalog',
        groupId: $groupId,
        uniqueKey: 'catalog:summer-2026',
        maxAttempts: 5,
        availableAt: new DateTimeImmutable('+10 minutes'),
    ),
);

Одинаковые (type, unique_key) дедуплицируются, пока существующая задача имеет статус queued или running; scheduler вернёт её id. После terminal status тот же key можно запланировать снова.

Чтение, группы, отмена и ручный retry

$job = $jobReader->find($jobId);
$progress = $jobReader->groupProgress($groupId);

$jobControl->cancel($jobId); // queued -> cancelled; running -> cancellation request
$jobControl->retry($failedJobId, new DateTimeImmutable('+5 minutes'));

GroupProgress содержит счётчики всех статусов и completionPercentage(). Ручной retry разрешён только для failed, сбрасывает attempts и вновь ставит задачу в queued.

Workers

Один или несколько типов задаются повторяющимся option:

bin/console background-job:consume \
  --type=catalog.reindex \
  --type=export.generate \
  --memory-limit=256M \
  --poll-interval=1

Полезные режимы:

bin/console background-job:consume --type=catalog.reindex --run-once
bin/console background-job:consume --type=catalog.reindex --limit=100
bin/console background-job:consume --type=catalog.reindex --worker-id=worker-catalog-01

SIGTERM и SIGINT останавливают цикл между задачами. В v1 handler выполняется в процессе worker, поэтому сигнал не прерывает его посередине. Запуск, рестарт и hard timeout процесса настраиваются в Supervisor, systemd, Kubernetes или Docker.

Пример Supervisor:

[program:background-catalog]
command=php /app/bin/console background-job:consume --type=catalog.reindex --memory-limit=256M
numprocs=4
autorestart=true
stopwaitsecs=360

Lifecycle events

Через Symfony EventDispatcher публикуются JobScheduled, JobStarted, JobCompleted, JobRetried, JobFailed и JobCancelled. Они подходят для metrics/audit logging, но не являются транзакционным outbox: listener не должен быть необходим для корректности lifecycle задачи.

Разработка и проверки

make install
make test
make phpstan
make cs-check

make postgres-up
make test-integration
make postgres-down

Integration suite проверяет конкурентный claim, fencing token, retry/backoff, active dedupe, восстановление expired lease и неизменяемость configuration JSON.

CI проверяет нижнюю поддерживаемую комбинацию (PHP 8.4, Symfony 7.4, DBAL 3.10, DoctrineBundle 2.13) и актуальную (PHP 8.5, Symfony 8.1, DBAL 4.4, DoctrineBundle 3.3); обе также выполняют PostgreSQL integration suite.

Лицензия

Пакет распространяется на условиях MIT License.