From 2db19ed2dd255a1c1162a257d7079bd773fe1f56 Mon Sep 17 00:00:00 2001 From: sorcier Date: Fri, 6 Nov 2026 00:10:44 +0100 Subject: [PATCH] fix(cluster): use native sqlite3 .restore API to eliminate WAL/SHM desynchronization and corruption during live sync --- scripts/cluster/notify_backup.sh | 2 ++ scripts/cluster/notify_master.sh | 2 ++ src/ClusterManager.php | 36 +++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/scripts/cluster/notify_backup.sh b/scripts/cluster/notify_backup.sh index 10bb06a..f600b34 100755 --- a/scripts/cluster/notify_backup.sh +++ b/scripts/cluster/notify_backup.sh @@ -14,6 +14,8 @@ echo "[$TIMESTAMP] [CLUSTER] <<< Transition vers l'état BACKUP / STANDBY (En ve DB_PATH="/var/www/nexus-pbx/storage/database.sqlite" if [ -f "$DB_PATH" ]; then sqlite3 "$DB_PATH" "UPDATE cluster_settings SET last_sync_status='standby_idle', updated_at=CURRENT_TIMESTAMP WHERE id=1;" 2>/dev/null || true + chown www-data:asterisk "$DB_PATH" "${DB_PATH}-wal" "${DB_PATH}-shm" 2>/dev/null || true + chmod 660 "$DB_PATH" "${DB_PATH}-wal" "${DB_PATH}-shm" 2>/dev/null || true fi # 2. Désenregistrement des Trunks PJSIP pour éviter les conflits d'appels entrants avec le Master diff --git a/scripts/cluster/notify_master.sh b/scripts/cluster/notify_master.sh index 3a0bc68..56a855d 100755 --- a/scripts/cluster/notify_master.sh +++ b/scripts/cluster/notify_master.sh @@ -16,6 +16,8 @@ ASTDB_PATH="/var/lib/asterisk/astdb.sqlite3" if [ -f "$DB_PATH" ]; then sqlite3 "$DB_PATH" "UPDATE cluster_settings SET last_sync_status='master_active', updated_at=CURRENT_TIMESTAMP WHERE id=1;" 2>/dev/null || true + chown www-data:asterisk "$DB_PATH" "${DB_PATH}-wal" "${DB_PATH}-shm" 2>/dev/null || true + chmod 660 "$DB_PATH" "${DB_PATH}-wal" "${DB_PATH}-shm" 2>/dev/null || true fi # 2. Vérification et réparation d'intégrité de la base SQLite AstDB (contacts téléphones) diff --git a/src/ClusterManager.php b/src/ClusterManager.php index 7341e71..62b00cd 100644 --- a/src/ClusterManager.php +++ b/src/ClusterManager.php @@ -653,7 +653,7 @@ class ClusterManager throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application."); } - // Sauvegarde d'urgence de la base actuelle avant remplacement + // Sauvegarde d'urgence de la base actuelle avant synchronisation $dbPath = Database::getDatabasePath(); if (file_exists($dbPath)) { @copy($dbPath, $dbPath . '.pre_cluster_sync.bak'); @@ -666,16 +666,32 @@ class ClusterManager $localSettingsRow = $this->db->query("SELECT local_net, local_networks_json, admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC); } catch (\Throwable $e) {} - // Libérer les verrous PDO SQLite locaux et purger les résidus WAL/SHM pour éviter tout malformed - Database::resetConnection(); - @unlink($dbPath . '-wal'); - @unlink($dbPath . '-shm'); + // Restauration officielle en ligne via l'API SQLite .restore + // Ne détruit aucun descripteur de fichier, ne touche pas aux journaux WAL/SHM, + // et n'invalide aucune lecture concurrente dans PHP-FPM. + if (file_exists($dbPath)) { + $restoreCmd = "sqlite3 " . escapeshellarg($dbPath) . " \".restore " . escapeshellarg($extractedDb) . "\" 2>&1"; + $restoreOut = shell_exec($restoreCmd); + if (!empty($restoreOut)) { + error_log("[CLUSTER] Avertissement sqlite3 .restore : " . trim($restoreOut)); + } + } else { + @copy($extractedDb, $dbPath); + @chmod($dbPath, 0660); + @chown($dbPath, 'www-data'); + @chgrp($dbPath, 'asterisk'); + } - // Remplacement atomique de la base de données - rename($extractedDb, $dbPath); - @chmod($dbPath, 0660); - @chown($dbPath, 'www-data'); - @chgrp($dbPath, 'asterisk'); + @unlink($extractedDb); + + // Contrôle d'intégrité immédiat après synchronisation + $postCheck = trim(shell_exec("sqlite3 " . escapeshellarg($dbPath) . " 'PRAGMA integrity_check;' 2>/dev/null") ?? ''); + if ($postCheck !== 'ok') { + if (file_exists($dbPath . '.pre_cluster_sync.bak')) { + shell_exec("sqlite3 " . escapeshellarg($dbPath) . " \".restore " . escapeshellarg($dbPath . '.pre_cluster_sync.bak') . "\" 2>/dev/null || true"); + } + throw new RuntimeException("Échec de vérification d'intégrité après synchronisation ({$postCheck}). Restauration préventive effectuée."); + } // Extraction des sons personnalisés si présents if (is_dir('/var/lib/asterisk/sounds')) {