# apexl/io-jobs

Job queue module for Cygnus (`apexl/io`). Provides a database-backed queue of deferred tasks with locking, retry, failure handling, and a console runner.

## What it provides

- **`QueueJob` entity** — a queued job row (`handler`, contents, lock state, `run_after`, failure tracking) on the `job_queue` table.
- **`TaskRunnerService`** — pulls batches of due tasks and runs each: `lock()` → `run()` → `markProcessed()` → `unlock()`, catching and routing failures to `handleFailure()`. Skips already-locked tasks.
- **`QueuedTaskFinderInterface`** (default `QueuedTaskFinderDb`) — finds and batches due tasks from the DB.
- **Task contracts** — `QueueTaskInterface` (a runnable unit: `lock`/`unlock`/`run`/`markProcessed`/`handleFailure`/`isLocked`), `QueueTaskHandlerInterface` (the work: `run()` + `handleFailure()`), and `RetryableTaskHandlerInterface` for handlers that should be retried.
- **`QueueJobTask`** — adapter wrapping a `QueueJob` as a `QueueTaskInterface`.
- **`jobs:run` console command** (`RunJobsCommand`) — processes the queue with an optional `--limit` (default 10). Registered via the `ConsoleCommands` hook.
- **`jobs:queue` console command** (`JobQueueCommand`) + **`QueueRecovery`** — operating the queue once something has failed: what is stuck, what is behind it, and retry/discard/sweep to clear it.
- **`SequencedTaskHandlerInterface`** — marks a handler whose jobs must apply in order, so a failure is a barrier rather than one job's problem.
- **`QueueProcessController`** — HTTP entry point to trigger queue processing.

## Installation

```
composer require apexl/io-jobs
```

Hosted on Bitbucket (not the `packages.apexl` Satis registry). Register the module in the host's `config/modules.php`:

```php
apexl\Io\modules\jobs\jobsModule::class,
```

Migrations install and evolve the `job_queue` table (including the `run_after` delayed-execution column).

## Key concepts / usage

A producer enqueues a `QueueJob` referencing a handler that implements `QueueTaskHandlerInterface` (or `RetryableTaskHandlerInterface`). A scheduled invocation of the runner drains the queue:

```
php bin/console jobs:run --limit=20
```

Run it on a cron. `TaskRunnerService` locks each job before running so concurrent runners don't double-process, and routes thrown exceptions to the handler's `handleFailure()` for retry/dead-lettering. The `run_after` column lets jobs be scheduled for future execution.

### When something has failed

A failed job is never picked up again — `QueuedTaskFinderDb` filters on `failed IS NULL` — so it waits for a person:

```
jobs:queue                          # status: what failed, what is queued behind it
jobs:queue handlers                 # every handler in the queue, and which ones sequence
jobs:queue show 4212                # the stored payload
jobs:queue retry 4212               # back in the queue
jobs:queue discard all --handler=AddFileJob
jobs:queue sweep                    # fail jobs left locked by a worker that died
```

Everything but `show` covers the whole queue unless `--handler` narrows it (a short class name, a comma-separated list, or `all`).

Where one family of jobs is what "the queue" means to whoever gets woken up, point the default at it:

```php
// config/jobs.php
'default_handlers' => [EtlSyncJob::class, DeleteJob::class],   // null = the whole queue
```

The bare command then means those handlers, and says so in its output, so nobody mistakes a narrowed default for an empty queue. `--handler=all` still reaches everything, and `jobs:queue handlers` always lists the whole queue.

Where a handler implements `SequencedTaskHandlerInterface`, its jobs must not overtake each other, so one failure holds the rest back. `retry` and `discard` bring that backlog forward once the last failure is cleared — clearing `run_after` and the attempt counts that blocking inflated. Handlers without the marker are left alone: their deferral is their own backoff, and resetting it would loop them.

`sweep` is for the silent stall — a worker killed mid-job leaves `locked` set, and the finder skips locked rows, so the job vanishes from the queue without failing or being reported. Only run it when no worker is running; a locked row is indistinguishable from one legitimately in flight, so hold the same lock the worker does. Swept jobs are reported on the stream named by `jobs.alert_log_stream` (default `queue`, which is file-only — point it at a stream that mails or posts).

## Conventions

`services.php` autowires `QueuedTaskFinderInterface` → `QueuedTaskFinderDb` — swap the binding to change task discovery. Tests live in `tests/` (PHPUnit).
