Naar de inhoud
Recognized by Laravel Match je project Contact

Chained jobs en het FIFO probleem

Laravel maakt het ontzettend eenvoudig om complexe processen op te delen in een reeks jobs. Met Bus::chain() bepaal je welke jobs elkaar opvolgen en Laravel zorgt ervoor dat ze in de juiste volgorde worden uitgevoerd.

Maar wat gebeurt er als je niet één, maar tientallen queue workers hebt draaien? En wat als je queues ondertussen flink gevuld raken? Dan blijkt “deze job na die job” iets minder eenvoudig dan het in eerste instantie lijkt.

In dit artikel kijken we naar wat er achter de schermen gebeurt met chained jobs in Laravel, waar het onverwachte gedrag vandaan komt en hoe je hier op een slimme manier mee om kunt gaan.

Chained jobs en het FIFO probleem

De casus

Stel, je hebt een job voor het genereren van een voedingsschema voor één dag. Je wilt in één keer een schema genereren voor 14 dagen. Je wilt zoveel mogelijk variatie in het voedingsschema en wilt voorkomen dat één gerecht meerdere malen voorkomt in de tijdspanne van 14 dagen. Het is daarom belangrijk dat de jobs synchroon worden afgehandeld, zodat de job altijd betrouwbaar naar de schema’s van de voorgaande dagen kan kijken.

Job batching of job chaining

Als je maar één queue worker hebt, dan is dit gemakkelijk opgelost met een job batch. Ga je met meerdere queue workers werken, dan kom je er snel achter dat jobs in schijnbaar willekeurige volgorde worden afgehandeld. De jobs voor dag 1, 2 en 3 kunnen tegelijkertijd worden opgepakt door verschillende queue workers, waardoor race conditions ontstaan en de variatie in het voedingsschema niet meer gegarandeerd kan worden.

FIFO queue met batch afhandeling

Laravel heeft daarom iets slims bedacht: job chaining. Met job chaining garandeert Laravel dat een job in de reeks pas naar de queue wordt gepushed, wanneer zijn voorganger is afgehandeld. Het maakt dan dus niet uit hoeveel queue workers je hebt. Klinkt ideaal voor ons scenario, maar geheel zonder nadelen is deze aanpak niet.

First in first out

Met job chaining weten we zeker dat jobs in de juiste volgorde worden afgehandeld en we kunnen naar hartenlust extra queue workers opspinnen. Wanneer je een drukke queue hebt, zul je echter ervaren dat het een stuk langer duurt om alle 14 jobs af te handelen dan je misschien verwacht. Dat komt doordat de meeste queue systemen volgens het first in first out principe werken. In de praktijk betekent dit, dat elke job in de chain opnieuw achteraan moet sluiten. Een manier om jobs naar de voorkant van de queue te pushen is er niet.

Priority queues

We kunnen dus niet bepalen waar in de queue een job terechtkomt. Wat we wél kunnen doen, is onze queue worker vertellen in welke volgorde verschillende queues geprioriteerd moeten worden. We kunnen ook voor elke job in de chain bepalen naar welke queue deze gepushed moet worden.

Met deze tools kunnen we ons FIFO probleem grotendeels oplossen. Elke job in de chain pushen we naar een andere queue, steeds met een hogere prioriteit dan de voorgaande.

<?php

namespace App\Console\Commands;

use App\Jobs\GenerateMealPlanJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Str;

class GenerateMealPlansCommand extends Command
{
    protected $signature = 'meal-plans:generate';

    protected $description = 'Chain 14 jobs that generate a meal plan for each upcoming day';

    public function handle(): void
    {
        $chainId = (string) Str::uuid();

        $jobs = array_map(
            fn(int $day) => (new GenerateMealPlanJob($chainId, $day))->onQueue("meal-plans-{$day}"),
            range(1, 14),
        );

        Bus::chain($jobs)->dispatch();

        $this->info("Dispatched chain {$chainId} with 14 meal plan jobs.");
    }
}

De queue worker configureren we op zo’n manier, dat het prioriteit geeft aan het afhandelen van jobs in queue 14, dan 13, etc. Het resultaat daarvan is dat er altijd prioriteit wordt gegeven aan het afhandelen van een reeks. Er worden geen jobs uit queue 1 opgepakt, zolang er nog jobs in queue 2 staan enzovoorts.

php artisan queue:work --queue=meal-plans-14,meal-plans-13,meal-plans-12,meal-plans-11,meal-plans-10,meal-plans-9,meal-plans-8,meal-plans-7,meal-plans-6,meal-plans-5,meal-plans-4,meal-plans-3,meal-plans-2,meal-plans-1  

FIFO queue met chain afhandeling

Conclusie

Job chaining is een krachtige manier om ervoor te zorgen dat jobs in een vaste volgorde worden uitgevoerd, ook wanneer je meerdere queue workers gebruikt. Bij lange chains kan FIFO er echter voor zorgen dat elke volgende job onnodig lang moet wachten. Door gebruik te maken van priority queues kun je dit gedrag sturen en krijgen de volgende jobs in de chain voorrang. Zo combineer je de betrouwbaarheid van job chaining met de snelheid en schaalbaarheid van meerdere queue workers.

Over de auteur
Dennis Koster

Dennis Koster

Software Engineer bij Zig

Dennis Koster is Software Engineer bij Zig en bestuurslid van de Dutch Laravel Foundation.