fix(cluster): use native sqlite3 .restore API to eliminate WAL/SHM desynchronization and corruption during live sync

This commit is contained in:
sorcier committed 2026-11-06 00:10:44 +01:00
1 parent 2db63d66ce
commit 2db19ed2dd
3 files changed
+28 -8

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ echo "[$TIMESTAMP] [CLUSTER] <<< Transition vers l'état BACKUP / STANDBY (En ve
DB_PATH="/var/www/nexus-pbx/storage/database.sqlite" DB_PATH="/var/www/nexus-pbx/storage/database.sqlite"
if [ -f "$DB_PATH" ]; then 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 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 fi
# 2. Désenregistrement des Trunks PJSIP pour éviter les conflits d'appels entrants avec le Master # 2. Désenregistrement des Trunks PJSIP pour éviter les conflits d'appels entrants avec le Master
+2
View File
@@ -16,6 +16,8 @@ ASTDB_PATH="/var/lib/asterisk/astdb.sqlite3"
if [ -f "$DB_PATH" ]; then 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 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 fi
# 2. Vérification et réparation d'intégrité de la base SQLite AstDB (contacts téléphones) # 2. Vérification et réparation d'intégrité de la base SQLite AstDB (contacts téléphones)
+24 -8
View File
@@ -653,7 +653,7 @@ class ClusterManager
throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application."); 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(); $dbPath = Database::getDatabasePath();
if (file_exists($dbPath)) { if (file_exists($dbPath)) {
@copy($dbPath, $dbPath . '.pre_cluster_sync.bak'); @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); $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) {} } catch (\Throwable $e) {}
// Libérer les verrous PDO SQLite locaux et purger les résidus WAL/SHM pour éviter tout malformed // Restauration officielle en ligne via l'API SQLite .restore
Database::resetConnection(); // Ne détruit aucun descripteur de fichier, ne touche pas aux journaux WAL/SHM,
@unlink($dbPath . '-wal'); // et n'invalide aucune lecture concurrente dans PHP-FPM.
@unlink($dbPath . '-shm'); if (file_exists($dbPath)) {
$restoreCmd = "sqlite3 " . escapeshellarg($dbPath) . " \".restore " . escapeshellarg($extractedDb) . "\" 2>&1";
// Remplacement atomique de la base de données $restoreOut = shell_exec($restoreCmd);
rename($extractedDb, $dbPath); if (!empty($restoreOut)) {
error_log("[CLUSTER] Avertissement sqlite3 .restore : " . trim($restoreOut));
}
} else {
@copy($extractedDb, $dbPath);
@chmod($dbPath, 0660); @chmod($dbPath, 0660);
@chown($dbPath, 'www-data'); @chown($dbPath, 'www-data');
@chgrp($dbPath, 'asterisk'); @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 // Extraction des sons personnalisés si présents
if (is_dir('/var/lib/asterisk/sounds')) { if (is_dir('/var/lib/asterisk/sounds')) {