@pierre-gilles @cicoub13 petite avancé
Résumé par chatGPT:
Freeze complet de Gladys lors de la mise à jour d’un appareil Zigbee2MQTT
Je rencontre un blocage reproductible de Gladys lorsque je mets à jour depuis l’interface Zigbee2MQTT une prise nommée Station informatique.
À l’origine, je voulais simplement mettre à jour cet appareil car certaines fonctionnalités liées à l’énergie/consommation étaient absentes.
Symptômes
Lors de la mise à jour de l’appareil depuis Gladys :
-
la requête finit par timeout ;
-
l’interface devient partiellement ou totalement inutilisable ;
-
les écritures SQLite commencent ensuite à échouer avec :
SequelizeTimeoutError
SQLITE_BUSY: database is locked
Par exemple :
Unable to reset failure count of integration ...
SQLITE_BUSY: database is locked
Le serveur HTTP lui-même continue toutefois à répondre sur le port 8455.
Configuration DB
La base SQLite est assez importante :
gladys-production.db ~12 Go
gladys-production.db-wal ~1 Go
gladys-production.duckdb ~1,9 Go
SQLite fonctionne en WAL :
PRAGMA journal_mode;
wal
Un checkpoint passif fonctionne :
PRAGMA wal_checkpoint(PASSIVE);
0|227|227
Les requêtes simples sur SQLite restent rapides :
SELECT COUNT(*) FROM t_device_feature;
446
real 0m0.090s
Et :
SELECT COUNT(*) FROM t_device_feature_state;
0
Les états semblent donc bien avoir été migrés vers DuckDB.
Appareil concerné
L’appareil est :
id:
fd928ba6-ca0d-4a0d-8483-5b74cfddce8c
external_id:
zigbee2mqtt:Station informatique
Avant la tentative de mise à jour, ses features enregistrées sont notamment :
Commutateur
Puissance consommée
Intensité consommée
Tension moyenne
Energie consommée
Intensité du signal
Mode de contrôle d'accès
La feature « Mode de contrôle d’accès » correspond à :
zigbee2mqtt:Station informatique:access-control:mode:child_lock
Investigation dans device.create.js
J’ai instrumenté :
/src/server/lib/device/device.create.js
afin de déterminer précisément où la transaction se bloque.
Le début de la mise à jour fonctionne normalement :
[DEBUG DEVICE CREATE] START zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE feature cleanup zigbee2mqtt:Station informatique
Le blocage se produit donc pendant cette partie de device.create.js :
await Promise.map(deviceInDb.features, async (existingFeature) => {
if (!matchFeatureInList(existingFeature, features)) {
await existingFeature.destroy({ transaction });
}
});
J’ai ensuite instrumenté chaque feature.
Résultat :
[DEBUG FEATURE CLEANUP] ...:switch:binary:state matched= true
[DEBUG FEATURE CLEANUP] ...:switch:power:power matched= true
[DEBUG FEATURE CLEANUP] ...:switch:current:current matched= true
[DEBUG FEATURE CLEANUP] ...:switch:voltage:voltage matched= true
[DEBUG FEATURE CLEANUP] ...:switch:energy:energy matched= true
Puis :
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:access-control:mode:child_lock matched= false
[DEBUG FEATURE DESTROY] BEFORE zigbee2mqtt:Station informatique:access-control:mode:child_lock
Et aucun DEBUG FEATURE DESTROY AFTER n’apparaît.
La feature suivante est encore inspectée par le Promise.map :
[DEBUG FEATURE CLEANUP] ...:signal:integer:linkquality matched= true
mais le Promise.map ne termine jamais puisque le destroy() de child_lock reste bloqué.
Ce qui semble se produire
La nouvelle définition remontée par Zigbee2MQTT ne contient plus la feature :
access-control:mode:child_lock
Gladys considère donc logiquement cette ancienne feature comme supprimée et exécute :
await existingFeature.destroy({ transaction });
C’est précisément cet appel qui ne retourne pas.
La transaction device.create() reste alors ouverte.
Peu après, d’autres composants de Gladys essayent d’écrire dans SQLite et commencent à produire :
SQLITE_BUSY: database is locked
Par exemple les mises à jour de t_service :
UPDATE `t_service`
SET `failure_count`=$1,`updated_at`=$2
WHERE `id` = $3
finissent en SequelizeTimeoutError.
Cela donne donc, à ce stade, la séquence suivante :
Mise à jour appareil Z2M
↓
device.create()
↓
deviceInDb.update() OK
↓
nettoyage des anciennes features
↓
child_lock absent du nouveau device
↓
existingFeature.destroy({transaction})
↓
BLOCAGE
↓
transaction SQLite restant ouverte
↓
autres écritures
↓
SQLITE_BUSY / database is locked
À propos de l’énergie
Le problème est apparu pendant que j’essayais de récupérer les fonctionnalités de consommation d’énergie de cette prise, ce qui m’avait initialement orienté vers le nouveau code d’energy monitoring.
Cependant les traces montrent maintenant que le blocage intervient avant la création/mise à jour des features énergie.
La feature energy existante est d’ailleurs correctement reconnue :
zigbee2mqtt:Station informatique:switch:energy:energy matched=true
Le blocage est déclenché par la tentative de suppression de l’ancienne feature child_lock.
Autre observation
Pendant le freeze, le service Energy Monitoring continue à démarrer ses traitements et indique notamment :
Found 52 energy devices
Found 0 devices with both INDEX and thirty-minutes-consumption features
Puis les SQLITE_BUSY apparaissent sur différentes parties de Gladys.
J’ai également vérifié t_job : les jobs de migration SQLite → DuckDB et de purge des états orphelins DuckDB sont indiqués success.
Patch testé sans succès
J’avais également testé une modification de device.create.js afin de ne déclencher la purge des états que lorsque keep_history passe réellement de true à false :
const keepHistoryBeforeUpdate = deviceFeature.keep_history;
await deviceFeature.update(featureToUpdate, { transaction });
if (keepHistoryBeforeUpdate !== false && deviceFeature.keep_history === false) {
deviceFeaturesIdsToPurge.push(deviceFeature.id);
}
Cela ne corrige pas ce problème : grâce aux traces supplémentaires, on sait maintenant que le freeze intervient plus tôt, pendant le destroy() de la feature obsolète.
État actuel du diagnostic
Le point de blocage reproductible est donc maintenant identifié assez précisément :
existingFeature.destroy({ transaction })
pour :
zigbee2mqtt:Station informatique:access-control:mode:child_lock