8. Persistencia, transacciones y concurrencia
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Las lecciones 1 a 7 abrían una base de datos, la consultaban y la cerraban. Una aplicación la mantiene abierta, escribe desde varios hilos y a veces se detiene en mal momento. Esta lección pregunta qué promete entonces LadybugDB, y comprueba cada promesa: en un script Cypher, en un script Bash que inicia y mata procesos de la CLI, y en un programa Java con varias conexiones e hilos.
| SQL Server | DuckDB (lección 8) | LadybugDB | |
|---|---|---|---|
| Escritores | muchos, con bloqueos de fila y de página; un escritor espera los bloqueos que necesita | muchos; una actualización de la misma fila falla al instante, una clave duplicada en el commit | una sola transacción de escritura en toda la base de datos; otra se rechaza al instante |
| Un lector durante una escritura | espera con READ COMMITTED, lee versiones de fila con SNAPSHOT |
lee una instantánea | lee una instantánea |
| Un error dentro de una transacción | con XACT_ABORT OFF, la mayoría de los errores solo deshacen la sentencia |
la transacción se aborta: las sentencias siguientes fallan, y COMMIT revierte |
la transacción se revierte, y las sentencias siguientes se ejecutan en auto-commit |
| El registro | el registro de transacciones | un archivo .wal, hasta el siguiente checkpoint |
un archivo .wal, hasta el siguiente checkpoint |
| Procesos | clientes de un servidor | un proceso de lectura-escritura, o varios de solo lectura | lo mismo, según la documentación |
Los tres scripts están en code/ladybugdb, y la CI compara su salida en los tres sistemas operativos:
cypher/08-transactions.cypher, concypher/expected/08-transactions.csv, como los scripts de las lecciones 1 a 4;files/files.sh, confiles/expected.txt: las líneas que empiezan por#dependen del sistema operativo, ycheck.shlas imprime sin compararlas;java/src/main/java/graph/Transactions.java, conjava/expected-transactions.txt, y la misma convención de#.
lbug < cypher/08-transactions.cypherbash files/files.shmvn -q -f java/pom.xml exec:java -Dexec.mainClass=graph.Transactionsfiles.sh necesita Bash, kill y la CLI 0.19.1 de la lección 7 como lbug19 para su última parte; en Windows, ejecútalo desde Git Bash, como hace la CI.
Auto-commit y transacciones manuales
Sección titulada «Auto-commit y transacciones manuales»Cada sentencia enviada sin BEGIN TRANSACTION es una transacción por sí sola (transacciones). Una sentencia que crea dos proyectos, el segundo con la clave primaria de GA.Core (líneas 6-9):
UNWIND ['New/Zero.csproj', 'Common/GA.Core/GA.Core.csproj'] AS pathCREATE (:Project {path: path, name: 'Zero'});MATCH (p:Project) RETURN count(*) AS projects;Error: Runtime exception: Found duplicated primary key value Common/GA.Core/GA.Core.csproj, which violates the uniqueness constraint of the primary key column.┌──────────┐│ projects ││ INT64 │├──────────┤│ 111 │└──────────┘Siguen los 111 proyectos de ga: Zero, creado antes del error, desapareció con el resto de la sentencia. Una transacción manual ve sus propios cambios, y ROLLBACK los deshace (líneas 11-16):
BEGIN TRANSACTION;CREATE (:Project {path: 'New/One.csproj', name: 'One'});MATCH (p:Project) RETURN count(*) AS inside;ROLLBACK;MATCH (p:Project) RETURN count(*) AS after_rollback;┌────────┐│ inside ││ INT64 │├────────┤│ 112 │└────────┘┌────────────────┐│ after_rollback ││ INT64 │├────────────────┤│ 111 │└────────────────┘Crear una tabla y cargarla también forman parte de la transacción, como dice la documentación del DDL y de COPY (líneas 18-24):
BEGIN TRANSACTION;CREATE NODE TABLE Package(name STRING PRIMARY KEY);COPY Package FROM (LOAD FROM 'data/ga/package_refs.csv' (HEADER = true) RETURN DISTINCT package);MATCH (k:Package) RETURN count(*) AS packages_inside;ROLLBACK;CALL show_tables() RETURN name ORDER BY name;┌─────────────────┐│ packages_inside ││ INT64 │├─────────────────┤│ 136 │└─────────────────┘┌─────────┐│ name ││ STRING │├─────────┤│ Project │└─────────┘136 paquetes dentro de la transacción; después del rollback, ninguna tabla Package, como con CREATE TABLE en una transacción de SQL Server o de DuckDB.
Un error termina la transacción
Sección titulada «Un error termina la transacción»Tres creaciones en una transacción, la segunda un duplicado, y luego ROLLBACK (líneas 26-32):
BEGIN TRANSACTION;CREATE (:Project {path: 'New/Two.csproj', name: 'Two'});CREATE (:Project {path: 'Common/GA.Core/GA.Core.csproj', name: 'Duplicate'});CREATE (:Project {path: 'New/Three.csproj', name: 'Three'});ROLLBACK;MATCH (p:Project) WHERE p.path STARTS WITH 'New/' RETURN p.name;Error: Runtime exception: Found duplicated primary key value Common/GA.Core/GA.Core.csproj, which violates the uniqueness constraint of the primary key column.Error: No active transaction for ROLLBACK.┌────────┐│ p.name ││ STRING │├────────┤│ Three │└────────┘Three está en la base de datos, después de un ROLLBACK. El error revirtió toda la transacción, Two incluido, sin decirlo; CREATE Three se ejecutó entonces solo, en auto-commit, y se confirmó; ROLLBACK no encontró ninguna transacción. Un script que confía en su ROLLBACK final, o en su COMMIT, conserva las sentencias que siguen al error y pierde las que lo preceden.
DuckDB se detiene en el mismo punto, pero rechaza las sentencias siguientes con Current transaction is aborted (please ROLLBACK): nada se ejecuta por accidente. SQL Server, con XACT_ABORT OFF, solo deshace la sentencia fallida y mantiene abierta la transacción. LadybugDB no hace ni lo uno ni lo otro.
Otros errores hacen lo mismo. Un BEGIN dentro de una transacción (líneas 34-39):
BEGIN TRANSACTION;MATCH (p:Project {name: 'Three'}) DELETE p;BEGIN TRANSACTION;COMMIT;MATCH (p:Project) WHERE p.path STARTS WITH 'New/' RETURN p.name;Error: Connection already has an active transaction. Cannot start a transaction within another one. For concurrent multiple transactions, please open other connections.Error: No active transaction for COMMIT.┌────────┐│ p.name ││ STRING │├────────┤│ Three │└────────┘La eliminación se deshace, y COMMIT no tiene nada que confirmar. CHECKPOINT dentro de una transacción también la termina (líneas 47-52):
Error: Found active transaction for CHECKPOINT.Error: No active transaction for COMMIT.┌────────┐│ p.name ││ STRING │├────────┤│ Three │└────────┘Five, creado antes del CHECKPOINT, se pierde. En mi máquina, un error de binder (una propiedad desconocida) y un error de catálogo (una tabla que no existe) dentro de una transacción se comportaron igual.
Un error no termina la transacción: una escritura en una transacción de solo lectura (líneas 41-45):
BEGIN TRANSACTION READ ONLY;MATCH (p:Project) RETURN count(*) AS read_only;CREATE (:Project {path: 'New/Four.csproj', name: 'Four'});COMMIT;┌───────────┐│ read_only ││ INT64 │├───────────┤│ 112 │└───────────┘Error: Can not execute a write query inside a read-only transaction.El COMMIT tiene éxito, en silencio: la transacción seguía abierta. El ejercicio 1 escribe una función que maneja ambos casos.
Dos conexiones, un escritor
Sección titulada «Dos conexiones, un escritor»El programa Java abre una base de datos en memoria y dos conexiones, first y second, sobre el mismo objeto Database: la concurrencia que la documentación considera segura. execute devuelve el primer valor de un resultado, ok, o el error, tanto si lo informa el resultado como si la llamada lanza una excepción (líneas 134-144). La primera sección (líneas 18-28):
print("first: BEGIN", execute(first, "BEGIN TRANSACTION"));print("first: CREATE One", execute(first, "CREATE (:Project {path: 'New/One.csproj', name: 'One'})"));print("first sees", execute(first, "MATCH (p:Project) RETURN count(*)"));print("second sees", execute(second, "MATCH (p:Project) RETURN count(*)"));print("second: CREATE Two", execute(second, "CREATE (:Project {path: 'New/Two.csproj', name: 'Two'})"));print("first: COMMIT", execute(first, "COMMIT"));print("second: CREATE Two", execute(second, "CREATE (:Project {path: 'New/Two.csproj', name: 'Two'})"));== 1. Two connections, one write transactionfirst: BEGIN: okfirst: CREATE One: okfirst sees: 112second sees: 111second: CREATE Two: error: Cannot start a new write transaction in the system. Only one write transaction at a time is allowed in the system.first: COMMIT: oksecond: CREATE Two: oksecond no ve el proyecto sin confirmar, y lee sin esperar. Su escritura se rechaza al instante: sin esperar el bloqueo, sin tiempo límite que configurar. Y el rechazo no depende de lo que tocan las transacciones: One y Two son nodos distintos, y en SQL Server, dos bloqueos de fila. La documentación lo dice: una transacción de escritura a la vez, en toda la base de datos.
Una transacción de solo lectura lee una instantánea, y no bloquea al escritor (líneas 30-38):
== 2. A read-only transaction reads a snapshotsecond: BEGIN READ ONLY: oksecond sees: 113first: CREATE Three: okfirst sees: 114second sees: 113second: COMMIT: oksecond sees: 114Como el aislamiento SNAPSHOT de SQL Server: el recuento se queda en 113 hasta el final de la transacción de solo lectura, mientras first confirma. El error de la sección 3 (líneas 40-46) se comporta desde Java como en la CLI:
== 3. An error ends the transactionfirst: BEGIN: okfirst: CREATE Four: okfirst: CREATE GA.Core again: error: Runtime exception: Found duplicated primary key value Common/GA.Core/GA.Core.csproj, which violates the uniqueness constraint of the primary key column.first: CREATE Five: okfirst: ROLLBACK: error: No active transaction for ROLLBACK.new projects: [Five,One,Three,Two]Four se pierde, Five se confirma.
Un BEGIN rechazado
Sección titulada «Un BEGIN rechazado»El rechazo de arriba era una sentencia en auto-commit, y second siguió con normalidad. Cuando la sentencia rechazada es BEGIN TRANSACTION, la conexión se rompe. El programa ejecuta ese caso en un modo propio, refused-begin (líneas 110-124), porque el programa se detiene ahí en dos de los tres runners:
print("first: BEGIN", execute(first, "BEGIN TRANSACTION"));print("second: BEGIN", execute(second, "BEGIN TRANSACTION"));print("first: COMMIT", execute(first, "COMMIT"));System.out.flush();print("second: MATCH", execute(second, "MATCH (p:Project) RETURN count(*)"));print("second: BEGIN", execute(second, "BEGIN TRANSACTION"));print("second: MATCH", execute(second, "MATCH (p:Project) RETURN count(*)"));print("second: COMMIT", execute(second, "COMMIT"));first: BEGIN: oksecond: BEGIN: error: Cannot start a new write transaction in the system. Only one write transaction at a time is allowed in the system.first: COMMIT: oksecond: MATCH: thrown RuntimeException: Unknown Errorsecond: BEGIN: oksecond: MATCH: 0second: COMMIT: okUna consulta que debería devolver 0 lanza una excepción sin mensaje: el código nativo lanzó algo que no es una excepción de LadybugDB, y el catch (...) del código JNI lo convierte en Unknown Error (lbug_java.cpp, líneas 751-752). Un BEGIN que tiene éxito repara la conexión.
first: BEGIN: oksecond: BEGIN: error: Cannot start a new write transaction in the system. Only one write transaction at a time is allowed in the system.first: COMMIT: ok# A fatal error has been detected by the Java Runtime Environment:# SIGSEGV (0xb) at pc=0x00007f53bb338b98, pid=3375, tid=3448# C [liblbug_java_native3142814795411884706.so+0xb38b98] lbug::catalog::CatalogSet::traverseVersionChainsForTransactionNoLock(lbug::transaction::Transaction const*, lbug::catalog::CatalogEntry*)+0x8exit code of mvn: 134La JVM cae en la biblioteca nativa, mientras el catálogo busca la tabla con una transacción que ya no existe. Ninguna excepción que capturar: el proceso termina.
first: BEGIN: oksecond: BEGIN: error: Cannot start a new write transaction in the system. Only one write transaction at a time is allowed in the system.first: COMMIT: ok# A fatal error has been detected by the Java Runtime Environment:# SIGSEGV (0xb) at pc=0x000000012d64409c, pid=1392, tid=42755# C [liblbug_java_native8035757179035443103.so+0xdc09c] lbug::catalog::CatalogSet::containsEntry(lbug::transaction::Transaction const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&)+0x50exit code of mvn: 134La JVM cae en la biblioteca nativa, en otra función del catálogo que en Linux. Ninguna excepción que capturar: el proceso termina.
Estos son los logs de CI del commit 93f6f82. En un servidor, un BEGIN rechazado seguido de una consulta tumba todo el proceso en Linux. La regla que esto sugiere, y que sigue el ejercicio 2: después de un BEGIN rechazado, la siguiente sentencia en esa conexión es otro BEGIN, o se cierra la conexión.
El archivo de base de datos y su registro de escritura anticipada
Sección titulada «El archivo de base de datos y su registro de escritura anticipada»El resto de la lección trata de archivos y procesos, y files.sh controla la CLI. Una CLI de solo lectura no puede crear una base de datos (líneas 33-35), y una de lectura-escritura que carga ga y sale deja un solo archivo (líneas 37-39):
== 1. A read-only CLI can't create a fileCannot create an empty database under READ ONLY mode.files:== 2. Create and load, then closefiles: ga.lbdbLuego una CLI que confirma una creación y se queda abierta, leyendo su entrada de un comando que espera 20 segundos (líneas 41-52):
lbug --no_progress_bar --no_stats --mode csv "$D/ga.lbdb" > "$D/writer.txt" 2>&1 < <( echo "CREATE (:Project {path: 'New/One.csproj', name: 'One'});" sleep 20) &writer=$!# El commit escribe el WALwait_for "$D/ga.lbdb.wal"== 3. A process that stays open after a commitfiles: ga.lbdb ga.lbdb.walEl commit escribió ga.lbdb.wal, el registro de escritura anticipada (write-ahead log). El script espera ese archivo en lugar de la salida de la CLI: con su salida redirigida a un archivo, la CLI no escribe nada hasta que sale. La página de archivos en disco describe el registro tal como lo encontró la lección 8 de DuckDB: lo crea una transacción de escritura confirmada, y se elimina cuando un checkpoint tiene éxito. El checkpoint se ejecuta con CHECKPOINT, al cerrar la base de datos, y al final de una transacción de escritura cuando el registro supera checkpoint_threshold, que lee el script Cypher (línea 53): 16777216 bytes, 16 MiB, lo mismo que DuckDB.
flowchart TB T[Transacción de escritura] -->|COMMIT| L[ga.lbdb.wal] L -->|"checkpoint: CHECKPOINT, cierre,<br/>registro por encima de checkpoint_threshold"| F[(ga.lbdb)] L -->|"proceso matado:<br/>se reproduce en la siguiente apertura"| F T -->|"ROLLBACK, error, cierre,<br/>proceso matado"| N[nada escrito]Un proceso matado
Sección titulada «Un proceso matado»kill -9 sobre el escritor, y luego una CLI nueva sobre el archivo (líneas 54-59):
== 4. The same process, killedfiles: ga.lbdb ga.lbdb.walprojects112files: ga.lbdbEl proceso matado dejó su registro. El siguiente lo volvió a aplicar al abrir el archivo, One está ahí, e hizo un checkpoint al cerrarse: no queda registro. Una copia de ga.lbdb solo, hecha mientras existe el registro, pierde los cambios confirmados; copia ambos archivos, o ejecuta antes CHECKPOINT.
Un proceso matado dentro de una transacción, después de su CREATE pero antes de cualquier COMMIT (líneas 61-72):
== 5. Killed inside a transactionfiles: ga.lbdbp.nameOneNingún registro: una transacción sin confirmar no escribe nada en disco, así que nada muestra que está en marcha, y el script solo puede esperar 5 segundos. Two se pierde, como promete la documentación. El programa Java cierra un Database cuya conexión tiene una transacción abierta (líneas 92-102):
== 6. A database filefiles: [ga.lbdb, ga.lbdb.wal]BEGIN: okCREATE One: okfiles after close: [ga.lbdb]reopened: [GA.Core]Cerrar revierte: One no está en el archivo reabierto.
Dos procesos sobre un archivo
Sección titulada «Dos procesos sobre un archivo»Mientras el escritor de la sección 3 está abierto, una segunda CLI abre el archivo en lectura-escritura, y luego otra en solo lectura (líneas 49-52):
-- a second process, read-writeIO exception: Could not set lock on file : out/files/ga.lbdbSee the docs: https://docs.ladybugdb.com/concurrency for more information.-- a second process, read-onlyEl proceso de lectura-escritura se rechaza en los tres sistemas operativos, con un código de error del sistema entre paréntesis que el script elimina. El de solo lectura depende del sistema operativo:
# IO exception: Cannot read from file: out/files/ga.lbdb numBytesRead: 0 numBytesToRead: 4096 position: 0. Error 33: The process cannot access the file because another process has locked a portion of the file.Rechazado, pero no por un bloqueo propio: la lectura falla. El escritor bloqueó el primer byte del archivo con LockFileEx (local_file_system.cpp, líneas 118-133), y Windows niega la lectura de una región bloqueada a cualquier otro handle.
# projects# 112Aceptado, y ve 112 proyectos: One, que solo está en el registro. El bloqueo del escritor es un bloqueo fcntl, consultivo: solo detiene a los procesos que piden un bloqueo.
# projects# 112Aceptado, y ve 112 proyectos: One, que solo está en el registro. El bloqueo del escritor es un bloqueo fcntl, consultivo: solo detiene a los procesos que piden un bloqueo.
Una base de datos de solo lectura no pide ninguno. El gestor de almacenamiento solo añade el flag de bloqueo a las aperturas en lectura-escritura (storage_manager.cpp, líneas 87-91). Así que el orden inverso funciona en todos los sistemas operativos: primero una CLI de solo lectura, luego un escritor que elimina GA.Core, crea 1.000 proyectos y hace un checkpoint, mientras el lector espera 8 segundos entre dos recuentos (líneas 74-90):
lbug --no_progress_bar --no_stats --mode csv --read_only "$D/ga.lbdb" > "$D/reader.txt" 2>&1 < <( echo "$COUNT" sleep 8 echo "$COUNT" echo "MATCH (p:Project {name: 'GA.Core'}) RETURN count(*) AS ga_core;") &reader=$!sleep 3q "$D/ga.lbdb" <<EOFMATCH (p:Project {name: 'GA.Core'}) DETACH DELETE p;UNWIND range(1, 1000) AS i CREATE (:Project {path: 'Generated/' + string(i), name: 'Generated' + string(i)});CHECKPOINT;$COUNTEOFprojects1111-- what the reader sawprojects112projects112ga_core1El escritor confirmó 1.111 proyectos y los llevó al archivo con un checkpoint; el lector, todavía abierto, cuenta 112 y sigue encontrando GA.Core. La página de concurrencia explica por qué no es seguro, cada Database guarda en caché datos que otro no puede refrescar, y dice que LadybugDB no lo permitirá. Lo permite, en Windows, Linux y macOS: el lector responde desde un estado que ya no existe. Si devuelve datos incorrectos, en lugar de datos antiguos, cuando lee páginas que no tenía en caché antes del checkpoint está por verificar.
El mensaje Could not set lock tiene una variante para Linux y macOS, Lock is held by PID …, que nunca aparece: el código cierra el descriptor de archivo, y luego pregunta a fcntl quién tiene el bloqueo sobre ese descriptor cerrado, lo que falla (líneas 147-172). El mensaje de DuckDB nombra el proceso; el de LadybugDB no.
Dentro de un proceso, el programa Java abre un segundo Database sobre el archivo mientras el primero está abierto (líneas 96-97 y 103-106):
# a second read-write Database, same process: java.lang.Exception: IO exception: Could not set lock on file : out\java-files\ga.lbdb (Error: 33)# a read-only Database, same process: java.lang.Exception: IO exception: Cannot read from file: out\java-files\ga.lbdb handle: 1764 numBytesRead: 0 numBytesToRead: 4096 position: 0. Error 33: The process cannot access the file because another process has locked a portion of the file.# a read-only Database, same process: [GA.Core]# a read-only Database, then a read-write one: [GA.Core]# a read-only Database, same process: [GA.Core]# a read-only Database, then a read-write one: [GA.Core]En Linux y macOS, los bloqueos fcntl pertenecen al proceso: un segundo bloqueo desde el mismo proceso tiene éxito, y dos objetos Database de lectura-escritura funcionan sobre un archivo, con dos cachés. En Windows, los bloqueos LockFileEx pertenecen al handle, y el segundo Database falla. Un Database de solo lectura seguido de uno de lectura-escritura se abre en todas partes. La regla que el bloqueo no impone, entonces: un Database por archivo por proceso, compartido por todos los hilos, y ningún otro proceso sobre el archivo mientras escribe.
new Database(path) no declara ninguna excepción, pero lanza java.lang.Exception, una excepción comprobada, cuando el archivo no se puede abrir: el código JNI la lanza por su clase (lbug_java.cpp, líneas 618-636), cosa que el compilador de Java no puede ver. catch (IOException e) no la captura, y catch (Exception e) sí; el open del programa la captura así (líneas 184-191).
EXPORT DATABASE e IMPORT DATABASE
Sección titulada «EXPORT DATABASE e IMPORT DATABASE»Una macro, luego una exportación a archivos CSV, y una importación en un archivo nuevo (líneas 92-107):
CREATE MACRO major(version) AS split_part(version, '.', 1);EXPORT DATABASE 'out/files/export' (format = 'csv', header = true);export: Project.csv REFERENCES_Project_Project.csv copy.cypher index.cypher schema.cypherCREATE NODE TABLE `Project` (`path` STRING,`name` STRING,`language` STRING,`sdk` STRING,`frameworks` STRING,`in_solution` BOOL, PRIMARY KEY(`path`));CREATE REL TABLE `REFERENCES` (FROM `Project` TO `Project`, MANY_MANY);CREATE MACRO `MAJOR` (version) AS split_part(version, '.', 1);a.path,a.name,a.language,a.sdk,a.frameworks,a.in_solutionUn archivo de datos por tabla, y tres scripts Cypher. La página de migración enumera un archivo macro.cypher; la macro está en schema.cypher, después de las tablas, con su nombre en mayúsculas. index.cypher está vacío: la base de datos no tiene más índices que las claves primarias, que declara schema.cypher. La cabecera CSV de Project.csv tiene un prefijo a. en cada columna, que no molesta a la importación de abajo. copy.cypher enumera las columnas y las opciones de COPY, en un orden que cambia de un sistema operativo a otro:
# COPY `Project` (`path`,`name`,`language`,`sdk`,`frameworks`,`in_solution`) FROM "Project.csv" (parallel=true, header=true);# COPY `REFERENCES` FROM "REFERENCES_Project_Project.csv" (parallel=true, header=true, from='Project', to='Project');# COPY `Project` (`path`,`name`,`language`,`sdk`,`frameworks`,`in_solution`) FROM "Project.csv" (header=true, parallel=true);# COPY `REFERENCES` FROM "REFERENCES_Project_Project.csv" (to='Project', from='Project', header=true, parallel=true);# COPY `Project` (`path`,`name`,`language`,`sdk`,`frameworks`,`in_solution`) FROM "Project.csv" (header=true, parallel=true);# COPY `REFERENCES` FROM "REFERENCES_Project_Project.csv" (to='Project', from='Project', header=true, parallel=true);La importación, y luego una importación en la base de datos que se exportó:
IMPORT DATABASE 'out/files/export';MATCH (p:Project) RETURN count(*) AS projects;MATCH ()-[r:REFERENCES]->() RETURN count(*) AS references;RETURN major('10.0.1') AS major;resultImported database successfully.projects1111references233major10-- into a database that isn't emptyError: Runtime exception: Import database failed: Binder exception: Project already exists in catalog.1.111 proyectos y 233 referencias: las 265 de la lección 7, menos las 32 que apuntaban a GA.Core, eliminado con DETACH DELETE. La macro funciona en el archivo nuevo. IMPORT DATABASE exige una base de datos vacía, como dice la documentación, que también advierte que una importación fallida no se revierte: borra el archivo y vuelve a empezar.
Versiones de almacenamiento
Sección titulada «Versiones de almacenamiento»Un archivo creado por la CLI 0.19.1, y luego abierto por la 0.20.4 y de nuevo por la 0.19.1 (líneas 109-123):
== 8. A file of LadybugDB 0.19.1, opened by 0.20.4-- 0.20.4, read-onlyprojects1-- 0.19.1projects1-- 0.20.4, read-writeprojects1-- 0.19.1 againRuntime exception: Trying to read a database file with a different version. Database file version: 47, Current build storage version: 43La 0.20.4 lee el archivo antiguo, en solo lectura sin modificarlo. En lectura-escritura, actualiza el archivo de la versión de almacenamiento 43 a la 47 sin decir nada, y la 0.19.1 ya no puede abrirlo. La lección 5 encontró lo mismo con el paquete NuGet 0.19.1, cuya excepción no daba la causa; la CLI 0.19.1 sí la da. El consejo de la lección 5, lbug --read_only sobre los archivos de una aplicación, protege contra ello. Para volver atrás, o entre versiones que no pueden leerse entre sí, EXPORT DATABASE con una versión e IMPORT DATABASE con la otra.
Puntos clave
Sección titulada «Puntos clave»- Cada sentencia es una transacción; el DDL y
COPYdentro de una transacción manual se revierten con ella. - Un error dentro de una transacción manual la revierte y la termina, en silencio: las sentencias siguientes se ejecutan en auto-commit, y
ROLLBACKoCOMMITinformanNo active transaction. Detente en el primer error. Una escritura rechazada por una transacción de solo lectura es la excepción: la transacción sigue abierta. - Una sola transacción de escritura en toda la base de datos: una segunda se rechaza al instante, toque lo que toque. Los lectores ven una instantánea y no bloquean al escritor.
- Desde Java, un
BEGINrechazado rompe la conexión:Unknown Erroren Windows, una caída de la JVM en Linux y macOS en la siguiente consulta. Reintenta elBEGIN, o cierra la conexión. - Los commits van a
.walhasta un checkpoint; un proceso matado deja el registro, y la siguiente apertura lo vuelve a aplicar. Una transacción sin confirmar no deja nada. - El bloqueo de lectura-escritura no detiene a un proceso de solo lectura en Linux y macOS, y nada detiene a un escritor después de un proceso de solo lectura: el lector responde entonces desde un estado antiguo. Dentro de un proceso, Linux y macOS aceptan dos objetos
Databasede lectura-escritura sobre un archivo. Mantén un soloDatabasepor archivo, en un solo proceso. - Una apertura en lectura-escritura con una versión más reciente actualiza el archivo en silencio;
EXPORT DATABASEeIMPORT DATABASEmueven los datos entre versiones.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en Transactions.java, secciones 4 y 5, y su salida en java/expected-transactions.txt: la CI las comprueba con el resto.
- Escribe
String transaction(Connection connection, List<String> statements): ejecuta las sentencias en una transacción y las confirma todas, o ninguna. Devuelveok, o el primer error. Pruébala con una lista cuya segunda sentencia falla, luego con una lista válida, y muestra qué proyectos existen.
Solución
// Ejercicio 1: todas las sentencias o ninguna. Ante un fallo, se detiene antes de la siguiente sentencia, que se ejecutaría en auto-commitstatic String transaction(Connection connection, List<String> statements) { var begin = execute(connection, "BEGIN TRANSACTION"); if (!begin.equals("ok")) { return begin; } for (var statement : statements) { var outcome = execute(connection, statement); if (outcome.startsWith("error") || outcome.startsWith("thrown")) { // El motor ya ha revertido; ROLLBACK es para los errores que dejan la transacción abierta execute(connection, "ROLLBACK"); return outcome; } } return execute(connection, "COMMIT");}== 4. Exercise 1: a transaction that stops at the first errortransaction: error: Runtime exception: Found duplicated primary key value Common/GA.Core/GA.Core.csproj, which violates the uniqueness constraint of the primary key column.transaction: oknew projects: [Five,One,Seven,Six,Three,Two]La primera llamada crea Six, falla en GA.Core y se detiene: Seven nunca se ejecuta, y el motor revierte Six. La segunda llamada crea ambos. Detenerse es precisamente la idea: un bucle que siguiera confirmaría Seven solo. El ROLLBACK después de un error suele informar No active transaction, y su resultado se ignora; está ahí para los errores que dejan la transacción abierta, como una escritura en una transacción de solo lectura. La prueba de error también cubre thrown: una excepción del código JNI, como Unknown Error, es un error.
- Ocho hilos, cada uno con su propia conexión sobre el mismo
Database, crean 25 proyectos cada uno, una transacción por proyecto, contransactiondel ejercicio 1. EscribewithRetry, que reintenta una transacción rechazada porque otra está escribiendo, y comprueba que los 200 proyectos están ahí.
Solución
Líneas 163-176, y los hilos en las líneas 58-80:
// Ejercicio 2: reintentar una transacción que el motor rechaza porque otra está escribiendostatic int withRetry(Callable<String> transaction, AtomicInteger failures) throws Exception { for (int retries = 0; ; retries++) { var outcome = transaction.call(); if (!outcome.contains("Only one write transaction at a time")) { if (!outcome.equals("ok")) { throw new IllegalStateException(outcome); } return retries; } failures.incrementAndGet(); Thread.sleep(1 + ThreadLocalRandom.current().nextInt(5)); }}== 5. Exercise 2: eight threads that writethread projects: 200Los 200 proyectos, en los tres runners. El número de rechazos no se compara: 153 en Windows, 155 en Linux y 24 en macOS en la ejecución de CI de 93f6f82. Una pausa aleatoria de 1 a 5 milisegundos evita que los hilos reintenten al unísono. Cualquier otro error termina el hilo con una excepción, en lugar de reintentarse indefinidamente.
Los rechazos vienen de BEGIN TRANSACTION, y la siguiente sentencia en la conexión es el BEGIN del reintento: el patrón que sobrevive al bug del BEGIN rechazado en los tres sistemas operativos. ExecutorService es AutoCloseable desde Java 19: el try-with-resources espera a las ocho tareas, e invokeAll devuelve sus recuentos de reintentos.
Fuentes
Sección titulada «Fuentes»- Documentación de LadybugDB: transacciones, conexiones y concurrencia, archivos en disco, configuración, migrar una base de datos
- Código fuente de LadybugDB 0.20.4:
storage_manager.cppylocal_file_system.cpp;lbug_java.cppdeladybug-javaen el commitf2fb39f LockFileExyfcntlde POSIX- SQL Server:
SET XACT_ABORT, bloqueos y versionado de filas