Para reanudar un pipeline de Python desde SQLite, guarda en una tabla una fila por unidad de trabajo (elemento, página o lote pequeño). Escribe su resultado y su estado «completado» en la misma transacción. Al arrancar, procesa solo las filas que no estén completadas. Eso da un checkpoint de aplicación fiable para el trabajo local. No da «exactly once» para efectos externos como una llamada HTTP o un correo, y no tiene nada que ver con el «checkpoint WAL» de SQLite, que es otra cosa. Este artículo explica las dos ideas, el diseño de la tabla, el código y los límites.
Dos «checkpoints» que no deben confundirse
El término aparece con dos significados distintos en este tema:
As an Amazon Associate I earn from qualifying purchases.
| Checkpoint de aplicación | Checkpoint WAL de SQLite | |
|---|---|---|
| Qué es | Filas que registran qué pasos terminaron y qué resultados quedaron guardados | Copia de páginas desde el archivo WAL al archivo principal de la base de datos |
| Quién lo define | Tu código y tu esquema | SQLite (automático o con PRAGMA wal_checkpoint) |
| Para qué sirve | Saber desde dónde continuar tras una caída | Mantener el WAL acotado y el archivo principal al día |
| Afecta a la reanudación | Sí, es la reanudación | No: los datos confirmados son legibles esté o no hecho el checkpoint WAL |
Para la durabilidad, la documentación de SQLite («SQLite Is Transactional») afirma: «SQLite implements serializable transactions that are atomic, consistent, isolated, and durable, even if the transaction is interrupted by a program crash, an operating system crash, or a power failure to the computer.» Esa garantía cubre las transacciones de la base de datos. No se extiende a lo que tu programa haga fuera de ella.
Paso 1: define la unidad reanudable
Antes de escribir código, decide qué es lo mínimo que puedes volver a hacer sin dolor. Hay tres opciones habituales:
#1 Best Overall
- Un elemento: máxima granularidad. Tras una caída repites como mucho un elemento, pero haces más commits.
- Una página de entrada: equilibrio natural si la fuente ya pagina (API, CSV por bloques).
- Un lote pequeño: menos commits, pero una caída obliga a repetir todo el lote en curso.
La regla práctica: elige el tamaño según cuánto trabajo (y cuántos efectos externos) puedes permitirte repetir. Si repetir es caro o peligroso, baja la granularidad.
Paso 2: diseña la tabla de estado
Guarda una identidad estable de la ejecución, la clave del elemento, el estado, el resultado que necesites reutilizar, un contador de intentos con diagnóstico y una marca de actualización. Una restricción UNIQUE impide que la misma unidad aparezca dos veces.
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
pipeline_version INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS items (
run_id TEXT NOT NULL REFERENCES runs(run_id),
item_key TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','running','done','failed')),
result TEXT,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (run_id, item_key)
);
Si el trabajo cambia entre versiones del código, guarda pipeline_version (o la versión del esquema) y decide por escrito si un estado antiguo puede reanudarse o debe invalidarse. Reanudar con resultados calculados por una lógica distinta es una fuente silenciosa de datos incoherentes.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Rank #2
Paso 3: transacciones explícitas en Python
La documentación actual de Python recomienda el atributo autocommit de sqlite3.connect(), disponible desde Python 3.12. Con autocommit=False se sigue el comportamiento de PEP 249: siempre hay una transacción abierta y el programa confirma o revierte. Con isolation_level se conserva el comportamiento anterior (LEGACY_TRANSACTION_CONTROL), y su valor por omisión depende del modo y de la versión.
Los ejemplos de aquí usan Python 3.12 o superior con autocommit=True y transacciones BEGIN IMMEDIATE escritas a mano. Es una elección propia, por dos motivos: PRAGMA journal_mode=WAL no puede cambiarse dentro de una transacción (con autocommit=False ya habría una abierta), y BEGIN IMMEDIATE toma el bloqueo de escritura desde el principio, de modo que el tiempo de espera se aplica de forma predecible. En versiones anteriores, isolation_level=None da un comportamiento equivalente.
import json
import sqlite3
from contextlib import contextmanager
def connect(path: str) -> sqlite3.Connection:
conn = sqlite3.connect(path, autocommit=True, timeout=10.0)
conn.execute("PRAGMA journal_mode=WAL") # persistente en el archivo
conn.execute("PRAGMA busy_timeout=10000")
conn.execute("PRAGMA foreign_keys=ON")
return conn
@contextmanager
def tx(conn):
conn.execute("BEGIN IMMEDIATE")
try:
yield conn
except BaseException:
conn.execute("ROLLBACK")
raise
else:
conn.execute("COMMIT")
Paso 4: el bucle que se puede reanudar
Al arrancar, el proceso (único trabajador en este ejemplo) devuelve a pending las filas que quedaron en running por una caída anterior, siembra los elementos que falten y procesa lo pendiente.
Rank #3
PIPELINE_VERSION = 3
def start_or_resume(conn, run_id, keys):
with tx(conn):
row = conn.execute(
"SELECT pipeline_version FROM runs WHERE run_id=?", (run_id,)
).fetchone()
if row is None:
conn.execute(
"INSERT INTO runs(run_id, pipeline_version) VALUES (?,?)",
(run_id, PIPELINE_VERSION),
)
elif row[0] != PIPELINE_VERSION:
raise RuntimeError(
f"Estado de la versión {row[0]}; el código es la {PIPELINE_VERSION}. "
"Invalida o migra el estado antes de reanudar."
)
conn.executemany(
"INSERT OR IGNORE INTO items(run_id, item_key) VALUES (?,?)",
[(run_id, k) for k in keys],
)
conn.execute(
"UPDATE items SET status='pending' "
"WHERE run_id=? AND status='running'", (run_id,)
)
def run(conn, run_id, transform):
while True:
with tx(conn):
row = conn.execute(
"SELECT item_key FROM items "
"WHERE run_id=? AND status='pending' ORDER BY item_key LIMIT 1",
(run_id,),
).fetchone()
if row is None:
return
key = row[0]
conn.execute(
"UPDATE items SET status='running', attempts=attempts+1, "
"updated_at=datetime('now') WHERE run_id=? AND item_key=?",
(run_id, key),
)
try:
result = transform(key) # trabajo fuera de la transacción
except Exception as exc:
with tx(conn):
conn.execute(
"UPDATE items SET status='failed', last_error=?, "
"updated_at=datetime('now') WHERE run_id=? AND item_key=?",
(repr(exc), run_id, key),
)
continue
with tx(conn): # resultado + marca, juntos
conn.execute(
"UPDATE items SET status='done', result=?, last_error=NULL, "
"updated_at=datetime('now') WHERE run_id=? AND item_key=?",
(json.dumps(result), run_id, key),
)
Puntos que importan:
- El resultado y
donese escriben en una sola transacción. Tras una caída verás ambos o ninguno; nunca un «hecho» sin resultado. - El trabajo pesado queda fuera de la transacción. Mantener transacciones de escritura breves evita bloquear a otros escritores.
- Los pasos siguientes leen de la base, no de memoria. Así un reinicio y una ejecución normal siguen el mismo camino.
- Las filas
failedno se reintentan solas. Decide tu política (reintentar con límite deattempts, o revisión manual) y haz que sea explícita.
Si hay varios procesos trabajadores sobre el mismo archivo, la reclamación de filas necesita más cuidado (por ejemplo, identificar al dueño de cada running y caducar reclamos antiguos). El reinicio «todo running vuelve a pending» de arriba solo es correcto con un único trabajador activo.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →El límite que SQLite no resuelve: efectos externos
Una transacción de SQLite no incluye una llamada HTTP, un correo ni una escritura en otro sistema. Considera esta secuencia: envías el pago o el correo, el proceso muere y aún no has guardado «hecho». Al reanudar, el elemento sigue pendiente y el efecto se repite. Guardar un cursor no convierte esto en «exactly once»; solo reduce el trabajo repetido.
Opciones, de más a menos robusta:
- Idempotency key remota. Si el servicio la admite, deriva la clave de la identidad estable del trabajo (por ejemplo
f"{run_id}:{item_key}"), envíala en cada intento y guarda la respuesta junto al estado. Un reintento devolverá el resultado original en lugar de duplicar el efecto, según cómo implemente el servicio la idempotencia. - Deduplicación en el destino. Escribe con una clave natural (upsert) para que repetir sea inocuo.
- Protocolo coordinado. Si ambos lados lo permiten, registra la intención antes del efecto y confirma después, con un paso de reconciliación para las intenciones sin confirmar.
- Sin idempotencia posible. Asume que puede repetirse, o exige reconciliación manual de los elementos ambiguos, y comunícalo a quien use el pipeline.
Estos son patrones de diseño de aplicación, no garantías automáticas de SQLite.
Rank #4
Un elemento, una página o un lote: qué se repite tras una caída
| Granularidad | Trabajo que puede repetirse | Coste de commits | Cuándo encaja |
|---|---|---|---|
| Cada elemento | Un elemento | Más commits y más escrituras serializadas | Efectos externos caros o con riesgo |
| Página de entrada | Una página | Intermedio | Fuentes paginadas |
| Lote pequeño | Todo el lote en curso | Menos commits | Transformaciones locales baratas y repetibles |
Rollback journal frente a WAL
SQLite usa por omisión un rollback journal. El modo WAL (PRAGMA journal_mode=WAL) cambia la forma en que se registran los cambios y, en muchos casos, permite que lectores y un escritor avancen a la vez. Para un pipeline resulta útil cuando otro proceso o un panel de monitorización consulta el progreso mientras el pipeline escribe.
| Rollback journal | WAL | |
|---|---|---|
| Lectura y escritura simultáneas | Más limitadas | Lectores y escritor pueden progresar a la vez en muchos casos |
| Escritores | Uno a la vez | Uno a la vez: SQLite sigue serializando escritores |
| Entorno | Más permisivo | Todos los procesos en el mismo host; no sirve entre máquinas por sistema de archivos de red |
SQLITE_BUSY |
Posible | Sigue siendo posible en ciertos escenarios |
Por eso hay que configurar una espera (timeout o busy_timeout), limitar los reintentos y registrar si un fallo parece un bloqueo temporal o un error permanente. Si necesitas coordinar varios hosts, WAL de SQLite no ofrece esa coordinación: necesitas un servidor de base de datos o una cola diseñada para ello.
Checkpoints WAL: qué hacen y cuándo vigilarlos
Un checkpoint WAL copia cambios del WAL al archivo principal. Según la documentación de WAL de SQLite, por defecto se inicia un checkpoint automático cuando un COMMIT hace que el WAL alcance 1000 páginas. Es un valor predeterminado documentado, no una recomendación universal ni el resultado de un estudio.
Best Value
El problema real es que los lectores de larga duración pueden impedir que el checkpoint termine y que el WAL se reinicie, con lo que el archivo crece. Controla el tamaño del archivo -wal y la duración de las transacciones de lectura (cierra cursores y no dejes lecturas abiertas durante procesos largos).
| Modo | Comportamiento resumido | Úsalo si… |
|---|---|---|
PASSIVE |
Copia lo que puede sin esperar a lectores ni escritores | No toleras bloqueos; aceptas que quede trabajo pendiente |
FULL |
Espera a que no haya escritores y lectores que lo impidan, y completa el checkpoint | Puedes tolerar una breve espera |
RESTART |
Como FULL, y además espera a que los lectores usen ya el archivo principal para que el siguiente escritor reinicie el WAL |
Quieres que el WAL se reutilice desde el principio |
TRUNCATE |
Como RESTART, y además trunca el archivo WAL a cero bytes |
Necesitas recuperar espacio en disco, p. ej. al terminar la ejecución |
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
# devuelve (busy, páginas_en_wal, páginas_checkpointed); busy=1 indica que no terminó
Un patrón razonable: deja el checkpoint automático por defecto durante la ejecución y lanza un TRUNCATE al terminar, comprobando el valor busy. Ninguno de estos modos cambia qué pasos están «hechos»: eso lo dicen solo tus filas.
Respaldos de una base activa
No copies a ciegas el archivo de una base en uso: con WAL, parte de los datos confirmados puede estar aún en el archivo -wal. Dos alternativas:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
- Online Backup API, expuesta en Python como
Connection.backup(). VACUUM INTO 'copia.db', que genera una copia compacta.
import sqlite3
src = sqlite3.connect("pipeline.db")
dst = sqlite3.connect("pipeline-backup.db")
with dst:
src.backup(dst)
dst.close(); src.close()
Si prefieres copiar archivos, incluye el WAL y sigue un procedimiento consistente. En cualquier caso, prueba la restauración: abre la copia, ejecuta PRAGMA integrity_check y reanuda el pipeline con ella.
Durabilidad y velocidad
Cada commit durable implica sincronizar a disco, y eso es el coste dominante de guardar elemento a elemento. El ajuste PRAGMA synchronous cambia ese equilibrio; antes de relajarlo, consulta en la documentación de SQLite qué garantías pierdes ante un corte de energía frente a una caída del proceso. Para un checkpoint de aplicación, perder el último commit equivale a repetir esa unidad, así que decide con eso en mente.
Quick Recap
Lista de comprobación antes de ponerlo en producción
- La unidad reanudable está definida y tiene clave estable con restricción
UNIQUE. - Resultado y estado «completado» se escriben en la misma transacción.
- Los efectos externos usan idempotency key, deduplicación o reconciliación; los límites están documentados.
- La versión del pipeline se guarda y existe una política para estados antiguos.
- Hay espera y reintentos limitados para
SQLITE_BUSY, con registros que distinguen bloqueo temporal de fallo permanente. - Los lectores no dejan transacciones largas abiertas; se vigila el tamaño del
-wal. - Todos los procesos están en el mismo host.
- El respaldo usa Backup API o
VACUUM INTO, y la restauración se ha probado. - Probaste la reanudación matando el proceso (
kill -9) en mitad de un elemento y comprobaste que el resultado final es el esperado.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




