Уявіть ситуацію: імпорт продуктів торкається одного й того ж запису сорок разів за хвилину. Подія ProductUpdated спрацьовує сорок разів, а слухач, який перебудовує пошуковий індекс, виконується сорок разів. Кожен запуск індексує стан, який наступний одразу перезаписує. Черга робить саме те, що їй сказали, але 97% роботи - марна трата ресурсів.
Що насправді потрібно: щоб серія однакових подій схлопнулася в один запуск слухача наприкінці серії, передавши лише останній актуальний стан.
Дебаунсинг для слухачів подій
Laravel 13.6 додав таке схлопування для черг завдань у вигляді debounceable queued jobs. Тепер Laravel 13.26 розширює той самий атрибут #[DebounceFor] на слухачі подій у черзі. Цей внесок зробив @stevebauman у
#61169, тож тепер event-driven код отримує цю поведінку без необхідності реструктуризації слухачів у завдання, що диспатчаться вручну.
Раніше цю прогалину заповнювали пакети спільноти, зокрема Laravel Debounce. Власна відповідь фреймворку почалася з завдань, а тепер поширилася й на слухачі.
Як додати дебаунсинг до слухача
Достатньо додати атрибут до слухача в черзі й вказати вікно дебаунсингу в секундах:
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 для продукту з ID 42 у межах 30-секундного вікна призведе до одного виклику handle(), який виконається для останньої події серії. Події для продукту 43 дебаунсяться незалежно, оскільки метод debounceId() створює окреме вікно для кожного продукту.
Якщо debounceId() не визначений, усі диспатчі слухача ділять одне вікно - саме така поведінка потрібна для слухачів на кшталт "перебудувати карту сайту", які не залежать від конкретного ресурсу. ID також може бути звичайною властивістю $debounceId, коли він не залежить від події.
Принцип роботи дебаунсингу
При дебаунсингу перемагає останній диспатч. Кожен диспатч ставить слухача в чергу із затримкою, що дорівнює вікну дебаунсингу, і записує токен власника в кеш з ключем, що складається з класу слухача та ID дебаунсингу. Новіший диспатч перезаписує токен, і коли стара копія з черги нарешті виконується, вона бачить, що більше не володіє токеном, і відкидає себе.
Один нюанс випливає з дизайну на основі затримки: тихий ресурс все одно чекає повне вікно. Тому одна подія для неактивного продукту виконається через 30 секунд, а не негайно.
maxWait і проблема голодування
Чистий дебаунсинг має режим відмови: потік подій, який ніколи не зупиняється на 30 секунд, відкладає слухача назавжди. Саме для цього існує параметр maxWait.
Із #[DebounceFor(30, maxWait: 120)] коли диспатчі просувають вікно протягом 120 секунд, наступний диспатч виконається без затримки замість чергового продовження відкладення. Активний імпорт все одно отримає схлопнення записів - приблизно один запуск індексації на дві хвилини замість сорока запусків або нуля.
Додаткові налаштування
Два регулятори взаємодіють із затримкою:
- Явна затримка, встановлена на слухачі, має пріоритет над тією, що походить від дебаунсингу
- Слухач може перевизначити сховище кешу, яке керує токенами власності, визначивши метод
debounceVia() - корисно, коли ваш типовий кеш не є спільним для кожного сервера, що диспатчить події
Три важливі обмеження
Перед впровадженням варто знати три обмеження:
1. Несумісність із ShouldBeUnique
Не можна поєднувати з ShouldBeUnique. Ці дві функції тримають протилежні блокування: "перемагає перший" проти "перемагає останній". Їх комбінування тепер викидає LogicException при диспатчі замість мовчазного вибору переможця.
2. Область дії - слухач, а не подія
Інші слухачі на ProductUpdated виконуються для кожної події. Тільки слухач із атрибутом схлопується. Дебаунсинг - це властивість роботи, а не потоку подій.
3. Обробник бачить тригерну подію, тому перечитуйте стан
Об'єкт події, що переживає дебаунсинг, - це останній диспатчений, але до моменту виконання навіть він може застаріти. Саме тому приклад вище викликає $event->product->fresh(). Дебаунсований слухач має трактувати подію як вказівник на ресурс, а не як корисне навантаження.
Саме ця остання звичка робить дебаунсинг безпечним: якщо слухач перевиводить свій результат із бази даних, схлопування сорока запусків в один змінює вартість, а не результат.