
# Opravila

Opravila so osnovna enota dela v Queuety. Pošljite opravilo v izvajanje z imenom obravnavalnika in vsebino, nato pa ga delavec prevzame in izvede v ozadju.

## Pošiljanje opravil v izvajanje

```php
use Queuety\Queuety;

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

`dispatch()` vrne gradnik `PendingJob`. Pred samodejnim pošiljanjem lahko verižite možnosti:

```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 );
```

Če želite takoj dobiti ID opravila, pokličite `->id()`:

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

## Pisanje obravnavalnikov

Obravnavalnik je kateri koli razred, ki implementira vmesnik `Queuety\Handler`:

```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',
        ];
    }
}
```

Metoda `handle()` prejme vsebino opravila in izvede delo. Metoda `config()` vrne neobvezne privzete vrednosti:

| Ključ | Vrsta | Opis |
|---|---|---|
| `queue` | `string` | Privzeto ime čakalne vrste |
| `max_attempts` | `int` | Največje število zagonov, vključno s prvim, preden je opravilo odstranjeno iz čakalne vrste |
| `backoff` | `string` | Strategija čakanja pred ponovnim poskusom: `exponential`, `linear` ali `fixed` |
| `rate_limit` | `array` | Omejitev hitrosti kot `[max_executions, window_seconds]` |
| `concurrency_group` | `string` | Skupina za sprejemanje delavcev za drage obravnavalnike |
| `concurrency_limit` | `int` | Največje število sočasno obdelovanih opravil v tej skupini |
| `cost_units` | `int` | Relativni strošek izvajanja, ki se uporablja pri uteženih proračunih čakalnih vrst ali ponudnikov, proračunih potekov dela in hevristikah sprejemanja |

Ti ključi virov so del širšega modela upravljanja virov Queuety, v katerem skupaj delujejo metapodatki opravil, proračuni potekov dela in sprejemanje delavcev: [Upravljanje virov](https://queuety.inline0.dev/sl/docs/features/resource-management).

Registrirajte obravnavalnik:

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

Ali pa za samodejno registracijo uporabite atribute [PHP](https://queuety.inline0.dev/sl/docs/features/attributes).

## Prednostna raven

Opravila imajo štiri prednostne ravni. Delavci najprej prevzamejo opravila z višjo prednostno ravnjo.

```php
use Queuety\Enums\Priority;

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

| Raven | Vrednost | Opis |
|---|---|---|
| `Priority::Low` | `0` | Privzeto. Delo v ozadju, ki lahko počaka. |
| `Priority::Normal` | `1` | Standardna prednostna raven. |
| `Priority::High` | `2` | Obdela se pred nizko in običajno prednostjo. |
| `Priority::Urgent` | `3` | Obdela se najprej. Uporabljajte zmerno. |

## Zakasnitev

Zakasnite opravilo, da bo na voljo šele po določenem številu sekund:

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

Opravilo se vstavi takoj, vendar ga delavci ne bodo prevzeli, dokler se zakasnitev ne izteče.

## Enolična opravila

Preprečite podvojeno pošiljanje za isti obravnavalnik in vsebino. Če že obstaja čakajoče ali obdelovano opravilo z istim obravnavalnikom in vsebino, se namesto ustvarjanja novega vrne ID obstoječega opravila.

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

## Odvisnosti opravil

Opravilo je lahko odvisno od drugega opravila. Delavec ga ne bo prevzel, dokler odvisno opravilo ni dokončano.

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

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

## Skupinsko pošiljanje

Pošljite več opravil z enim večvrstičnim INSERT za največjo prepustnost:

```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' ] ],
] );
```

Vsak element lahko vključuje neobvezne ključe: `queue`, `priority`, `delay`, `max_attempts`.

## Ponovni poskusi in eksponentno čakanje

Ko opravilo sproži izjemo, ga Queuety vrne v `pending` z zakasnjenim `available_at`, dokler se skupaj ne izvede `max_attempts`-krat (šteje se tudi prvi poskus, zato `max_attempts( 3 )` pomeni največ dva ponovna poskusa). Zakasnitev pred vsakim ponovnim poskusom določa strategija čakanja, pri čemer je `attempt` številka pravkar neuspelega poskusa:

| Strategija | Formula zakasnitve | Primer (po poskusu 1, 2, 3) |
|---|---|---|
| `exponential` | `2^attempt * 30` sekund, največ 3600 | 60s, 120s, 240s |
| `linear` | `attempt * 60` sekund | 60s, 120s, 180s |
| `fixed` | `60` sekund | 60s, 60s, 60s |

Nastavite globalno privzeto vrednost v `wp-config.php`:

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

Ali pa za posamezen obravnavalnik v `config()`:

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

Ali pa za posamezno pošiljanje:

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

## Arhivirana opravila

Ko so vsi poskusi ponovnega zagona izčrpani, je opravilo označeno kot **zakopano**. Zakopana opravila ostanejo v zbirki podatkov za pregled, vendar se ne obdelujejo. Ročno jih lahko znova poskusite izvesti. Pri ročnem ponovnem poskusu se opravilo znova `pending`, ne da bi se ponastavilo število poskusov, zato se zakopano opravilo izvede še enkrat in je ob neuspehu tega poskusa znova zakopano:

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

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

Ali prek CLI:

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

## Časovna omejitev

Queuety nastavi `pcntl_alarm()` za najdaljši čas izvajanja in naj bi ob prekoračitvi sprožil neuspeh poskusa z `TimeoutException`. Ta časovna omejitev trenutno ni uveljavljena ([#10](https://github.com/inline0/queuety/issues/10)): delavec ne omogoči asinhronih signalov, zato se dolgotrajni obravnavalnik izvede do konca, obravnavalnik, blokiran v `sleep()`, pa se prekine, ko se sproži alarm, in je nato označen kot dokončan. Globalno nastavite časovno omejitev:

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

Opravila, ki so v stanju `processing` dlje kot `QUEUETY_STALE_TIMEOUT` sekund, se samodejno obnovijo:

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

Zastarela opravila obnovite ročno:

```bash
wp queuety recover
```

## Čakalne vrste

Opravila se pošiljajo v poimenovane čakalne vrste. Vsak delavec obdeluje eno čakalno vrsto ali urejen seznam čakalnih vrst (glejte [Delavci](https://queuety.inline0.dev/sl/docs/features/workers#multi-queue-priorities)):

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

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

Privzeta čakalna vrsta je `default`. Za ločevanje delovnih obremenitev lahko za vsako čakalno vrsto zaženete ločen proces delavca.

## Omejevanje hitrosti

Omejite, kako pogosto se lahko obravnavalnik izvede v določenem časovnem obdobju:

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

Podrobnosti najdete v razdelku [Omejevanje hitrosti](https://queuety.inline0.dev/sl/docs/features/rate-limiting).

## Razredi opravil, ki jih je mogoče poslati

Queuety v0.6.0 uvaja opravila, temelječa na razredih, prek API. Namesto ločenega navajanja imena obravnavalnika, vsebine in razreda obravnavalnika določite en razred, ki združuje vse: vsebino (kot tipizirane javne lastnosti), logiko izvajanja in neobvezno vmesno programsko opremo.

### Vmesnik `Contracts\Job`

```php
use Queuety\Contracts\Job;

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

Razred `Job` hrani svojo vsebino kot javne lastnosti in izvaja lastno logiko v `handle()`. Ločena metoda `config()` ali registracija obravnavalnika nista potrebni.

### Trait `Dispatchable`

Razredu opravila dodajte trait `Dispatchable`, da pridobite statično metodo `dispatch()`:

```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 );
    }
}
```

Pošljite ga tako, da posredujete argumente konstruktorja:

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

Trait `Dispatchable` interno pokliče `Queuety::dispatch()`, zato vse možnosti `PendingJob` (queue, priority, delay, max_attempts, unique, after) delujejo enako.

### Kako deluje serializacija

Ko pošljete primerek `Job`, Queuety iz `JobSerializer` pridobi polno kvalificirano ime razreda kot obravnavalnik in vse javne lastnosti kot vsebino. Na strani delavca serializator obnovi primerek tako, da ključe vsebine poveže z imeni parametrov konstruktorja. Naštevanja s podprto vrednostjo se serializirajo v osnovno vrednost in samodejno obnovijo.

### Lastnosti opravil

Razredi opravil lahko določijo javne lastnosti za nadzor ponovnih poskusov in časovne omejitve. Delavec jih prebere z refleksijo in samodejno uporabi.

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

| Lastnost | Vrsta | Opis |
|---|---|---|
| `$tries` | `int` | Največje število poskusov, preden je opravilo zakopano. Preglasi vrednost `max_attempts`, določeno ob pošiljanju. |
| `$timeout` | `int` | Najdaljši čas izvajanja v sekundah. Preglasi globalno konstanto `QUEUETY_MAX_EXECUTION_TIME`. Zahteva `pcntl` in še ni uveljavljena (glejte [Časovna omejitev](https://queuety.inline0.dev/sl/docs/jobs#timeout)). |
| `$backoff` | `array` | Polje vrednosti zakasnitve v sekundah za vsak ponovni poskus. Zadnja vrednost se uporabi tudi za vse nadaljnje poskuse. |
| `$concurrency_group` | `string` | Skupina za sprejemanje opravil s skupnim delavcem pri zahtevnih opravilih. |
| `$concurrency_limit` | `int` | Največje število sočasnih opravil `processing` v tej skupini. Brez `$concurrency_group` se ne upošteva. |
| `$cost_units` | `int` | Relativni strošek izvajanja, ki ga uporabljajo proračuni delovnih tokov in sprejemanje opravil s strani delavcev. |

Polje `$backoff` določa naraščajoče zakasnitve med ponovnimi poskusi. Na primer, `[10, 30, 60, 120]` pomeni, da je treba pred prvim ponovnim poskusom počakati 10 sekund, pred drugim 30 sekund, pred tretjim 60 sekund, pred vsemi nadaljnjimi pa 120 sekund.

Lastnosti, povezane z viri, so uporabne, kadar razredi opravil predstavljajo različne stroškovne profile. Hitro osveževanje predpomnilnika in obsežen klic ponudnika sta lahko oba »eno opravilo«, vendar se ne bi smela nujno šteti enako, ko proračun delovnega toka ali odločitev o sprejemu opravila poskuša proces ohraniti znotraj varnih omejitev.

Celoten model, vključno s proračuni delovnih tokov in sprejemanjem opravil na strani delavca, je opisan v razdelku [Upravljanje virov](https://queuety.inline0.dev/sl/docs/features/resource-management).

### Kavelj ob napaki

Ko je opravilo zakopano po izčrpanju vseh poskusov, Queuety pokliče metodo `failed()` na primerku opravila, če je ta določena. Uporabite jo za čiščenje, opozarjanje ali beleženje.

```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()}" );
    }
}
```

Metoda `failed()` prejme izjemo, ki je povzročila končni neuspeh. Primerek opravila se deserializira z izvirno vsebino, zato so na voljo vse javne lastnosti.

### Pogojno pošiljanje

Trait `Dispatchable` omogoča `dispatch_if()` in `dispatch_unless()` za pogojno pošiljanje. Obe sprejmeta logični pogoj kot prvi argument, za njim pa argumente konstruktorja opravila.

```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()` pošlje opravilo, ko je pogoj `true`, in vrne `PendingJob`. Vrne `null`, ko je pogoj `false`.

`dispatch_unless()` pošlje opravilo, ko je pogoj `false`, in vrne `PendingJob`. Vrne `null`, ko je pogoj `true`.

Obe metodi podpirata veriženje možnosti `PendingJob` pri pošiljanju:

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

### Sinhrono pošiljanje

Uporabite `Queuety::dispatch_sync()` za takojšnje izvajanje opravila v trenutnem procesu, ne da bi ga dodali v čakalno vrsto. To je uporabno za testiranje in opravila, ki se morajo izvesti neposredno.

```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' ] );
```

Metoda `handle()` opravila se pokliče neposredno. Vrstica v zbirki podatkov se ne ustvari, delavec ni vključen, vmesna programska oprema pa se ne uporabi.

### Veriga opravil

Veriženje opravil izvede zaporedje opravil enega za drugim. Vsako opravilo v verigi je odvisno od uspešnega zaključka prejšnjega. Če katero koli opravilo spodleti, naslednja opravila v verigi ostanejo čakajoča.

#### Uporaba `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()` vrne `ChainBuilder` s tekočimi metodami za nastavitev čakalne vrste in obravnavalnika napak. Metoda `dispatch()` ustvari vsa opravila z zaporednimi povezavami `depends_on` in vrne ID prvega opravila.

#### Uporaba `with_chain()`

Trait `Dispatchable` ponuja metodo `with_chain()`. Trenutno je enakovredna metodi `Queuety::chain( $jobs )`: razred, na katerem je poklicana, ni dodan v verigo ([#13](https://github.com/inline0/queuety/issues/13)), zato v polje vključite tudi prvo opravilo:

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

#### Obravnava napak v verigi

Registrirajte razred obravnavalnika napak z uporabo `catch()`. Razred obravnavalnika mora imeti metodo `handle()`. Pokliče se, ko je katero koli opravilo v verigi trajno zakopano. Delavec podpira `handle()`, `handle( Job $job )` in `handle( Job $job, \Throwable $exception )`.

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

### Dodajanje vmesne programske opreme

Razredi opravil lahko določijo metodo `middleware()`, ki vrne vmesno programsko opremo za ovijanje izvajanja:

```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 ),
        ];
    }
}
```

Podrobnosti o vgrajeni vmesni programski opremi in pisanju lastne najdete v razdelku [Vmesna programska oprema](https://queuety.inline0.dev/sl/docs/features/middleware).

### Primerjava opravil, ki temeljijo na obravnavalniku

| | Temelji na obravnavalniku (v0.1+) | Temelji na razredu (v0.6+) |
|---|---|---|
| Pošiljanje | `Queuety::dispatch( 'send_email', $payload )` | `SendEmailJob::dispatch( $to, $subject )` |
| Obravnavalnik | Ločen razred, ki implementira `Handler` | Isti razred implementira `Contracts\Job` |
| Vsebina | Netipiziran `array $payload` | Tipizirane javne lastnosti |
| Registracija | `Queuety::register()` ali `#[QueuetyHandler]` | Ni potrebna (uporablja FQCN) |
| Vmesna programska oprema | Ni na voljo | Neobvezna metoda `middleware()` |
| Konfiguracija | Metoda `config()` | Lastnosti opravila + veriga `PendingJob` |

Oba pristopa sta v celoti podprta. Obstoječa opravila, ki temeljijo na obravnavalniku, še naprej delujejo brez sprememb. V istem projektu lahko kombinirate oba sloga.

## Življenjski cikel opravila

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

| Stanje | Opis |
|---|---|
| `pending` | Čaka, da ga prevzame delavec |
| `processing` | Trenutno se izvaja |
| `completed` | Uspešno dokončano |
| `failed` | Rezervirano; delavec ga trenutno ne nastavi. Neuspešen poskus, ki bo ponovljen, se vrne v `pending` |
| `buried` | Vsi poskusi ponovnega zagona so izčrpani |
