
# Job

Job adalah unit kerja inti di Queuety. Kirim job dengan nama handler dan payload, lalu worker akan mengambil dan menjalankannya di latar belakang.

## Mengirim job

```php
use Queuety\Queuety;

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

`dispatch()` mengembalikan builder `PendingJob`. Anda dapat merangkai opsi sebelum job dikirim secara otomatis:

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

Untuk langsung mendapatkan ID job, panggil `->id()`:

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

## Menulis handler

Handler adalah kelas apa pun yang mengimplementasikan antarmuka `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',
        ];
    }
}
```

Metode `handle()` menerima payload job dan menjalankan pekerjaan. Metode `config()` mengembalikan nilai default opsional:

| Kunci | Tipe | Deskripsi |
|---|---|---|
| `queue` | `string` | Nama antrean default |
| `max_attempts` | `int` | Jumlah maksimum eksekusi, termasuk yang pertama, sebelum job dikubur |
| `backoff` | `string` | Strategi jeda percobaan ulang: `exponential`, `linear`, atau `fixed` |
| `rate_limit` | `array` | Batas laju sebagai `[max_executions, window_seconds]` |
| `concurrency_group` | `string` | Grup penerimaan worker bersama untuk handler yang mahal |
| `concurrency_limit` | `int` | Jumlah maksimum job pemrosesan serentak di dalam grup tersebut |
| `cost_units` | `int` | Biaya eksekusi relatif yang digunakan oleh anggaran antrean atau penyedia berbobot, anggaran alur kerja, dan heuristik penerimaan |

Kunci sumber daya ini merupakan bagian dari model [Manajemen Sumber Daya](https://queuety.inline0.dev/id/docs/features/resource-management) Queuety yang lebih luas, yang menyatukan metadata job, anggaran alur kerja, dan penerimaan worker.

Daftarkan handler:

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

Atau gunakan atribut [PHP](https://queuety.inline0.dev/id/docs/features/attributes) untuk pendaftaran otomatis.

## Prioritas

Job memiliki empat tingkat prioritas. Worker mengambil job berprioritas lebih tinggi terlebih dahulu.

```php
use Queuety\Enums\Priority;

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

| Tingkat | Nilai | Deskripsi |
|---|---|---|
| `Priority::Low` | `0` | Default. Pekerjaan latar belakang yang dapat menunggu. |
| `Priority::Normal` | `1` | Prioritas standar. |
| `Priority::High` | `2` | Diproses sebelum Low dan Normal. |
| `Priority::Urgent` | `3` | Diproses lebih dahulu. Gunakan seperlunya. |

## Penundaan

Tunda job agar hanya tersedia setelah jumlah detik yang ditentukan:

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

Job langsung dimasukkan, tetapi worker tidak akan mengambilnya sampai waktu penundaan berlalu.

## Job unik

Cegah pengiriman duplikat untuk handler dan payload yang sama. Jika sudah ada job tertunda atau sedang diproses dengan handler dan payload yang sama, ID job yang ada akan dikembalikan alih-alih membuat job baru.

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

## Dependensi job

Sebuah job dapat bergantung pada job lain. Worker tidak akan mengambilnya sampai dependensinya selesai.

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

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

## Pengiriman batch

Kirim beberapa job dalam satu INSERT multi-baris untuk throughput maksimum:

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

Setiap item dapat menyertakan kunci opsional: `queue`, `priority`, `delay`, `max_attempts`.

## Percobaan ulang dan jeda

Saat job memunculkan pengecualian, Queuety mengembalikannya ke `pending` dengan `available_at` tertunda hingga total dijalankan sebanyak `max_attempts` kali (eksekusi pertama ikut dihitung, jadi `max_attempts( 3 )` berarti hingga dua kali percobaan ulang). Penundaan sebelum setiap percobaan ulang ditentukan oleh strategi jeda, dengan `attempt` sebagai nomor eksekusi yang baru saja gagal:

| Strategi | Rumus penundaan | Contoh (setelah eksekusi 1, 2, 3) |
|---|---|---|
| `exponential` | `2^attempt * 30` detik, dibatasi hingga 3600 | 60s, 120s, 240s |
| `linear` | `attempt * 60` detik | 60s, 120s, 180s |
| `fixed` | `60` detik | 60s, 60s, 60s |

Tetapkan default global di `wp-config.php`:

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

Atau per handler di `config()`:

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

Atau per pengiriman:

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

## Pekerjaan yang dikarantina

Setelah semua percobaan ulang habis, job ditandai sebagai **dikubur**. Job yang dikubur tetap berada di database untuk diperiksa, tetapi tidak diproses. Anda dapat mencoba ulang secara manual. Percobaan ulang manual membuat job `pending` kembali tanpa mengatur ulang jumlah percobaannya, sehingga job yang dikubur dan dicoba ulang akan mendapat satu kali eksekusi lagi dan dikubur kembali jika eksekusi itu gagal:

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

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

Atau melalui CLI:

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

## Batas waktu

Queuety mengaktifkan `pcntl_alarm()` selama waktu eksekusi maksimum dan dirancang untuk menggagalkan percobaan dengan `TimeoutException` jika batas waktu terlampaui. Batas waktu tersebut saat ini belum diterapkan ([#10](https://github.com/inline0/queuety/issues/10)): worker tidak mengaktifkan sinyal asinkron, sehingga handler yang berjalan lama tetap berjalan hingga selesai, sedangkan handler yang terblokir di `sleep()` terhenti saat alarm berbunyi lalu ditandai selesai. Konfigurasikan batas waktu secara global:

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

Job yang terjebak dalam status `processing` lebih lama dari `QUEUETY_STALE_TIMEOUT` detik akan dipulihkan secara otomatis:

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

Pulihkan job usang secara manual:

```bash
wp queuety recover
```

## Antrean

Job dikirim ke antrean bernama. Setiap worker memproses satu antrean atau daftar antrean berurutan (lihat [Worker](https://queuety.inline0.dev/id/docs/features/workers#multi-queue-priorities)):

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

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

Antrean default adalah `default`. Anda dapat menjalankan proses worker terpisah untuk setiap antrean guna mengisolasi beban kerja.

## Pembatasan laju

Batasi seberapa sering handler dapat dijalankan dalam jangka waktu tertentu:

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

Lihat [Pembatasan Laju](https://queuety.inline0.dev/id/docs/features/rate-limiting) untuk detailnya.

## Kelas job yang dapat di-dispatch

Queuety v0.6.0 memperkenalkan API job berbasis kelas. Alih-alih memisahkan nama handler, payload, dan kelas handler, Anda dapat menentukan satu kelas yang merangkum semuanya: payload (sebagai properti publik bertipe), logika eksekusi, dan middleware opsional.

### Antarmuka `Contracts\Job`

```php
use Queuety\Contracts\Job;

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

Kelas `Job` menyimpan payload sebagai properti publik dan menjalankan logikanya sendiri di `handle()`. Metode `config()` terpisah atau pendaftaran handler tidak diperlukan.

### Trait `Dispatchable`

Tambahkan trait `Dispatchable` untuk mendapatkan metode statis `dispatch()` pada kelas job Anda:

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

Kirim job dengan memberikan argumen konstruktor:

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

Trait `Dispatchable` memanggil `Queuety::dispatch()` secara internal, sehingga semua opsi `PendingJob` (queue, priority, delay, max_attempts, unique, after) berfungsi sama persis.

### Cara kerja serialisasi

Saat Anda mengirim instance `Job`, Queuety`JobSerializer` mengekstrak nama kelas berkualifikasi penuh sebagai handler dan semua properti publik sebagai payload. Di sisi worker, serializer membangun kembali instance dengan mencocokkan kunci payload dengan nama parameter konstruktor. Enum backed diserialisasi menjadi nilai dasarnya dan dipulihkan secara otomatis.

### Properti pekerjaan

Kelas job dapat mendeklarasikan properti publik untuk mengendalikan perilaku percobaan ulang dan batas waktu. Worker membacanya melalui refleksi dan menerapkannya secara otomatis.

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

| Properti | Tipe | Deskripsi |
|---|---|---|
| `$tries` | `int` | Jumlah maksimum percobaan sebelum job dikubur. Menggantikan nilai `max_attempts` saat pengiriman. |
| `$timeout` | `int` | Waktu eksekusi maksimum dalam detik. Menggantikan konstanta `QUEUETY_MAX_EXECUTION_TIME` global. Memerlukan `pcntl` dan belum diterapkan (lihat [Batas Waktu](https://queuety.inline0.dev/id/docs/jobs#timeout)). |
| `$backoff` | `array` | Array nilai penundaan dalam detik untuk setiap percobaan ulang. Nilai terakhir digunakan kembali untuk percobaan berikutnya. |
| `$concurrency_group` | `string` | Grup penerimaan worker bersama untuk job yang mahal. |
| `$concurrency_limit` | `int` | Jumlah maksimum job `processing` yang berjalan serentak dalam grup tersebut. Diabaikan tanpa `$concurrency_group`. |
| `$cost_units` | `int` | Biaya eksekusi relatif yang digunakan oleh anggaran alur kerja dan penerimaan worker. |

Array `$backoff` menyediakan penundaan percobaan ulang yang makin lama. Misalnya, `[10, 30, 60, 120]` berarti percobaan ulang pertama menunggu 10 detik, kedua 30 detik, ketiga 60 detik, dan percobaan selanjutnya menunggu 120 detik.

Properti terkait sumber daya berguna ketika kelas job mewakili profil biaya yang berbeda. Penyegaran cache yang cepat dan pemanggilan penyedia berskala besar sama-sama dapat disebut "satu job", tetapi belum tentu semestinya dihitung dengan cara yang sama ketika anggaran alur kerja atau keputusan penerimaan worker berupaya menjaga proses tetap dalam batas aman.

Untuk model lengkap, termasuk anggaran alur kerja dan penerimaan di sisi worker, lihat [Manajemen Sumber Daya](https://queuety.inline0.dev/id/docs/features/resource-management).

### Hook kegagalan

Saat job dikubur setelah semua percobaan ulang habis, Queuety memanggil metode `failed()` pada instance job jika metode tersebut didefinisikan. Gunakan untuk pembersihan, pemberian peringatan, atau pencatatan log.

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

Metode `failed()` menerima pengecualian yang menyebabkan kegagalan terakhir. Instance job dideserialisasi dengan payload aslinya, sehingga semua properti publik tersedia.

### Pengiriman bersyarat

Trait `Dispatchable` menyediakan `dispatch_if()` dan `dispatch_unless()` untuk pengiriman bersyarat. Keduanya menerima kondisi boolean sebagai argumen pertama, diikuti argumen konstruktor job.

```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()` mengirim job jika kondisi bernilai `true` dan mengembalikan `PendingJob`. Metode ini mengembalikan `null` jika kondisi bernilai `false`.

`dispatch_unless()` mengirim job jika kondisi bernilai `false` dan mengembalikan `PendingJob`. Metode ini mengembalikan `null` jika kondisi bernilai `true`.

Kedua metode mendukung perangkaian opsi `PendingJob` saat mengirim job:

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

### Pengiriman sinkron

Gunakan `Queuety::dispatch_sync()` untuk menjalankan job segera dalam proses saat ini tanpa menambahkannya ke antrean. Ini berguna untuk pengujian dan job yang harus dijalankan secara inline.

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

Metode `handle()` milik job dipanggil secara langsung. Tidak ada baris database yang dibuat, worker tidak dilibatkan, dan middleware tidak diterapkan.

### Perantaian job

Perantaian job menjalankan serangkaian job satu per satu. Setiap job dalam rantai bergantung pada keberhasilan penyelesaian job sebelumnya. Jika ada job yang gagal, job berikutnya dalam rantai tetap tertunda.

#### Menggunakan `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()` mengembalikan `ChainBuilder` dengan metode fluent untuk mengatur antrean dan handler kegagalan. Metode `dispatch()` membuat semua job dengan relasi `depends_on` berurutan dan mengembalikan ID job pertama.

#### Menggunakan `with_chain()`

Trait `Dispatchable` menyediakan metode `with_chain()`. Saat ini, metode tersebut setara dengan `Queuety::chain( $jobs )`: kelas tempat metode dipanggil tidak ditambahkan ke rantai ([#13](https://github.com/inline0/queuety/issues/13)), jadi sertakan juga job pertama dalam array:

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

#### Penanganan kegagalan rantai

Daftarkan kelas handler kegagalan menggunakan `catch()`. Kelas handler harus memiliki metode `handle()`. Metode ini dipanggil saat job mana pun dalam rantai dikubur secara permanen. Worker mendukung `handle()`, `handle( Job $job )`, dan `handle( Job $job, \Throwable $exception )`.

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

### Menambahkan middleware

Kelas job dapat mendefinisikan metode `middleware()` untuk mengembalikan middleware yang membungkus eksekusi:

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

Lihat [Middleware](https://queuety.inline0.dev/id/docs/features/middleware) untuk detail tentang middleware bawaan dan cara menulis middleware khusus.

### Perbandingan dengan job berbasis handler

| | Berbasis handler (v0.1+) | Berbasis kelas (v0.6+) |
|---|---|---|
| Dispatch | `Queuety::dispatch( 'send_email', $payload )` | `SendEmailJob::dispatch( $to, $subject )` |
| Handler | Kelas terpisah yang mengimplementasikan `Handler` | Kelas yang sama mengimplementasikan `Contracts\Job` |
| Payload | `array $payload` tanpa tipe | Properti publik bertipe |
| Registrasi | `Queuety::register()` atau `#[QueuetyHandler]` | Tidak diperlukan (menggunakan FQCN) |
| Middleware | Tidak tersedia | Metode `middleware()` opsional |
| Konfigurasi | Metode `config()` | Properti job + rangkaian `PendingJob` |

Kedua pendekatan didukung sepenuhnya. Job berbasis handler yang sudah ada tetap berfungsi tanpa perubahan. Anda dapat mencampur kedua gaya dalam proyek yang sama.

## Siklus hidup job

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

| Status | Deskripsi |
|---|---|
| `pending` | Menunggu untuk diklaim oleh worker |
| `processing` | Sedang dijalankan |
| `completed` | Berhasil diselesaikan |
| `failed` | Dicadangkan; worker tidak menetapkannya saat ini. Percobaan yang gagal dan akan dicoba ulang kembali ke `pending` |
| `buried` | Semua percobaan ulang telah habis |
