notiziephp.it

Eseguire lo Scheduler di Laravel su più server

Traduzione da Laravel News
8 Ottobre 2026 Tempo di lettura: 5 minuti
Eseguire lo Scheduler di Laravel su più server

Un'applicazione Laravel che esegue schedule:run su più di un server esegue ogni attività pianificata una volta per server. Va bene per un'attività che svuota una cache locale. Tuttavia, per un'attività che invia un'email di report giornaliero non è l'ideale.

Laravel 13.35 aggiunge due modifiche per questa configurazione. Il nuovo metodo Schedule::alwaysOnOneServer() esegue ogni attività su un solo server, mentre Schedule::hasBeenInterruptedSince() consente a un comando pianificato di lunga durata di fermarsi quando un deploy esegue schedule:interrupt.

Usare onOneServer() per ogni attività

Laravel dispone di onOneServer() da molto tempo. Il primo server che avvia l'attività acquisisce un lock nella cache, e gli altri server la saltano per quella esecuzione:

Schedule::command('reports:send')->daily()->onOneServer();

Il problema è ricordarsi che ogni nuova attività abbia bisogno di quella chiamata, e una dimenticanza si nota solo quando compaiono i duplicati. Il workaround consisteva nell'avvolgere l'intera pianificazione in un gruppo:

Schedule::onOneServer()->group(function () {
Schedule::command('reports:send')->daily();
Schedule::command('invoices:generate')->monthly();
// every other task...
});

Jack Bayliss ha contribuito con Schedule::alwaysOnOneServer(), che lo attiva per l'intera pianificazione da un service provider:

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Schedule;

public function boot(): void
{
Schedule::alwaysOnOneServer();
}

Il flag viene applicato quando la pianificazione legge i propri eventi, quindi le attività definite più avanti in routes/console.php sono incluse, comprese quelle aggiunte dopo la chiamata. È disattivato per impostazione predefinita, e Schedule::alwaysOnOneServer(false) lo disattiva di nuovo.

Date un nome alle attività closure

Il metodo onOneServer() costruisce la propria chiave di lock a partire dal nome dell'attività. Un'attività basata su comando ne ha uno dalla sua riga di comando, ma un'attività closure ne ha uno solo se si chiama name(). Chiamare onOneServer() su una closure senza nome lancia un'eccezione, quindi alwaysOnOneServer() salta quelle closure. Esse continuano comunque a essere eseguite su ogni server:

// Runs on every server, even with alwaysOnOneServer()
Schedule::call(fn () => Report::sendDaily())->daily();

// Runs on one server
Schedule::call(fn () => Report::sendDaily())->name('send-daily-report')->daily();

Non c'è alcun avviso per una closure saltata, quindi verificate che le attività closure abbiano una chiamata a name() quando attivate il flag.

Una cache condivisa

Il lock risiede nella cache, quindi ogni server deve usare lo stesso cache store, come Redis, Memcached, DynamoDB o il database. Con lo store file o array, ogni server ha il proprio lock e tutti i server eseguono l'attività.

Per impostazione predefinita, lo scheduler acquisisce i propri lock nel cache store predefinito dell'applicazione, quello impostato da CACHE_STORE. Se quello store è locale a ciascun server, per esempio perché l'app usa file per la propria cache, chiamate Schedule::useCache() con il nome di uno store condiviso da config/cache.php. Va inserito nello stesso metodo boot() di alwaysOnOneServer():

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Schedule;

public function boot(): void
{
Schedule::alwaysOnOneServer();

// The 'redis' store from config/cache.php, shared by every server
Schedule::useCache('redis');
}

Il metodo useCache() cambia lo store solo per i lock dello scheduler, il che copre onOneServer() e withoutOverlapping(). Il resto dell'applicazione continua a usare il proprio store predefinito. Se lo store predefinito è già condiviso, come redis o database su ogni server, potete saltare questa chiamata.

Non esiste un opt-out per singola attività. Se alcune attività devono essere eseguite su ogni server, come quella che elimina file sul disco locale, lasciate alwaysOnOneServer() disattivato e chiamate onOneServer() sulle attività che devono essere eseguite una sola volta.

Fermare i comandi di lunga durata durante un deploy

Uno script di deploy potrebbe eseguire php artisan schedule:interrupt prima di passare alla nuova release. Prima della 13.35, quel comando impostava un flag nella cache fino alla fine del minuto corrente, e solo schedule:run lo controllava, per fermare il proprio ciclo di attività sub-minuto. Un comando avviato con runInBackground() viene eseguito in un processo separato e non controllava mai il flag, quindi un'attività che durava un'ora terminava sul vecchio codice oppure veniva uccisa a metà.

cyppe ha modificato schedule:interrupt in modo che memorizzi l'orario dell'interruzione, allo stesso modo in cui queue:restart memorizza un orario di riavvio per i queue worker. Un comando di lunga durata può ora confrontare quell'orario con il proprio orario di avvio e fermarsi tra un batch e l'altro:

use Illuminate\Console\Command;
use Illuminate\Console\Scheduling\Schedule;

class SyncInventory extends Command
{
protected $signature = 'inventory:sync';

public function handle(Schedule $schedule): int
{
$startedAt = now();

foreach (Product::lazyById(500) as $product) {
if ($schedule->hasBeenInterruptedSince($startedAt)) {
$this->info('Interrupted by a deploy, stopping.');

return self::SUCCESS;
}

$this->syncProduct($product);
}

return self::SUCCESS;
}
}
Schedule::command('inventory:sync')->hourly()->runInBackground();

Il comando deve poter fermarsi in qualsiasi punto in cui effettua il controllo, e l'esecuzione pianificata successiva deve riprendere il lavoro rimanente. In questo caso, la sincronizzazione può essere rieseguita su tutti i prodotti, quindi fermarsi a metà non fa perdere nulla.

Il metodo hasBeenInterruptedSince() restituisce true solo per un'interruzione avvenuta al momento indicato o dopo, quindi un'interruzione di un deploy precedente non ferma una nuova esecuzione. Lo stesso controllo ora guida schedule:run, che non si ferma più per un'interruzione inviata prima nello stesso minuto.

L'orario dell'interruzione è memorizzato nel cache store predefinito, non nello store passato a useCache(), quindi su più server lo store predefinito deve essere uno condiviso da tutti. Se chiamate Schedule::withoutInterruptionPolling() per risparmiare le letture della cache, hasBeenInterruptedSince() restituisce sempre false.

Uno script di deploy che segnala sia lo scheduler sia i queue worker è simile a questo:

php artisan schedule:interrupt
# switch to the new release, run migrations, and so on
php artisan queue:restart

Per approfondire

Jack Bayliss ha contribuito con alwaysOnOneServer() nella #61789, e cyppe ha contribuito con hasBeenInterruptedSince() nella #61764.

Condividi su: LinkedIn

Altri articoli recenti