Hay un abismo entre "la base de datos funciona" y "la base de datos está lista para producción". De un lado: CRUD correcto, pruebas pasando, funcionalidades que funcionan en desarrollo. Del otro lado: integridad de datos ante pérdida de energía, protección contra acceso concurrente, eficiencia de almacenamiento a escala y bases de datos autodescriptivas que pueden recuperarse sin el código fuente original.
La Sesión 308 cerró ese abismo. Seis funcionalidades de endurecimiento, cada una abordando un modo de fallo de producción específico. Checksums CRC-32 para detectar corrupción del WAL. Auto-checkpointing para prevenir crecimiento ilimitado del WAL. Bloqueo de archivos para prevenir corrupción por procesos concurrentes. Archivos de datos por tipo de entidad para eliminar cuellos de botella del sistema de archivos. Deduplicación de historial del WAL para prevenir crecimiento cuadrático del almacenamiento. Persistencia de esquemas para hacer la base de datos autodescriptiva.
Esta es la sesión que transformó FlinDB de "funciona en mi portátil" a "desplegar en producción".
Checksums CRC-32: detectando corrupción
El problema
La corrupción de disco, pérdida de energía y escrituras incompletas pueden dejar el WAL con entradas ilegibles. Sin verificaciones de integridad, reproducir un WAL corrupto podría insertar silenciosamente datos incorrectos en la base de datos. La base de datos parecería funcionar normalmente, pero algunos registros contendrían valores basura -- el peor tipo de bug, porque es invisible hasta que alguien lee los datos corruptos.
La solución
Cada entrada del WAL ahora tiene un prefijo con un checksum CRC-32:
CRC:a1b2c3d4\t{"op":"Save","entity_type":"Todo","data":{...}}
CRC:e5f6a7b8\t{"op":"Delete","entity_type":"Todo","id":42}El formato es:
CRC:{hex_checksum}\t{json_payload}El checksum se calcula sobre los bytes JSON crudos (todo después del carácter de tabulación). Durante la reproducción del WAL, cada entrada se verifica:
Read WAL line
|
+-- Starts with "CRC:" ?
| +-- Yes: Extract checksum + payload
| | Compute CRC-32 of payload
| | Match? --> Apply entry
| | Mismatch? --> Log WARNING, skip entry
| |
| +-- No: Parse as legacy plain JSON (backward-compatible)
|
+-- Empty/unparseable: SkipLa implementación usa el crate Rust crc32fast, que aprovecha instrucciones SSE 4.2 aceleradas por hardware en CPUs compatibles. La sobrecarga de rendimiento es insignificante -- CRC-32 funciona a velocidades de ancho de banda de memoria, añadiendo microsegundos por entrada del WAL.
Compatibilidad retroactiva
Las entradas escritas por versiones anteriores de FLIN (sin el prefijo CRC:) se parsean como JSON plano heredado. Esto significa que actualizar a la versión endurecida no requiere migración. Las entradas antiguas se reproducen normalmente. Las nuevas entradas se escriben con checksums. Después del primer checkpoint, el WAL se trunca y todas las entradas futuras usan checksums.
El contador de corrupción
ZeroCore rastrea el número total de discrepancias CRC encontradas durante la recuperación. Si es distinto de cero, el servidor registra una advertencia al inicio:
WARNING: 3 WAL entries skipped due to CRC mismatch (possible disk corruption)Este es un enfoque de seguridad ante fallos. Las entradas corruptas se omiten en lugar de causar un error fatal. La base de datos recupera tantos datos como sea posible, reporta cuántas entradas se perdieron y continúa operando. En la práctica, una sola entrada corrupta en un WAL con miles de entradas significa que la base de datos pierde una mutación -- no todo el conjunto de datos.
Auto-checkpointing: limitando el crecimiento del WAL
El problema
Sin checkpointing, el WAL crece indefinidamente. Una aplicación ocupada escribiendo cientos de registros por hora acumula un archivo WAL que toma cada vez más tiempo reproducir en la recuperación, consume espacio en disco ilimitado y hace que los archivos de respaldo sean innecesariamente grandes.
La solución
Auto-checkpointing con dos umbrales configurables:
| Umbral | Predeterminado | Variable de entorno |
|---|---|---|
| Conteo de entradas | 1.000 | FLIN_DB_MAX_WAL_ENTRIES |
| Tamaño en bytes | 10 MB | FLIN_DB_MAX_WAL_BYTES |
El primer umbral que se alcance dispara el checkpoint:
App writes --> WAL entry appended --> Check thresholds
|
entries >= 1000 OR bytes >= 10MB?
|
Yes
|
CHECKPOINT:
1. Write data/ files
2. Write schema.flindb
3. Truncate WALAl apagar limpiamente el servidor (Ctrl+C o SIGTERM), ZeroCore realiza un checkpoint final independientemente de los umbrales. Esto asegura que los archivos de datos siempre estén actualizados cuando el servidor se detiene de forma elegante.
Bloqueo de archivos: previniendo corrupción concurrente
El problema
Ejecutar dos servidores de desarrollo FLIN contra el mismo directorio .flindb/ simultáneamente causaría que ambos procesos escriban al mismo WAL. Entradas entrelazadas, corrupción de datos y comportamiento impredecible seguirían.
La solución
Una estructura DbLock adquiere un bloqueo exclusivo de archivo en .flindb/lock al inicio:
Server Start
|
+-- Create/open .flindb/lock
+-- Acquire exclusive file lock (fs2 crate)
| +-- Success: Write PID to lock file, continue
| +-- Failure: "Database locked by another process" error, exit
|
Server Running (lock held)
|
Server Stop
|
+-- Lock released automatically (Rust Drop trait)El bloqueo usa el crate fs2 para compatibilidad multiplataforma (Windows, macOS, Linux). El bloqueo es exclusivo -- sin bloqueos compartidos/de lectura. El PID se escribe en el archivo de bloqueo para depuración ("¿qué proceso tiene el bloqueo?").
Manejo de bloqueos obsoletos: Si un servidor se cae sin apagado limpio, el SO libera automáticamente el bloqueo de archivo. El siguiente inicio del servidor adquiere el bloqueo normalmente -- sin intervención manual necesaria.
Archivos de datos por tipo de entidad
El problema
El formato de almacenamiento antiguo creaba un archivo JSON por registro:
.flindb/data/
+-- Todo_1.json
+-- Todo_2.json
+-- Todo_3.json
+-- ...
+-- Todo_847.jsonCon una aplicación ocupada, esto significa miles de archivos pequeños. El listado del directorio se vuelve lento. El uso de inodos del sistema de archivos aumenta. Las herramientas de respaldo luchan con muchos archivos pequeños. La recuperación requiere leer y parsear cientos o miles de archivos individuales.
La solución
Todos los registros de un tipo de entidad se consolidan en un solo archivo .flindb:
.flindb/data/
+-- Todo.flindb # All 847 Todo records
+-- User.flindb # All 203 User records
+-- ChatMessage.flindb # All ChatMessage recordsLa mejora
| Métrica | Formato antiguo | Formato nuevo |
|---|---|---|
| Archivos para 1.000 Todos | 1.000 archivos | 1 archivo |
| Listado de directorio | Lento (miles de entradas) | Rápido (uno por tipo de entidad) |
| Eficiencia de respaldo | Muchos archivos pequeños | Pocos archivos grandes |
| Sobrecarga del sistema de archivos | Alta (inodo por registro) | Mínima |
| Velocidad de recuperación | Leer + parsear 1.000 archivos | Leer + parsear 1 archivo |
Deduplicación de historial del WAL
El problema
Antes de la Sesión 308, cada entrada Save del WAL incluía el historial completo de versiones de la entidad. Para una entidad actualizada 100 veces, la entrada de guardado 101 incluía las 100 versiones anteriores. El crecimiento del WAL era cuadrático.
La solución
Las entradas Save del WAL ya no incluyen el array de historial. Solo los datos actuales se escriben al WAL. El historial completo se reconstruye durante el checkpoint a partir de la secuencia de entradas del WAL.
Esto cambia el crecimiento del WAL de cuadrático a lineal. Una reducción de 50x para una entidad actualizada 100 veces.
Persistencia de esquemas
El problema
Antes de la Sesión 308, la base de datos no era autodescriptiva. Si perdías los archivos fuente FLIN, no podías interpretar los datos en .flindb/ -- los nombres de campos, tipos, validadores y restricciones solo estaban en el código fuente.
La solución
schema.flindb persiste el esquema completo de entidades junto a los datos. Esto es análogo a la tabla sqlite_master de SQLite, que almacena las sentencias CREATE TABLE que definen el esquema. El schema.flindb de FlinDB sirve el mismo propósito -- hacer la base de datos autodescriptiva y recuperable sin información externa.
Principios de diseño
Tres principios guiaron el trabajo de endurecimiento:
Configuración cero. Cada funcionalidad de endurecimiento tiene valores predeterminados sensatos. Los checksums CRC siempre están activados. El auto-checkpointing usa umbrales razonables. El bloqueo de archivos sucede automáticamente. El desarrollador no necesita habilitar ninguna de estas funcionalidades -- son el comportamiento predeterminado.
Compatible retroactivamente. El código endurecido lee formatos heredados y auto-migra. Las entradas WAL antiguas sin prefijos CRC se parsean normalmente. Los archivos JSON antiguos por registro se leen durante la recuperación. Después del primer checkpoint, todo está en el formato nuevo. Sin paso de migración manual.
Seguridad ante fallos. Las entradas WAL corruptas se omiten, no son fatales. Una entrada corrupta registra una advertencia y continúa. Un archivo de bloqueo obsoleto no previene el reinicio. Un archivo de esquema faltante dispara derivación de esquema desde las declaraciones de entidades. La base de datos recupera tantos datos como sea posible y sigue ejecutándose.
Estos principios reflejan la realidad de los entornos de producción. Se perderá la energía. Los discos corromperán bits. Los procesos se caerán sin apagado limpio. Los desarrolladores actualizarán sin leer el changelog. Una base de datos de producción debe manejar todo esto con gracia, sin pérdida de datos y sin requerir intervención manual.
Esta es la Parte 13 de la serie "Cómo construimos FlinDB", documentando cómo construimos un motor de base de datos embebido completo para el lenguaje de programación FLIN.
Navegación de la serie: - [066] Database Encryption and Configuration - [067] Tree Traversal and Integration Testing - [068] FlinDB Hardening for Production (estás aquí) - [069] FlinDB vs SQLite: Why We Built Our Own - [070] Persistence in the Browser