¿Cómo saber qué proceso es responsable de un “Error operativo: la base de datos está bloqueada”?

A veces me encuentro al azar:

Error operacional: la base de datos está bloqueada

en un proceso que actualiza una base de datos SQLite, pero me resulta difícil reproducir el error:

  • ningún otro proceso está insertando / eliminando filas al mismo tiempo
  • solo un proceso puede hacer algunas consultas de solo lectura ( SELECT , etc.) aquí y allá, pero no hay confirmación

Ya he leído OperationalError: la base de datos está bloqueada

Pregunta: ¿Hay alguna forma, cuando se produce este error, de registrar qué otra ID de proceso es responsable del locking?

Más generalmente, ¿cómo depurar un error OperationalError: database is locked ?

¿Hay alguna forma, cuando ocurra este error, de registrar qué otro ID de proceso es responsable del locking?

No, esa información no se registra cuando se produce la excepción. La excepción OperationalError: database is locked generalmente se genera después de un tiempo de espera (5 minutos es el valor predeterminado) cuando se intenta obtener una SQLITE_BUSY SQLITE_BUSY y un locking de archivo en las partes internas de SQLite, momento en el que SQLite devuelve SQLITE_BUSY , pero también se puede informar de SQLITE_BUSY otros puntos Los códigos de error de SQLite no tienen ningún contexto adicional, como el PID de otro proceso que mantuvo un locking, ¡y es posible que el locking se haya pasado entre otros dos procesos antes de que el proceso actual dejara de intentar controlarlo!

En el mejor de los casos, puede enumerar qué procesos están accediendo actualmente al archivo utilizando lsof , pero eso no le ayudará a descubrir cuál de ellos está demorando demasiado en comprometerse.

En su lugar, instrumentaría su código con transacciones explícitas y un registro detallado de cuándo inicia y confirma las transacciones. Luego, cuando se ejecuta en las excepciones de OperationalError , puede revisar los registros para ver qué sucedió en ese intervalo de tiempo.

Un administrador de contexto de Python que podría usarse para esto es:

 import logging import sys import time import threading from contextlib import contextmanager from uuid import uuid4 logger = logging.getLogger(__name__) @contextmanager def logged_transaction(con, stack_info=False, level=logging.DEBUG): """Manage a transaction and log start and end times. Logged messages include a UUID transaction ID for ease of analysis. If trace is set to True, also log all statements executed. If stack_info is set to True, a stack trace is included to record where the transaction was started (the last two lines will point to this context manager). """ transaction_id = uuid4() thread_id = threading.get_ident() def _trace_callback(statement): logger.log(level, '(txid %s) executing %s', transaction_id, statement) if trace: con.set_trace_callback(_trace_callback) logger.log(level, '(txid %s) starting transaction', transaction_id, stack_info=stack_info) start = time.time() try: with con: yield con finally: # record exception information, if an exception is active exc_info = sys.exc_info() if exc_info[0] is None: exc_info = None if trace: con.set_trace_callback(None) logger.log(level, '(txid %s) transaction closed after %.6f seconds', transaction_id, time.time() - start, exc_info=exc_info) 

Lo anterior creará entradas de inicio y finalización, incluirá información de excepción si hay alguna, opcionalmente rastreará todas las declaraciones que se ejecutan en la conexión y puede incluir un seguimiento de stack que le indicará la ubicación donde se usó el administrador de contexto. Asegúrese de incluir la fecha y la hora cuando formatee los mensajes de registro para que pueda hacer un seguimiento de cuándo comenzaron las transacciones.

Lo usaría alrededor de cualquier código que use la conexión, para que también pueda seleccionar los tiempos:

 with logged_transaction(connection): cursor = connection.cursor() # ... 

Puede ser que el solo hecho de usar este administrador de contexto haga que su problema desaparezca, en ese momento tendría que analizar por qué el código sin este administrador de contexto deja abierta una transacción sin comprometerse.

También es posible que desee utilizar un valor de timeout menor en las llamadas a sqlite3.connect() para acelerar el proceso; Es posible que no tenga que esperar 5 minutos completos para detectar la situación.

Nota sobre los subprocesos: cuando se habilita el rastreo, se supone que utiliza conexiones separadas para subprocesos separados. Si este no es el caso, entonces deberá registrar de forma permanente una callback de seguimiento que luego ordena qué ID de transacción usar para el hilo actual.

Solución: ¡ Siempre cierre el cursor para las consultas (incluso de solo lectura)!

Primero, aquí hay una manera de reproducir el problema:

  1. Primero ejecuta este código, una vez:

     import sqlite3 conn = sqlite3.connect('anothertest.db') conn.execute("CREATE TABLE IF NOT EXISTS mytable (id int, description text)") for i in range(100): conn.execute("INSERT INTO mytable VALUES(%i, 'hello')" % i) conn.commit() 

    Para inicializar la prueba.

  2. Luego comience una consulta de solo lectura :

     import sqlite3, time conn = sqlite3.connect('anothertest.db') c = conn.cursor() c.execute('SELECT * FROM mytable') item = c.fetchone() print(item) print('Sleeping 60 seconds but the cursor is not closed...') time.sleep(60) 

    y mantenga este script en ejecución mientras ejecuta el siguiente paso :

  3. A continuación, intente eliminar algunos contenidos y cometer:

     import sqlite3 conn = sqlite3.connect('anothertest.db') conn.execute("DELETE FROM mytable WHERE id > 90") conn.commit() 

    Se activará este error de hecho:

    sqlite3.OperationalError: la base de datos está bloqueada

¿Por qué? Debido a que no es posible eliminar los datos a los que se accede actualmente mediante una consulta de lectura: si el cursor todavía está abierto, significa que los datos aún se pueden buscar con fetchone o fetchall .

Aquí es cómo resolver el error: en el paso # 2, simplemente agregue:

 item = c.fetchone() print(item) c.close() time.sleep(60) 

Luego, mientras esto todavía se está ejecutando, inicie el script # 3, verá que no hay más errores.