PHP
Éviter les commandes en double dans un connecteur : idempotence et file d’attente
Le défaut le plus coûteux d’un connecteur n’est pas la commande qui ne part pas : celle-là se voit. C’est la commande envoyée deux fois, parce qu’un paiement a été validé deux fois, qu’une tâche s’est relancée pendant que la précédente tournait, ou qu’un délai d’attente a été dépassé alors que l’ERP avait bien reçu. La réponse tient en un mot : idempotence. Envoyer deux fois doit produire le même résultat qu’envoyer une fois.
CREATE TABLE sync_file (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(32) NOT NULL,
reference VARCHAR(64) NOT NULL,
statut ENUM('attente', 'en_cours', 'fait', 'erreur') NOT NULL DEFAULT 'attente',
tentatives TINYINT UNSIGNED NOT NULL DEFAULT 0,
erreur TEXT NULL,
cree_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
maj_le DATETIME NULL,
UNIQUE KEY uniq_type_reference (type, reference)
) ENGINE=InnoDB;
-- Mise en file : une deuxième insertion de la même commande est ignorée
INSERT IGNORE INTO sync_file (type, reference) VALUES ('commande', 'XKBKNABJK');
Trois protections se superposent. La clé d’unicité empêche qu’une commande entre deux fois dans la file. FOR UPDATE SKIP LOCKED garantit que deux processus lancés en même temps ne prennent jamais la même ligne. Et la vérification côté ERP avant création couvre le cas le plus sournois : l’envoi réussi dont la réponse s’est perdue. Si l’API d’en face accepte une clé d’idempotence dans ses en-têtes, on l’utilise en plus. Les erreurs restent en file avec leur message : on les relit, on corrige, elles repartent seules.
// Réserver une ligne sans gêner un autre processus (MariaDB 10.6+, MySQL 8+)
$pdo->beginTransaction();
$ligne = $pdo->query("
SELECT id, reference FROM sync_file
WHERE statut = 'attente' OR (statut = 'erreur' AND tentatives < 5)
ORDER BY id LIMIT 1
FOR UPDATE SKIP LOCKED")->fetch(PDO::FETCH_ASSOC);
if ($ligne) {
$pdo->prepare("UPDATE sync_file SET statut = 'en_cours', maj_le = NOW() WHERE id = ?")
->execute([$ligne['id']]);
}
$pdo->commit();
if (!$ligne) {
exit;
}
try {
// Vérifier avant d'écrire : la commande existe-t-elle déjà côté ERP ?
if (!$erp->commandeExiste($ligne['reference'])) {
$erp->creerCommande($ligne['reference']);
}
$pdo->prepare("UPDATE sync_file SET statut = 'fait', maj_le = NOW() WHERE id = ?")
->execute([$ligne['id']]);
} catch (Throwable $e) {
$pdo->prepare("UPDATE sync_file SET statut = 'erreur', tentatives = tentatives + 1,
erreur = ?, maj_le = NOW() WHERE id = ?")
->execute([mb_substr($e->getMessage(), 0, 2000), $ligne['id']]);
}
Sources
- MariaDB Knowledge Base, SELECT … SKIP LOCKED mariadb.com