Під час імпорту товарів один і той самий запис може оновлюватися сорок разів на хвилину. Це провокує сорок запусків події ProductUpdated та стільки ж запусків listener, який перебудовує пошуковий індекс. Черга сумлінно виконує завдання, хоча 97% цієї роботи — марна трата ресурсів, адже кожне наступне оновлення просто перезаписує попередній стан. Оптимальне рішення — об'єднати серію ідентичних подій в один запуск listener із актуальними даними наприкінці циклу.
Laravel 13.6 представив механізм об'єднання для queued jobs у вигляді debounceable queued jobs. Тепер Laravel 13.26 розширює цю функціональність: завдяки внеску @stevebauman у #61169, атрибут #[DebounceFor] став доступним для queued event listeners. Це дозволяє оптимізувати подієво-орієнтований код без переписування listener у звичайні джоби, що викликаються вручну.
Раніше цю прогалину заповнювали сторонні пакети, як-от Laravel Debounce, але тепер фреймворк пропонує власне нативне рішення.
# Як працює Debouncing для Listener
Просто додайте атрибут до queued listener та вкажіть час затримки (debounce window) у секундах:
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
public function debounceId(ProductUpdated $event): string
{
return (string) $event->product->getKey();
}
public function handle(ProductUpdated $event): void
{
ProductIndexer::index($event->product->fresh());
}
}
Тепер усі події ProductUpdated для товару №42 протягом 30-секундного вікна призведуть лише до одного виклику handle() для останньої події в серії. Події для товару №43 обробляються окремо, оскільки метод debounceId() створює унікальний ідентифікатор для кожного продукту. Якщо debounceId не вказано, усі виклики listener ділитимуть одне вікно затримки — це зручно для таких задач, як «перебудувати мапу сайту», що не залежать від конкретного ресурсу. Також можна використовувати звичайну властивість $debounceId, якщо вона статична.
Механізм працює за принципом «перемагає останній». Кожна нова подія додає listener у чергу з Delay, що дорівнює вікну затримки, і записує токен власника у кеш (ключ формується з назви класу та debounce ID). Новий виклик перезаписує токен. Коли старі копії в черзі нарешті доходять до виконання, вони бачать, що більше не володіють токеном, і завершуються. Важливо враховувати нюанс: навіть поодинока подія для неактивного товару все одно чекатиме повне вікно затримки (у прикладі — 30 секунд), перш ніж виконатися.
# maxWait та проблема «голодування»
Чистий debouncing має слабке місце: якщо потік подій не переривається довше за вказане вікно (наприклад, 30 секунд), виконання listener може відкладатися нескінченно. Для запобігання цьому існує параметр maxWait. З налаштуванням #[DebounceFor(30, maxWait: 120)], якщо події постійно зсувають вікно протягом 120 секунд, наступний виклик виконається без затримки. Таким чином, при тривалому імпорті оновлення індексу відбуватиметься приблизно раз на дві хвилини, замість сорока разів або жодного.
На затримку впливають два фактори: явний Delay, встановлений на рівні listener, має пріоритет над розрахованим debounce-часом. Також listener може перевизначити сховище кешу для токенів через метод debounceVia() — це корисно, якщо дефолтний кеш не є спільним для всіх серверів, що генерують події.
# Основні правила
Перед використанням варто знати про три обмеження:
- Ніякого
ShouldBeUnique. Ці функції мають протилежну логіку (перший виграв проти останній виграв). Поєднання їх призведе доLogicExceptionпід час відправки події. - Область дії — listener, а не подія. Інші обробники події
ProductUpdatedпрацюватимуть як зазвичай; об’єднується лише той listener, де вказано атрибут. Debouncing — це властивість конкретної роботи, а не всього потоку подій. - Обробник бачить тригерну подію, тому оновлюйте дані. Об’єкт події, що вижив після debouncing, — це останній відправлений екземпляр. Але навіть він може застаріти до моменту виконання. Саме тому в прикладі використовується
$event->product->fresh(). Сприймайте подію як вказівник на ресурс, а не як остаточне джерело даних.
Саме перечитування даних із бази робить debouncing безпечним: якщо listener заново отримує стан із БД, об’єднання сорока запусків в один змінює лише вартість операції, а не її результат.
# Що ще почитати
- Примітки до релізу Laravel 13.26, де також з’явилися read-through драйвер файлової системи та
Queue::forward(). - Debounceable queued jobs у Laravel 13.6 — оригінальне API для джоб.
- Queue::forward() у Laravel 13.26 — ще одне оновлення для черг у цьому релізі.
- Основи Laravel Jobs та Queues для розуміння бази роботи черг і воркерів.