Queue
PandaBear mengirim pekerjaan ke queue pada tiga area utama: export besar, import besar, dan outbound integration delivery. Ada dua hal tambahan yang juga melewati queue walaupun bukan job PandaBear secara langsung: seluruh realtime notification karena event ShouldBroadcast di-queue oleh Laravel, dan Email Code sign-in karena notification tersebut mengimplementasikan ShouldQueue.
Gunakan halaman ini saat menyiapkan worker, ketika export tidak pernah selesai, atau ketika toast realtime bekerja di lokal tetapi tidak di production.
Contoh minimal yang berfungsi
# .env
QUEUE_CONNECTION=database2
php artisan make:queue-table && php artisan migrate # hanya jika application belum memiliki jobs table
php artisan queue:work --queue=default --timeout=900 --max-time=36002
Pada setiap deploy:
php artisan queue:restartJangan memberikan --tries secara global pada worker jika tidak diperlukan. Setiap PandaBear job memiliki $tries sendiri berdasarkan karakteristik job tersebut. Worker-level --tries seharusnya menjadi fallback untuk job yang memang tidak menentukan nilainya.
Pekerjaan apa saja yang masuk queue
| Pekerjaan | Job | Masuk queue ketika |
|---|---|---|
| Export | PandaPanel\Jobs\RunPanelExport | jumlah record melebihi Exporter::queueAfter() |
| Import | PandaPanel\Jobs\RunPanelImport | jumlah row melebihi Importer::queueAfter() |
| Integration delivery | PandaPanel\Jobs\SendPanelIntegration | trigger merupakan jenis after_* |
| Broadcast notification | Laravel BroadcastEvent yang membungkus PanelNotificationSent | selalu, karena event menggunakan ShouldBroadcast |
| Email Code login | PandaPanel\Notifications\TwoFactorCode | selalu, karena menggunakan ShouldQueue |
Bagian lain tetap synchronous:
- Table rendering;
- form submission;
- authorization;
- navigation.
Tidak ada bagian dari Page render yang harus menunggu queue worker.
Export dan import tidak selalu di-queue
if ($exporter::queueAfter() >= 0 && $count > $exporter::queueAfter()) {
RunPanelExport::dispatch(/* … */);
return;
}2
3
4
5
| Class | Method | Default |
|---|---|---|
PandaPanel\Actions\Exports\Exporter | static queueAfter(): int | 2000 record |
PandaPanel\Actions\Imports\Importer | static queueAfter(): int | 500 row |
Contoh selalu queue:
use PandaPanel\Actions\Exports\Exporter;
final class UserExporter extends Exporter
{
public static function queueAfter(): int
{
return 0;
}
}2
3
4
5
6
7
8
9
Return -1 untuk tidak pernah queue. Guard >= 0 membuat branch queue dilewati dan pekerjaan dijalankan di request.
Threshold berupa angka, bukan boolean, karena small export sering lebih nyaman dijalankan langsung daripada membuat user menunggu background job, sedangkan export besar di request berisiko timeout.
Jumlah record/row dihitung sebelum pekerjaan mulai sehingga keputusan queue berdasarkan ukuran sebenarnya.
Job secara detail
RunPanelExport
use PandaPanel\Jobs\RunPanelExport;
RunPanelExport::dispatch(
$exporter, // class-string<Exporter>
$resource, // class-string<Resource>
$columns, // list<string>
$format, // PandaPanel\Actions\Enums\SpreadsheetFormat
$owner, // int|string — auth identifier user
$tableState, // array<string, mixed> — state query Table
$keys, // list<int|string>|null — selection eksplisit atau seluruh list
$panelId, // string
);2
3
4
5
6
7
8
9
10
11
12
| Property | Value | Alasan |
|---|---|---|
$tries | 3 | export hanya membaca record dan menulis file; output setengah jadi dapat diganti attempt berikutnya |
backoff() | [10, 60] | failure yang layak diretry biasanya membutuhkan jeda; tiga retry satu detik apart hanya menabrak outage yang sama |
Semua payload job adalah scalar atau array biasa. Eloquent Builder tidak dikirim ke queue karena Builder dapat membawa Closure dan tidak serializable.
Job membawa deskripsi query, bukan query object:
- Resource class;
- Table state;
- explicit selection.
Di worker, query dibangun ulang melalui TableQuery yang sama dengan List Page. Dengan demikian hasil export mengikuti:
- filter;
- search;
- selected records;
- scope Resource.
Saat sukses, owner menerima persistent notification dengan action Download.
Pada final failure:
failed(?Throwable $exception): voidmengirim notification danger berisi exception message. User yang sudah diberi tahu bahwa file sedang diproses tidak dibiarkan menunggu tanpa hasil.
RunPanelImport
use PandaPanel\Jobs\RunPanelImport;
RunPanelImport::dispatch(
$importer, // class-string<Importer>
$path, // file upload yang sudah berada pada disk
$mapping, // array<string, int>
$owner, // int|string
$panelId, // string
);2
3
4
5
6
7
8
9
| Property | Value | Alasan |
|---|---|---|
$tries | 1 | import menulis row; failure di tengah dapat berarti sebagian row sudah tersimpan dan retry generic dapat membuat duplicate/partial write kedua |
Import tidak otomatis di-retry karena framework tidak dapat mengetahui secara umum row mana yang sudah masuk sebelum failure.
Behavior yang dipilih adalah:
import gagal
↓
report apa yang sudah/ gagal
↓
user memperbaiki dan upload ulang2
3
4
5
Baik handle() maupun failed() menghapus uploaded file. File tersebut hanya alat sementara untuk proses import, bukan record yang dimaksudkan disimpan permanen.
SendPanelIntegration
use PandaPanel\Jobs\SendPanelIntegration;
SendPanelIntegration::dispatch(
$integrationId, // int
$payload, // array<string, mixed>
$timeout, // int — HTTP timeout outbound request
$deliveryId, // string|null
);2
3
4
5
6
7
8
| Property | Value |
|---|---|
$tries | 3 |
backoff() | [10, 60] |
Hanya trigger after_* yang di-queue.
Trigger before_* harus berjalan inline karena ia mendeskripsikan state record sebelum write. Jika menunggu worker, state tersebut sudah berubah atau hilang.
Payload integration dikirim sebagai array, bukan serialized Eloquent Model. Ini penting terutama untuk after_delete, karena record memang sudah tidak ada ketika worker berjalan. Menggunakan SerializesModels pada deleted record dapat membuat worker mencoba reload model yang sudah hilang.
Argument $timeout pada constructor adalah timeout HTTP outbound request, bukan job timeout.
Panel context di dalam job
Resource scope, Table, dan URL Panel dibaca melalui current Panel. Queued job berjalan di luar HTTP request, sehingga tidak ada Panel context secara otomatis.
Job file bawaan membawa panelId dan mengaturnya sebelum bekerja:
use PandaPanel\Core\PanelManager;
public function handle(PanelManager $manager): void
{
$panel = $manager->get($this->panelId);
$manager->setCurrentPanel($panel);
// Resource::query() dan $panel->routeName() sekarang membaca Panel yang benar
}2
3
4
5
6
7
8
9
10
Custom job application yang menggunakan Resource PandaBear atau membangun Panel URL harus melakukan hal yang sama.
Tanpa current Panel, Resource::query() tidak memiliki konteks Panel yang benar.
Tenant context
Tenant-scoped work harus memasuki Tenant secara eksplisit:
use PandaPanel\Tenancy\Tenancy;
Tenancy::for($tenant, fn () => InvoiceResource::query()->count());2
3
Resource yang membutuhkan Tenant tetapi dipanggil tanpa Tenant sengaja melempar exception daripada menjalankan query tanpa scope. Query unscoped dapat membocorkan seluruh Tenant dan terlihat “berhasil”.
Me-resolve user di worker
use Illuminate\Support\Facades\Auth;
$user = Auth::getProvider()->retrieveById($this->owner);
if ($user === null) {
return;
}2
3
4
5
6
7
Job membawa scalar auth identifier, bukan model.
Keuntungannya:
- payload tetap data biasa;
- custom auth provider tetap dihormati;
- user yang dihapus antara dispatch dan execution menghasilkan
null, bukan deserialize error.
Dalam kondisi user sudah tidak ada, job cukup berhenti.
Dua hop queue, bukan satu
Notification yang dikirim dari sebuah queued job dapat membutuhkan dua lapis queue work:
| Hop | Yang di-queue | Dikontrol oleh |
|---|---|---|
| Job utama | class ShouldQueue | queue.default, $connection, $queue milik job |
| Broadcast | Laravel BroadcastEvent yang membungkus event | queue.default kecuali event menentukan queue lain |
PanelNotificationSent dan PandaPanel\Broadcasting\PanelNotification menggunakan ShouldBroadcast, bukan ShouldBroadcastNow. Jadi event broadcast membuat queued job baru.
Dengan:
QUEUE_CONNECTION=synckedua hop berjalan langsung. Inilah alasan realtime notification sering terlihat sempurna di lokal lalu berhenti di production ketika queue benar-benar asynchronous.
Database notification tidak di-queue
PanelDatabaseNotification bukan ShouldQueue. Jadi ->persistent() menulis row database langsung di dalam job yang memanggil send().
Artinya persistent notification dapat sudah tersimpan walaupun broadcast job belum pernah dikerjakan worker.
Konfigurasi worker
Tidak ada konfigurasi queue khusus PandaBear, tetapi beberapa setting Laravel penting terhadap karakter job-nya.
Timeout
RunPanelExport dan RunPanelImport tidak menentukan $timeout, sehingga worker timeout berlaku. Default Laravel worker sering terlalu pendek untuk export besar.
php artisan queue:work --timeout=900Chunk default:
Exporter::chunkSize()→500;Importer::chunkSize()→200.
Jangan langsung menaikkan chunk tanpa memperhatikan memory. Biasanya lebih aman memperpanjang worker timeout daripada membuat chunk sangat besar.
Restart worker
php artisan queue:restartHarus menjadi bagian akhir deploy. Worker memegang code yang digunakan saat boot, termasuk manifest Panel yang sudah pernah dibaca.
Worker lama dapat menjalankan release sebelumnya terhadap database release baru.
Contoh Supervisor
[program:panel-worker]
command=php /var/www/current/artisan queue:work --queue=default --timeout=900 --max-time=3600
directory=/var/www/current
autostart=true
autorestart=true
stopwaitsecs=3600
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/panel-worker.log2
3
4
5
6
7
8
9
10
stopwaitsecs sebaiknya lebih panjang dari job terlama agar graceful restart tidak membunuh export yang masih berjalan.
--max-time merecycle process secara berkala, cara sederhana membatasi akumulasi memory pada long-running PHP worker.
Queue khusus untuk export
Export lambat dan integration delivery bisa sensitif terhadap latency. Memisah queue mencegah integration menunggu di belakang export besar.
Contoh manual dispatch:
use PandaPanel\Jobs\RunPanelExport;
RunPanelExport::dispatch(/* … */)->onQueue('exports');2
3
Workers:
php artisan queue:work --queue=integrations,default
php artisan queue:work --queue=exports2
Built-in action PandaBear dispatch ke default queue. Jika ingin routing lain, gunakan custom Action/job dispatch atau atur queue pada connection. Job bawaan tidak mendeklarasikan $queue sendiri.
Failure handling
php artisan queue:failed
php artisan queue:retry all
php artisan queue:flush2
3
| Job | Final failure |
|---|---|
RunPanelExport | failed() mengirim persistent notification ke owner berisi exception message |
RunPanelImport | failed() menghapus upload lalu mengirim notification berisi exception message |
SendPanelIntegration | tidak ada user-facing message; attempt disimpan di delivery history |
Exception message digunakan agar user mendapatkan informasi yang dapat ditindaklanjuti seperti:
unsupported file format
column count mismatch on row 122
failed() berjalan setelah seluruh attempt habis, bukan setiap retry.
Hal yang perlu diperhatikan
- Tidak ada worker = tidak ada realtime toast. Persistent notification masih bisa tersimpan dan bell count benar pada navigasi berikutnya, tetapi broadcast event tetap tertahan di queue.
QUEUE_CONNECTION=syncdi production membuat queued export berjalan di request yang memicunya dan berisiko timeout.- Worker yang tidak direstart menjalankan release lama. Selalu
queue:restartsetelah deploy. SendPanelIntegration::$timeoutadalah HTTP timeout, bukan worker/job timeout.- User yang sudah dihapus membuat job berhenti secara senyap setelah
retrieveById()menghasilkannull. - Queued work tidak otomatis memiliki Panel maupun Tenant context. Set keduanya secara eksplisit.
->persistent()adalah mekanisme yang bertahan walaupun broadcast/worker bermasalah. Broadcast-only notification dapat hilang jika browser tidak sedang terhubung.