Ce qui se passe
Dès que schedule:run tourne sur plus d'un serveur, chaque tâche planifiée
s'exécute une fois par serveur. Sans importance pour vider un cache local,
gênant pour un rapport quotidien envoyé par e-mail. onOneServer() existait,
mais il fallait y penser tâche par tâche, et l'oubli ne se voit qu'au premier
doublon.
Laravel 13.35 ajoute deux méthodes sur Schedule : alwaysOnOneServer() et
hasBeenInterruptedSince().
Une seule exécution, pour tout le planning
À appeler une fois, dans boot() d'un service provider :
use Illuminate\Support\Facades\Schedule;
public function boot(): void
{
Schedule::alwaysOnOneServer();
// Store partagé par tous les serveurs, déclaré dans config/cache.php
Schedule::useCache('redis');
}Le drapeau s'applique quand le planning lit ses événements : les tâches déclarées
plus tard dans routes/console.php sont couvertes. Il est désactivé par défaut ;
Schedule::alwaysOnOneServer(false) le coupe. Le mécanisme reste celui de
onOneServer() : un verrou dans le cache, le premier serveur à démarrer la
tâche l'emporte.
Deux pièges, tous deux silencieux :
- Une closure sans
name()est ignorée et tourne donc sur chaque serveur, sans avertissement. Le verrou se construit sur le nom de la tâche. - Le cache doit être partagé : Redis, Memcached, DynamoDB ou base de données.
Avec
fileouarray, chaque serveur a son verrou et tout s'exécute partout.Schedule::useCache()ne change le store que pour les verrous du scheduler (onOneServer()etwithoutOverlapping()).
// tourne sur chaque serveur, même avec alwaysOnOneServer()
Schedule::call(fn () => Rapport::envoyer())->daily();
// tourne sur un seul serveur
Schedule::call(fn () => Rapport::envoyer())->name('rapport-quotidien')->daily();Il n'y a pas d'exclusion par tâche : si certaines doivent tourner partout (vider
des fichiers locaux), laissez le drapeau éteint et mettez onOneServer() sur les
autres.
Arrêter une commande longue au déploiement
Avant 13.35, schedule:interrupt posait un drapeau valable jusqu'à la fin de la
minute, que seul schedule:run lisait. Une commande lancée avec
runInBackground() ne le voyait jamais : elle finissait sur l'ancien code ou se
faisait tuer. L'heure de l'interruption est maintenant stockée, comme
queue:restart le fait pour les workers :
$debut = now();
foreach (Produit::lazyById(500) as $produit) {
if ($schedule->hasBeenInterruptedSince($debut)) {
return self::SUCCESS;
}
$this->synchroniser($produit);
}hasBeenInterruptedSince() ne renvoie true que pour une interruption à
l'heure passée ou après : celle d'un déploiement précédent n'arrête pas un
nouveau run. La commande doit pouvoir s'arrêter là où elle vérifie, et le passage
suivant reprendre le travail restant. Autres réserves : l'heure vit dans le store
par défaut, pas celui de useCache(), et avec
Schedule::withoutInterruptionPolling() la méthode renvoie toujours false.
Source : Running Laravel's Scheduler on Multiple Servers, Laravel News, 9 octobre 2026. Contributions de Jack Bayliss (#61789) et de cyppe (#61764).