@pierre-gilles @cicoub13 pequeño avance
Resumen por chatGPT:
Congelamiento completo de Gladys al actualizar un dispositivo Zigbee2MQTT
Encuentro un bloqueo reproducible de Gladys cuando actualizo desde la interfaz Zigbee2MQTT un enchufe llamado Estación informática.
Originalmente, solo quería actualizar este dispositivo porque algunas funciones relacionadas con la energía/consumo estaban ausentes.
Síntomas
Al actualizar el dispositivo desde Gladys:
-
la solicitud termina en tiempo de espera;
-
la interfaz se vuelve parcialmente o totalmente inutilizable;
-
las escrituras SQLite comienzan a fallar con:
SequelizeTimeoutError
SQLITE_BUSY: database is locked
Por ejemplo:
No se puede restablecer el contador de fallos de la integración ...
SQLITE_BUSY: database is locked
El servidor HTTP en sí sigue respondiendo en el puerto 8455.
Configuración DB
La base de datos SQLite es bastante grande:
gladys-production.db ~12 Go
gladys-production.db-wal ~1 Go
gladys-production.duckdb ~1,9 Go
SQLite funciona en WAL:
PRAGMA journal_mode;
wal
Un checkpoint pasivo funciona:
PRAGMA wal_checkpoint(PASSIVE);
0|227|227
Las consultas simples en SQLite siguen siendo rápidas:
SELECT COUNT(*) FROM t_device_feature;
446
real 0m0.090s
Y:
SELECT COUNT(*) FROM t_device_feature_state;
0
Los estados parecen haber sido migrados correctamente a DuckDB.
Dispositivo afectado
El dispositivo es:
id:
fd928ba6-ca0d-4a0d-8483-5b74cfddce8c
external_id:
zigbee2mqtt:Estación informática
Antes del intento de actualización, sus características registradas son, entre otras:
Interruptor
Potencia consumida
Intensidad consumida
Tensión media
Energía consumida
Intensidad de la señal
Modo de control de acceso
La característica « Modo de control de acceso » corresponde a:
zigbee2mqtt:Estación informática:access-control:mode:child_lock
Investigación en device.create.js
He instrumentado:
/src/server/lib/device/device.create.js
a fin de determinar exactamente dónde se bloquea la transacción.
El inicio de la actualización funciona normalmente:
[DEBUG DEVICE CREATE] START zigbee2mqtt:Estación informática
[DEBUG DEVICE CREATE] BEFORE getDeviceInDb zigbee2mqtt:Estación informática
[DEBUG DEVICE CREATE] AFTER getDeviceInDb zigbee2mqtt:Estación informática
[DEBUG DEVICE CREATE] BEFORE device update zigbee2mqtt:Estación informática
[DEBUG DEVICE CREATE] AFTER device update zigbee2mqtt:Estación informática
[DEBUG DEVICE CREATE] BEFORE feature cleanup zigbee2mqtt:Estación informática
El bloqueo ocurre durante esta parte de device.create.js:
await Promise.map(deviceInDb.features, async (existingFeature) => {
if (!matchFeatureInList(existingFeature, features)) {
await existingFeature.destroy({ transaction });
}
});
Luego instrumenté cada característica.
Resultado:
[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
Luego:
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Estación informática:access-control:mode:child_lock matched= false
[DEBUG FEATURE DESTROY] BEFORE zigbee2mqtt:Estación informática:access-control:mode:child_lock
Y no aparece ningún DEBUG FEATURE DESTROY AFTER.
La siguiente característica aún es inspeccionada por el Promise.map:
[DEBUG FEATURE CLEANUP] ...:signal:integer:linkquality matched= true
pero el Promise.map nunca termina porque el destroy() de child_lock sigue bloqueado.
Lo que parece estar ocurriendo
La nueva definición proporcionada por Zigbee2MQTT ya no contiene la característica:
access-control:mode:child_lock
Gladys considera, por lo tanto, lógicamente esta antigua característica como eliminada y ejecuta:
await existingFeature.destroy({ transaction });
Es precisamente esta llamada la que no devuelve nada.
La transacción device.create() permanece entonces abierta.
Poco después, otros componentes de Gladys intentan escribir en SQLite y comienzan a producir:
SQLITE_BUSY: database is locked
Por ejemplo, las actualizaciones de t_service:
UPDATE `t_service`
SET `failure_count`=$1,`updated_at`=$2
WHERE `id` = $3
terminan en SequelizeTimeoutError.
Esto da, por lo tanto, en este punto, la siguiente secuencia:
Actualización dispositivo Z2M
↓
device.create()
↓
deviceInDb.update() OK
↓
limpieza de las antiguas características
↓
child_lock ausente del nuevo dispositivo
↓
existingFeature.destroy({transaction})
↓
BLOQUEO
↓
transacción SQLite permaneciendo abierta
↓
otras escrituras
↓
SQLITE_BUSY / database is locked
Sobre la energía
El problema apareció mientras intentaba recuperar las funciones de consumo de energía de este enchufe, lo que me había orientado inicialmente hacia el nuevo código de monitoreo de energía.
Sin embargo, los registros muestran ahora que el bloqueo ocurre antes de la creación/actualización de las características de energía.
La característica energy existente es correctamente reconocida:
zigbee2mqtt:Estación informática:switch:energy:energy matched=true
El bloqueo es desencadenado por el intento de eliminación de la antigua característica child_lock.
Otra observación
Durante el congelamiento, el servicio Energy Monitoring continúa iniciando sus procesos e indica, entre otras cosas:
Found 52 energy devices
Found 0 devices with both INDEX and thirty-minutes-consumption features
Luego aparecen los SQLITE_BUSY en diferentes partes de Gladys.
También verifiqué t_job: los trabajos de migración SQLite → DuckDB y de purga de los estados huérfanos DuckDB están indicados como success.
Parche probado sin éxito
También había probado una modificación de device.create.js para desencadenar la purga de los estados solo cuando keep_history realmente cambia de true a false:
const keepHistoryBeforeUpdate = deviceFeature.keep_history;
await deviceFeature.update(featureToUpdate, { transaction });
if (keepHistoryBeforeUpdate !== false && deviceFeature.keep_history === false) {
deviceFeaturesIdsToPurge.push(deviceFeature.id);
}
Esto no corrige este problema: gracias a los registros adicionales, ahora sabemos que el congelamiento ocurre antes, durante el destroy() de la característica obsoleta.
Estado actual del diagnóstico
El punto de bloqueo reproducible está ahora identificado con bastante precisión:
existingFeature.destroy({ transaction })
para:
zigbee2mqtt:Estación informática:access-control:mode:child_lock