Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿Se atasca la copia de SQLite en Python? Revisa ambas conexiones antes del commit

Distingue una escritura pendiente en el origen, un destino ocupado y un bloqueo externo: el indicador de transacción no basta para decidir.

Tres estados probados con indicador de transacción verdadero y resultados diferentes
Ilustración explicativa original: el mismo indicador puede acompañar reintentos, éxito o rechazo. Watch at Ease

Una copia de SQLite en Python puede seguir reintentando sin lanzar la habitual excepción de base de datos bloqueada. Añadir commit() arregla algunos ejemplos, pero también puede confirmar cambios que no corresponde decidir a una función auxiliar. Identifica primero el origen, el destino y el trabajo pendiente.

Doce controles pequeños distinguen una copia atascada, un rechazo inmediato del destino y una transacción de lectura que sí funciona. También muestran que in_transaction=True es una pista para investigar, no una orden de confirmar ni una razón universal para rechazar la copia.

1. Anota las funciones de ambas conexiones antes de cambiar su estado

Registra Python, sqlite3.sqlite_version, la llamada exacta, las rutas y los valores autocommit, isolation_level e in_transaction de ambas conexiones. Identifica su última instrucción SQL y quién controla cada transacción. Usa un candidato independiente con nombre único como destino; nunca apuntes al origen activo ni a una copia anterior que necesites conservar.

Distingue el fallo anterior al primer callback de los reintentos repetidos. Registra juntos el estado, las páginas restantes y el total. No consultes el destino durante backup: SQLite restringe expresamente el uso de esa conexión mientras dura la operación. Cero páginas restantes con estado busy no demuestra que haya terminado.

2. No conviertas un booleano en un diagnóstico

Con CPython 3.12.14 y SQLite 3.53.1 en Linux, un INSERT sin confirmar en el origen produjo repetidamente el estado 5, SQLITE_BUSY. El callback diagnóstico lo detuvo tras tres intentos. Cuando el responsable de la transacción confirmó deliberadamente los cambios del ensayo, el mismo origen se copió bien. Un bloqueo exclusivo externo también produjo 5; el número no identificaba al responsable.

Con autocommit=False, tanto una transacción diferida vacía como otra que había leído la tabla en el origen se copiaron con in_transaction=True. Un destino nuevo en ese modo también funcionó antes de leer. Después de un SELECT, otra copia falló con “destination database is in use” sin invocar el progreso. También se rechazó un destino con una escritura pendiente. Importan la función de la conexión y el trabajo realizado, no solo el indicador.

3. Decide qué datos debe contener la copia

Si debe incluir las escrituras pendientes del origen, deja que el responsable de la transacción termine la unidad prevista antes de reintentar. Una función genérica no debe confirmar cambios solo para ocultar el síntoma. Python documenta que asignar autocommit=True confirma una transacción pendiente: cambiarlo en una conexión existente implica una decisión real sobre los datos.

Para una base en archivo, una conexión dedicada de solo lectura puede copiar los datos confirmados sin confirmar al escritor. Nuestro control guardó la fila 1 mientras el escritor aún veía su fila 2 pendiente. Ese éxito excluyó intencionadamente la fila 2. Otra conexión a un :memory: simple representa una base diferente; la alternativa no se traslada automáticamente a un origen en memoria.

4. Limita los reintentos en el callback de la copia

La implementación de CPython probada repite con SQLITE_OK, SQLITE_BUSY y SQLITE_LOCKED. Ejecuta el callback de progreso tras cada paso; una excepción desde él finaliza y abandona la operación. El timeout de espera de bloqueos no fija un plazo global para backup. Usa backup(progress=...), sin confundirlo con el manejador independiente de progreso de ejecución SQL.

Para limitar el tiempo total, fija antes de llamar un plazo con time.monotonic(). En el callback registra los tres argumentos y lanza TimeoutError si se supera el plazo y el estado no es SQLITE_DONE. Mantén el plazo fijo si también debes limitar trabajo que progresa pero vuelve a empezar. Con timeouts de conexión a cero y un presupuesto de 0,03 segundos, el control bloqueado lanzó la excepción aproximadamente a los 0,03 segundos. Es una comprobación cooperativa entre pasos, no un límite estricto para una llamada de sistema detenida.

5. Separa la cancelación de la aceptación de una copia

Tras un rechazo o una excepción del callback, cierra las conexiones dedicadas y mantén el candidato fuera del conjunto de copias utilizables. Su existencia, la ejecución de un callback o capturar un error no justifican promoverlo. Si la llamada termina bien, cierra el destino y vuelve a abrir ese candidato exacto para comprobar su integridad y los datos de aplicación necesarios; la guía enlazada explica esa etapa posterior.

Los controles usaron bases pequeñas, locales y en memoria, con diario de rollback. No probaron escrituras continuas en WAL, sistemas de archivos de red, ORM ni todas las versiones de Python. Un informe independiente de Hermes condujo a una corrección integrada el 16 de agosto de 2026 que limita reintentos busy consecutivos. Esa política específica no equivale a un plazo global universal. Conserva el origen y define cómo aceptarás la finalización antes del siguiente intento.

Control del origen, destino aislado, presupuesto de reintentos y aceptación del candidato
Ilustración explicativa original: diagnostica las funciones antes de modificar datos. Watch at Ease