
# Jobs

Jobs sind die zentrale Arbeitseinheit in Queuety. Löse einen Job mit Handler-Namen und Payload aus; ein Worker übernimmt ihn und führt ihn im Hintergrund aus.

## Jobs auslösen

```php
use Queuety\Queuety;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] );
```

`dispatch()` gibt einen `PendingJob`-Builder zurück. Du kannst Optionen verketten, bevor der Job automatisch ausgelöst wird:

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High )
    ->delay( 300 )
    ->max_attempts( 5 );
```

Rufe `->id()` auf, um sofort die Job-ID zu erhalten:

```php
$job_id = Queuety::dispatch( 'send_email', $payload )->id();
```

## Handler schreiben

Ein Handler ist eine beliebige Klasse, die das Interface `Queuety\Handler` implementiert:

```php
use Queuety\Handler;

class SendEmailHandler implements Handler {
    public function handle( array $payload ): void {
        wp_mail( $payload['to'], $payload['subject'], $payload['body'] );
    }

    public function config(): array {
        return [
            'queue'           => 'emails',
            'max_attempts'    => 5,
            'backoff'         => 'exponential',
        ];
    }
}
```

Die Methode `handle()` erhält die Job-Payload und führt die Arbeit aus. Die Methode `config()` gibt optionale Standardwerte zurück:

| Schlüssel | Typ | Beschreibung |
|---|---|---|
| `queue` | `string` | Standardname der Warteschlange |
| `max_attempts` | `int` | Maximale Anzahl Ausführungen einschließlich des ersten Versuchs, bevor der Job zurückgestellt wird |
| `backoff` | `string` | Backoff-Strategie für Wiederholungen: `exponential`, `linear` oder `fixed` |
| `rate_limit` | `array` | Ratenbegrenzung als `[max_executions, window_seconds]` |
| `concurrency_group` | `string` | Gemeinsame Worker-Aufnahmegruppe für aufwendige Handler |
| `concurrency_limit` | `int` | Maximale Anzahl gleichzeitig verarbeiteter Jobs in dieser Gruppe |
| `cost_units` | `int` | Relative Ausführungskosten für gewichtete Warteschlangen- oder Anbieterbudgets, Workflow-Budgets und Aufnahmeheuristiken |

Diese Ressourcenschlüssel sind Teil des umfassenderen Modells für [Ressourcenverwaltung](https://queuety.inline0.dev/de/docs/features/resource-management) von Queuety, in dem Job-Metadaten, Workflow-Budgets und Worker-Aufnahme zusammenwirken.

Registriere den Handler:

```php
Queuety::register( 'send_email', SendEmailHandler::class );
```

Oder verwende [PHP-Attribute](https://queuety.inline0.dev/de/docs/features/attributes) zur automatischen Registrierung.

## Priorität

Jobs haben vier Prioritätsstufen. Worker beanspruchen Jobs mit höherer Priorität zuerst.

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'handler', $payload )->with_priority( Priority::Urgent );
```

| Stufe | Wert | Beschreibung |
|---|---|---|
| `Priority::Low` | `0` | Standard. Hintergrundarbeit, die warten kann. |
| `Priority::Normal` | `1` | Normale Priorität. |
| `Priority::High` | `2` | Wird vor Low und Normal verarbeitet. |
| `Priority::Urgent` | `3` | Wird zuerst verarbeitet. Sparsam verwenden. |

## Verzögerung

Verzögere einen Job, damit er erst nach der angegebenen Anzahl Sekunden verfügbar wird:

```php
Queuety::dispatch( 'send_reminder', $payload )->delay( 3600 ); // 1 hour
```

Der Job wird sofort eingefügt, aber Worker beanspruchen ihn erst nach Ablauf der Verzögerung.

## Eindeutige Jobs

Verhindere doppelte Auslösungen für denselben Handler und dieselbe Payload. Wenn bereits ein ausstehender oder verarbeiteter Job mit demselben Handler und derselben Payload existiert, wird dessen Job-ID zurückgegeben, statt einen neuen Job anzulegen.

```php
Queuety::dispatch( 'sync_inventory', [ 'sku' => 'ABC-123' ] )->unique();
```

## Job-Abhängigkeiten

Ein Job kann von einem anderen Job abhängen. Ein Worker beansprucht ihn erst, wenn die Abhängigkeit abgeschlossen ist.

```php
$first_id = Queuety::dispatch( 'import_data', $payload )->id();

Queuety::dispatch( 'send_notification', [ 'import_id' => $first_id ] )
    ->after( $first_id );
```

## Batch-Auslösung

Löse mehrere Jobs in einem einzigen INSERT mit mehreren Zeilen aus, um den Durchsatz zu maximieren:

```php
$job_ids = Queuety::batch( [
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'a@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'b@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'c@example.com' ] ],
] );
```

Jeder Eintrag kann optionale Schlüssel enthalten: `queue`, `priority`, `delay`, `max_attempts`.

## Wiederholungen und Backoff

Wenn ein Job eine Ausnahme auslöst, setzt Queuety ihn mit einem verzögerten `available_at` zurück auf `pending`, bis er insgesamt `max_attempts`-mal ausgeführt wurde (der erste Lauf zählt; `max_attempts( 3 )` bedeutet also bis zu zwei Wiederholungen). Die Wartezeit vor jedem erneuten Versuch richtet sich nach der Backoff-Strategie. Dabei bezeichnet `attempt` die Nummer des gerade fehlgeschlagenen Laufs:

| Strategie | Verzögerungsformel | Beispiel (nach Lauf 1, 2, 3) |
|---|---|---|
| `exponential` | `2^attempt * 30` Sekunden, begrenzt auf 3600 | 60s, 120s, 240s |
| `linear` | `attempt * 60` Sekunden | 60s, 120s, 180s |
| `fixed` | `60` Sekunden | 60s, 60s, 60s |

Lege den globalen Standard in `wp-config.php` fest:

```php
define( 'QUEUETY_RETRY_BACKOFF', 'linear' );
```

Oder pro Handler in `config()`:

```php
public function config(): array {
    return [ 'backoff' => 'exponential', 'max_attempts' => 5 ];
}
```

Oder per Dispatch:

```php
Queuety::dispatch( 'handler', $payload )->max_attempts( 10 );
```

## Zurückgestellte Jobs

Wenn alle Wiederholungsversuche ausgeschöpft sind, wird ein Job als **begraben** markiert. Begrabene Jobs bleiben zur Prüfung in der Datenbank, werden aber nicht verarbeitet. Du kannst sie manuell erneut versuchen. Bei einem manuellen erneuten Versuch wird der Job wieder zu `pending`, ohne den Versuchszähler zurückzusetzen. Ein erneut versuchter begrabener Job erhält also einen weiteren Lauf und wird wieder begraben, wenn dieser Lauf fehlschlägt:

```php
// Retry a specific buried job
Queuety::retry( $job_id );

// Retry all buried jobs
$count = Queuety::retry_buried();
```

Oder über CLI:

```bash
wp queuety retry 42
wp queuety retry-buried
```

## Timeout

Queuety startet einen `pcntl_alarm()` für die maximale Ausführungszeit und soll den Versuch mit einem `TimeoutException` fehlschlagen lassen, wenn diese überschritten wird. Dieses Timeout wird derzeit nicht erzwungen ([#10](https://github.com/inline0/queuety/issues/10)): Der Worker aktiviert keine asynchronen Signale, sodass ein lang laufender Handler bis zum Ende ausgeführt wird. Ein in `sleep()` blockierter Handler wird beim Auslösen des Alarms abgebrochen und anschließend als abgeschlossen markiert. Konfiguriere das Timeout global:

```php
define( 'QUEUETY_MAX_EXECUTION_TIME', 300 ); // 5 minutes
```

Jobs, die länger als `QUEUETY_STALE_TIMEOUT` Sekunden den Status `processing` haben, werden automatisch wiederhergestellt:

```php
define( 'QUEUETY_STALE_TIMEOUT', 600 ); // 10 minutes
```

Veraltete Jobs manuell wiederherstellen:

```bash
wp queuety recover
```

## Warteschlangen

Jobs werden an benannte Warteschlangen gesendet. Jeder Worker verarbeitet eine Warteschlange oder eine geordnete Liste von Warteschlangen (siehe [Workers](https://queuety.inline0.dev/de/docs/features/workers#multi-queue-priorities)):

```php
Queuety::dispatch( 'handler', $payload )->on_queue( 'emails' );
```

```bash
wp queuety work --queue=emails
```

Die Standardwarteschlange ist `default`. Du kannst für jede Warteschlange einen eigenen Worker-Prozess ausführen, um Workloads voneinander zu isolieren.

## Ratenbegrenzung

Begrenze, wie oft ein Handler innerhalb eines Zeitfensters ausgeführt werden kann:

```php
Queuety::dispatch( 'call_api', $payload )
    ->rate_limit( 10, 60 ); // max 10 executions per 60 seconds
```

Weitere Details findest du unter [Ratenbegrenzung](https://queuety.inline0.dev/de/docs/features/rate-limiting).

## Dispatchbare Jobklassen

Queuety v0.6.0 führt ein klassenbasiertes Job-API ein. Statt Handlernamen, Payload und Handlerklasse getrennt zu verwalten, definierst du eine einzelne Klasse, die alles kapselt: die Payload (als typisierte öffentliche Eigenschaften), die Ausführungslogik und optionale Middleware.

### Die Schnittstelle `Contracts\Job`

```php
use Queuety\Contracts\Job;

interface Job {
    public function handle(): void;
}
```

Eine `Job`-Klasse speichert ihre Payload in öffentlichen Eigenschaften und führt ihre eigene Logik in `handle()` aus. Eine separate Methode `config()` oder Handlerregistrierung ist nicht erforderlich.

### Das Trait `Dispatchable`

Füge deiner Jobklasse das Trait `Dispatchable` hinzu, damit sie eine statische Methode `dispatch()` erhält:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class SendEmailJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $to,
        public readonly string $subject,
        public readonly string $body,
    ) {}

    public function handle(): void {
        wp_mail( $this->to, $this->subject, $this->body );
    }
}
```

Sende den Job, indem du die Konstruktorargumente übergibst:

```php
SendEmailJob::dispatch( 'user@example.com', 'Welcome!', 'Hello there.' )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High );
```

Das Trait `Dispatchable` ruft intern `Queuety::dispatch()` auf, sodass alle Optionen von `PendingJob` (queue, priority, delay, max_attempts, unique, after) genauso funktionieren.

### So funktioniert die Serialisierung

Wenn du eine Instanz von `Job` sendest, extrahiert Queuetys `JobSerializer` den vollständig qualifizierten Klassennamen als Handler und alle öffentlichen Eigenschaften als Payload. Auf der Worker-Seite rekonstruiert der Serializer die Instanz, indem er die Payload-Schlüssel den Namen der Konstruktorparameter zuordnet. Backed Enums werden in ihren zugrunde liegenden Wert serialisiert und automatisch wiederhergestellt.

### Job-Eigenschaften

Jobklassen können öffentliche Eigenschaften definieren, um Wiederholungs- und Timeout-Verhalten zu steuern. Der Worker liest diese per Reflection aus und wendet sie automatisch an.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ImportProductsJob implements Job {
    use Dispatchable;

    public int $tries = 5;
    public int $timeout = 120;
    public array $backoff = [ 10, 30, 60, 120 ];
    public string $concurrency_group = 'imports';
    public int $concurrency_limit = 2;
    public int $cost_units = 4;

    public function __construct(
        public readonly int $supplier_id,
    ) {}

    public function handle(): void {
        // Import products...
    }
}
```

| Eigenschaft | Typ | Beschreibung |
|---|---|---|
| `$tries` | `int` | Maximale Anzahl von Versuchen, bevor der Job begraben wird. Überschreibt den Wert `max_attempts` beim Dispatch. |
| `$timeout` | `int` | Maximale Ausführungszeit in Sekunden. Überschreibt die globale Konstante `QUEUETY_MAX_EXECUTION_TIME`. Erfordert `pcntl` und wird noch nicht erzwungen (siehe [Timeout](https://queuety.inline0.dev/de/docs/jobs#timeout)). |
| `$backoff` | `array` | Array mit Verzögerungswerten in Sekunden für jeden Wiederholungsversuch. Der letzte Wert wird für alle weiteren Versuche wiederverwendet. |
| `$concurrency_group` | `string` | Gemeinsame Worker-Zulassungsgruppe für aufwendige Jobs. |
| `$concurrency_limit` | `int` | Maximale Anzahl gleichzeitiger Jobs vom Typ `processing` in dieser Gruppe. Wird ohne `$concurrency_group` ignoriert. |
| `$cost_units` | `int` | Relative Ausführungskosten für Workflow-Budgets und die Zulassung durch Worker. |

Das Array `$backoff` legt zunehmend längere Verzögerungen zwischen Wiederholungsversuchen fest. Zum Beispiel bedeutet `[10, 30, 60, 120]`: Vor dem ersten Wiederholungsversuch wird 10 Sekunden gewartet, vor dem zweiten 30 Sekunden, vor dem dritten 60 Sekunden und vor allen weiteren 120 Sekunden.

Eigenschaften zu Ressourcen sind nützlich, wenn Jobklassen unterschiedliche Kostenprofile abbilden. Eine schnelle Cache-Aktualisierung und ein umfangreicher Aufruf bei einem Anbieter können beide „ein Job“ sein, sollten aber nicht unbedingt gleich gewichtet werden, wenn ein Workflow-Budget oder eine Worker-Zulassungsentscheidung den Prozess innerhalb sicherer Grenzen halten soll.

Das vollständige Modell einschließlich Workflow-Budgets und Zulassung auf Worker-Seite findest du unter [Ressourcenverwaltung](https://queuety.inline0.dev/de/docs/features/resource-management).

### Hook bei Fehlschlag

Wenn ein Job nach Ausschöpfen aller Wiederholungsversuche begraben wird, ruft Queuety eine Methode `failed()` auf der Jobinstanz auf, sofern eine definiert ist. Nutze sie für Bereinigungen, Warnmeldungen oder Protokollierung.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ProcessPaymentJob implements Job {
    use Dispatchable;

    public int $tries = 3;

    public function __construct(
        public readonly int $order_id,
    ) {}

    public function handle(): void {
        // Process the payment...
    }

    public function failed( \Throwable $exception ): void {
        // Notify the team that payment processing failed permanently.
        error_log( "Payment failed for order {$this->order_id}: {$exception->getMessage()}" );
    }
}
```

Die Methode `failed()` erhält die Ausnahme, die zum endgültigen Fehlschlag geführt hat. Die Jobinstanz wird mit ihrer ursprünglichen Payload deserialisiert, sodass alle öffentlichen Eigenschaften verfügbar sind.

### Bedingter Dispatch

Das Trait `Dispatchable` stellt `dispatch_if()` und `dispatch_unless()` für bedingtes Senden bereit. Beide erwarten als erstes Argument eine boolesche Bedingung, gefolgt von den Konstruktorargumenten des Jobs.

```php
// Only dispatch if the user has opted in to notifications.
SendEmailJob::dispatch_if( $user->wants_notifications, $user->email, 'Update', $body );

// Dispatch unless the feature flag is disabled.
SyncInventoryJob::dispatch_unless( $maintenance_mode, $sku );
```

`dispatch_if()` sendet den Job, wenn die Bedingung `true` ist, und gibt das `PendingJob` zurück. Wenn die Bedingung `false` ist, gibt es `null` zurück.

`dispatch_unless()` sendet den Job, wenn die Bedingung `false` ist, und gibt das `PendingJob` zurück. Wenn die Bedingung `true` ist, gibt es `null` zurück.

Beide Methoden unterstützen das Verketten von `PendingJob`-Optionen beim Senden:

```php
SendEmailJob::dispatch_if( $should_send, $to, $subject, $body )
    ?->on_queue( 'emails' )
    ?->with_priority( Priority::High );
```

### Synchrone Ausführung

Nutze `Queuety::dispatch_sync()`, um einen Job sofort im aktuellen Prozess auszuführen, ohne ihn zur Warteschlange hinzuzufügen. Das ist hilfreich beim Testen und für Jobs, die direkt ausgeführt werden müssen.

```php
use Queuety\Queuety;

// With a Job instance
Queuety::dispatch_sync( new SendEmailJob( to: 'user@example.com', subject: 'Hello', body: 'Hi!' ) );

// With a handler name and payload
Queuety::dispatch_sync( 'send_email', [ 'to' => 'user@example.com' ] );
```

Die Methode `handle()` des Jobs wird direkt aufgerufen. Es wird kein Datenbankeintrag erstellt, kein Worker eingesetzt und keine Middleware angewendet.

### Jobverkettung

Bei einer Jobverkettung wird eine Folge von Jobs nacheinander ausgeführt. Jeder Job in der Kette hängt davon ab, dass der vorherige erfolgreich abgeschlossen wird. Wenn ein Job fehlschlägt, bleiben die nachfolgenden Jobs in der Kette ausstehend.

#### Mit `Queuety::chain()`

```php
use Queuety\Queuety;

$first_job_id = Queuety::chain( [
    new FetchDataJob( $url ),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->catch( ChainFailedHandler::class )
    ->dispatch();
```

`Queuety::chain()` gibt ein `ChainBuilder` mit Fluent-Methoden zum Festlegen der Warteschlange und eines Fehlerhandlers zurück. Die Methode `dispatch()` erstellt alle Jobs mit sequenziellen `depends_on`-Beziehungen und gibt die ID des ersten Jobs zurück.

#### Mit `with_chain()`

Das Trait `Dispatchable` stellt eine Methode `with_chain()` bereit. Sie entspricht derzeit `Queuety::chain( $jobs )`: Die Klasse, auf der sie aufgerufen wird, wird nicht zur Kette hinzugefügt ([#13](https://github.com/inline0/queuety/issues/13)). Führe daher auch den ersten Job im Array auf:

```php
FetchDataJob::with_chain( [
    new FetchDataJob(),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->dispatch();
```

#### Fehlerbehandlung bei Ketten

Registriere mit `catch()` eine Fehlerhandlerklasse. Die Handlerklasse muss über eine Methode `handle()` verfügen. Sie wird aufgerufen, wenn ein Job in der Kette endgültig begraben wird. Der Worker unterstützt `handle()`, `handle( Job $job )` und `handle( Job $job, \Throwable $exception )`.

```php
class ChainFailedHandler {
    public function handle(): void {
        // Alert, clean up, etc.
    }
}
```

### Middleware hinzufügen

Jobklassen können eine Methode `middleware()` definieren, die Middleware zurückgibt, um die Ausführung zu umschließen:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;

class CallOpenAIJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $prompt,
    ) {}

    public function handle(): void {
        // Call the API...
    }

    public function middleware(): array {
        return [
            new RateLimited( max: 60, window: 60 ),
            new Timeout( seconds: 120 ),
        ];
    }
}
```

Details zu integrierter Middleware und zum Schreiben eigener Middleware findest du unter [Middleware](https://queuety.inline0.dev/de/docs/features/middleware).

### Vergleich mit handlerbasierten Jobs

| | Handlerbasiert (v0.1+) | Klassenbasiert (v0.6+) |
|---|---|---|
| Dispatch | `Queuety::dispatch( 'send_email', $payload )` | `SendEmailJob::dispatch( $to, $subject )` |
| Handler | Separate Klasse, die `Handler` implementiert | Dieselbe Klasse implementiert `Contracts\Job` |
| Payload | Ungetyptes `array $payload` | Typisierte öffentliche Eigenschaften |
| Registrierung | `Queuety::register()` oder `#[QueuetyHandler]` | Nicht erforderlich (verwendet FQCN) |
| Middleware | Nicht verfügbar | Optionale Methode `middleware()` |
| Konfiguration | Methode `config()` | Job-Eigenschaften + `PendingJob`-Kette |

Beide Ansätze werden vollständig unterstützt. Bestehende handlerbasierte Jobs funktionieren unverändert weiter. Du kannst beide Stile im selben Projekt kombinieren.

## Job-Lebenszyklus

```
dispatch() -> pending -> processing -> completed
                  |           |
                  |           +-> exception -> pending (retry after backoff)
                  |                       |
                  |                       +-> buried (max attempts reached)
                  |
                  +-> delayed (available_at in the future)
```

| Status | Beschreibung |
|---|---|
| `pending` | Wartet darauf, von einem Worker übernommen zu werden |
| `processing` | Wird gerade ausgeführt |
| `completed` | Erfolgreich abgeschlossen |
| `failed` | Reserviert; der Worker setzt diesen Status derzeit nicht. Ein fehlgeschlagener Versuch, der wiederholt wird, erhält wieder den Status `pending` |
| `buried` | Alle Wiederholungsversuche ausgeschöpft |
