1 puntos por GN⁺ 2024-05-17 | 1 comentarios | Compartir por WhatsApp
  • En las pruebas, Datomic Pro 1.0.7075 pareció ofrecer una seguridad entre transacciones más fuerte de lo que afirmaba la documentación, pero la semántica dentro de la transacción difería mucho del modelo habitual de ejecución serial
  • Todo el historial de pruebas pareció ser Serializable, una sola sesión de peer fue Strong Session Serializable, y las escrituras junto con lecturas mediante d/sync estuvieron cerca de Strong Serializable
  • Las funciones add, retract y transaction function de Datomic no se ejecutan de forma acumulativa y secuencial dentro de una transacción; en cambio, cada función opera viendo solo el estado de la BD al momento de inicio
  • Si se combinan en la misma transacción transaction functions como approve y deny, que por separado son seguras, el resultado compuesto puede producir una violación de invariantes
  • Al poner varias transaction functions en una sola transacción, hay que revisar la relación entre read set y write set, y usar junto con ello restricciones explícitas como entity predicate, attribute predicate y entity spec

Modelo y arquitectura de Datomic Pro

  • Datomic es una base de datos OLTP Entity-Attribute-Value que modela explícitamente el concepto de tiempo
    • El estado de la BD en un momento específico se representa como un conjunto de datoms con forma [entity, attribute, value]
    • En cada datom también se conserva qué transacción lo agregó o lo retractó
    • El conjunto completo de datoms es una 5-tupla con forma [entity, attribute, value, transaction, asserted-or-retracted?]
  • Datomic es una temporal database, por lo que permite pedir snapshots no solo del presente sino también de una vista lógica pasada o de un momento según wall-clock time
    • También se puede consultar en una vista de historial completa si cierto hecho existió en el pasado
    • Ofrece una API estilo Datalog, una API de recorrido de grafos y el tipo Entity estilo ODM como formas de consulta
  • Datomic Pro es la versión que los usuarios pueden operar por su cuenta, mientras que Datomic Cloud corre en AWS y tiene una arquitectura algo distinta
  • Datomic Pro está compuesto por varios elementos que cooperan entre sí
    • El Transactor se encarga de ejecutar transacciones de escritura, mantener índices y registrar datos en el almacenamiento
    • El Peer es un cliente pesado que integra una librería de JVM y se encarga de enviar transacciones, consultar lecturas al almacenamiento y mantener caché
    • Para aplicaciones en otros lenguajes también ofrece un modelo cliente-servidor basado en thin client y peer server
  • Internamente, Datomic agrega cada transacción a un log en orden temporal y mantiene cuatro índices ordenados según combinaciones de entity, attribute, value y time
    • El log y los índices se almacenan como árboles persistentes e inmutables en sistemas de almacenamiento como Cassandra o DynamoDB
    • Como los nodos del árbol son inmutables, el almacenamiento subyacente solo necesita garantizar eventual consistency
    • Al hacer commit, el transactor guarda nuevos nodos inmutables del árbol y luego avanza el puntero raíz con compare-and-set (CaS); este CaS requiere sequential consistency
  • El CaS secuencial garantiza el orden global de las transacciones, pero ata el throughput de escritura a la velocidad de un único transactor
    • Normalmente, Datomic mantiene solo un transactor activo a la vez y despliega varios transactors para tolerancia a fallos
    • Los peers se conectan directamente al almacenamiento y al transactor, y cada uno mantiene su propia copia monotónicamente creciente del puntero raíz
    • Como las lecturas pueden usar caché de nodos de árbol inmutables, aumentar la cantidad de peers permite una escalabilidad de lectura casi lineal

Modelo de transacciones de Datomic

  • Datomic no ofrece interactive transaction como una base de datos OLTP convencional
    • No sigue el modelo de iniciar una transacción, recibir el resultado de una operación, enviar la siguiente y finalmente hacer commit
    • Tiene transaction functions parecidas a stored procedures, pero no pueden devolver valores arbitrarios al llamador
  • Las rutas de lectura y escritura están estrictamente separadas
    • db devuelve el estado más reciente de la BD que conoce el peer
    • d/sync sincroniza con el transactor para obtener el estado más reciente entre todos los peers o el estado posterior a un momento específico
    • d/as-of obtiene el estado de la BD en un momento pasado
    • Como el estado de la BD es inmutable, varias consultas sobre el mismo estado se ejecutan exactamente en la misma vista lógica
  • Las transacciones de escritura se representan como una lista ordenada de operaciones
    • Los ejemplos incluyen :db/add, :db/retract, db/cas y llamadas a transaction functions definidas por el usuario
    • Una transaction function recibe el estado de la BD al inicio de la transacción y sus argumentos, y devuelve un nuevo conjunto de operaciones
    • Las llamadas a funciones se expanden recursivamente hasta que solo quedan assertions y retractions
  • Las transaction functions pueden hacer lecturas internas para decidir escrituras condicionales, pero no devuelven directamente al llamador de transact el resultado de lectura ni información arbitraria
    • transact devuelve el estado de la BD justo antes de la transacción, el estado resultante después de la transacción y el conjunto expandido de datoms
    • El llamador puede usar el pre-state y el post-state para determinar si ocurrió una escritura condicional
  • Datomic está diseñado para resolver problemas alrededor de snapshots de BD baratos y transferibles
    • Nubank es la empresa que actualmente desarrolla Datomic, ofrece servicios financieros a unos 94 millones de usuarios y procesa en promedio 2.300 millones de transacciones de usuario por día
    • Casi todos los productos de Nubank usan Datomic como system of record

Afirmaciones de consistencia y diseño de pruebas

  • La documentación de Datomic afirma transacciones ACID, y sostiene que las transacciones se escriben en el almacenamiento como una sola escritura atómica y que cada peer observa, en un orden total, todas las transacciones completadas hasta cierto punto en el tiempo
    • Antes del acknowledgement al cliente, la transacción se hace flush a almacenamiento durable
  • La documentación vigente al comienzo del análisis, a inicios de enero de 2024, afirmaba de manera informal que las transacciones de escritura eran Serializable
    • La documentación también describía a Datomic como un sistema de “single-writer”, pero Jepsen considera que esta descripción es inexacta por dos razones
    • Para tolerancia a fallas, se pueden operar varios transactors, y como un detector de fallas no puede ser perfecto, puede existir una ventana en la que varios transactors se consideren activos al mismo tiempo
    • Incluso con un solo transactor, la latencia de red puede hacer que los mensajes de almacenamiento se intercalen con mensajes de otros transactors
  • Se evalúa que la seguridad de Datomic no proviene de la lógica de “single-writer”, sino de la consistencia secuencial de la operación CaS del almacenamiento
    • Incluso si hay varios transactors concurrentes, CaS debe proporcionar la seguridad
  • La prueba usó la suite de pruebas de Datomic, escrita con la biblioteca de pruebas Jepsen
    • Se instaló Datomic Pro 1.0.7075 en un clúster de nodos Debian Bookworm
    • El almacenamiento usó una tabla de AWS DynamoDB
    • Dos nodos ejecutaban transactors, y los demás ejecutaban peers
  • El peer era un pequeño programa en Clojure que usaba la biblioteca peer de Datomic y exponía una API HTTP para las operaciones de prueba
    • La prueba ejecutó tanto el modo que permite stale reads usando d/db como el modo que garantiza actualidad con d/sync
  • La inyección de fallas se aplicó tanto a transactors como a peers
    • Se inyectaron pausas de proceso, crashes y errores de reloj
    • Se crearon particiones de red entre transactor y peer, y entre nodos y almacenamiento
    • También se solicitó garbage collection de Datomic
  • El transactor se cierra por sí mismo si no puede mantener una conexión estable con el almacenamiento
    • En nodos fuera de AWS, con el timeout predeterminado de 5 segundos, se cerraba cada pocos minutos incluso con variaciones normales de red
    • Incluso en el entorno de prueba sobre EC2, con un timeout de 1 segundo se cerraba cada 10 a 20 minutos
    • Datomic recomienda que el operador reinicie el transactor con un supervisor daemon, y la prueba usó un servicio systemd con Restart=on-failure

Cuatro cargas de trabajo

  • List Append

    • La carga de trabajo List Append se usa junto con el verificador de transacciones Elle
    • Lógicamente maneja listas de elementos enteros, y cada lista se identifica por una primary key entera
    • Los clientes realizan transacciones aleatorias compuestas por lecturas de listas o append de elementos únicos
    • Elle verifica aborted reads, intermediate reads, violaciones de consistencia interna, discrepancias en el orden de los elementos, y determina violaciones del modelo de consistencia encontrando ciclos en el grafo de dependencias
    • En Datomic, una lista se codifica como una sola entity y dos atributos
    • append/key cumple el papel de primary key
    • append/elements almacena los elementos enteros de la lista como un atributo multivaluado
    • Como los atributos multivaluados son sets sin orden, Jepsen ordena los elementos por el transaction timestamp de cada datom para obtener el orden que Elle necesita
    • La restricción de no tener mixed read-write transactions se sortea con una transaction function y el cálculo del pre-state
    • Las escrituras se realizan con una transaction function
    • Usando el pre-state devuelto por transact, se calcula qué habría visto una lectura dentro de la transacción
    • La misma función se ejecuta una vez en transact y otra en el peer para complementar la lectura basada en el pre-state
  • List Append with CaS

    • La carga de trabajo List Append with CaS usa el patrón db/cas
    • El usuario puede leer el estado actual con d/db y luego enviar, por ejemplo, [:db/cas 123 :counter/value 4 5] para cambiar el valor a 5 solo si era 4
    • Si todas las escrituras usan db/cas, se puede construir una Snapshot Isolation ad hoc sobre una logical “user transaction” más amplia
    • Esta carga de trabajo almacena la lista no como multivalor, sino como una string de valor único separada por comas
    • Lee al inicio de la transacción, aplica lecturas y escrituras localmente, y luego construye una transacción CaS que garantiza que nada haya cambiado desde la lectura
  • Internal

    • La carga de trabajo Internal mide directamente la consistencia interna de la transacción
    • Incluye casos donde se hace assert de 1 y luego de 2 sobre el mismo atributo de una entity
    • Casos donde se hace assert y retract del mismo fact dentro de la misma transacción
    • Casos donde se hace assert de un valor y luego se intenta cambiar con CaS
    • Casos donde se realizan varios CaS como 1→2 y 2→3
    • Casos donde se crea una entity y luego se modifica mediante lookup ref
    • Y casos donde una transaction function intenta incrementar un valor dos veces
  • Grant

    • La carga de trabajo Grant verifica si una transaction function preserva los invariantes funcionales
    • Un grant se codifica como una sola entity con tres atributos: created-at, approved-at y denied-at
    • Un grant no debe quedar aprobado y denegado al mismo tiempo
    • Las funciones approve y deny primero revisan si el grant ya fue aprobado o denegado y, si hace falta, abortan
    • Se verifica si el grant puede quedar aprobado y denegado simultáneamente bajo varias combinaciones de límites transaccionales

Resultados de las pruebas: la seguridad entre transacciones se ve sólida

  • Jepsen no encontró comportamientos que contradijeran las afirmaciones centrales de seguridad de Datomic
    • Las transacciones parecían aplicarse como si siguieran un orden total
    • Ese orden coincidía con el orden local de operaciones en cada peer
  • Los historiales restringidos solo a transacciones de escritura, y los historiales de lecturas usando (d/sync conn), coincidían con el orden en tiempo real
    • Jepsen considera que esto se ve como Strict Serializable
  • Si se interpreta atando la sesión a un solo peer, Datomic parece garantizar Strong Session Serializability
    • El historial de transacciones no se distingue de uno ejecutado en algún orden total
    • Ese orden coincide con el orden observado en cada peer
  • d/db devuelve una copia de la base de datos actualizada de forma asíncrona, por lo que puede haber stale reads
    • La documentación de Datomic también indica explícitamente que una lectura de peer puede no observar algunas transacciones recientemente comprometidas
    • d/sync se sincroniza con el transactor para evitar stale reads
  • La validación experimental sigue teniendo límites
    • Es posible demostrar la existencia de bugs, pero no demostrar su ausencia
    • Los errores de corrección en los sistemas de almacenamiento de los que depende Datomic pueden llevar a violaciones de las garantías de Datomic
    • Datomic sobre DynamoDB es tan seguro como la operación compare-and-set de DynamoDB

Semántica interna de las transacciones: concurrencia, no orden

  • La mayoría de las bases de datos y las principales formalizaciones de aislamiento transaccional ofrecen semántica de ejecución serial dentro de una transacción
    • Si es set x = 1; read x;, normalmente el read ve 1
    • Formalizaciones como las de Adya, Cerone·Bernardi·Gotsman y Crooks·Alvisi·Pu·Clement especifican el orden de las operaciones dentro de la transacción y la propiedad de que “un read posterior observa un write previo”
  • La transaction request de Datomic es una lista ordenada, pero su ejecución no preserva ese orden
    • Los add, retract y transaction function actúan como si se ejecutaran simultáneamente entre sí
    • La transaction function siempre observa el estado de la DB al inicio de la transacción
    • No ve los efectos de assertions, retractions ni transaction functions previas
  • Si se inserta dos veces el mismo CaS sobre una entity cuyo valor actual es 0, en Datomic ambos ven el estado inicial 0 y tienen éxito
[[:db/cas 123 :internal/value 0 1]
 [:db/cas 123 :internal/value 0 1]]
  • En un modelo serial, el primer CaS debería cambiar el valor a 1 y el segundo CaS debería fallar
    • En Datomic, ambos CaS generan una assertion duplicada y el valor final queda en 1
  • Dos transaction functions de incremento también producen un resultado distinto al del modelo serial
[['internal/increment "x"]
 ['internal/increment "x"]]
  • Si el valor inicial es 0, el resultado en un modelo serial sería 2
  • En Datomic, ambas funciones ven el estado inicial 0 y el valor final queda en 1
  • Una transaction function tampoco ve una assertion anterior
[[:db/add id-of-x :internal/value 1]
 ['internal/increment "x"]]
  • En Datomic, el valor final no queda en 2 sino en 1
  • El lookup ref también usa el estado de la DB al inicio de la transacción
    • No se puede agregar una entity y luego referenciarla con un lookup ref dentro de la misma transacción
    • En ese caso, la transacción se aborta con el error Unable to resolve entity

Detección de conflictos y pseudo write skew

  • Datomic aborta con :db.error/datoms-conflict si dentro de la misma transacción se hacen assertions de valores distintos sobre un atributo de cardinalidad única
    • Si se hace una assertion del valor 2 sobre un valor inicial 0, y al mismo tiempo una increment function genera una assertion del valor 1, hay conflicto
    • Esta detección de conflictos puede evitar muchos resultados sorprendentes causados por una composición incorrecta de transaction functions
  • Si los write set son pares [entity, attribute] distintos, es difícil preservar invariantes solo con detección de conflictos
    • El workload de grant muestra esta situación
    • approve y deny verifican por separado que el grant aún no esté approved ni denied, y luego agregan atributos distintos
  • Si approve y deny se llaman desde transacciones distintas, las transacciones Serializable de Datomic garantizan el invariante
    • Pero si ambas se llaman juntas dentro de la misma transacción, ambas ven el estado inicial y tienen éxito
[['grant/approve id]
 ['grant/deny id]]
  • Como resultado, el grant termina teniendo tanto approved-at como denied-at
    • Se rompe el invariante de que “un grant no puede estar approved y denied al mismo tiempo”
    • El in-transaction conflict checker de Datomic no lo impide porque ambas funciones generan assertions sobre atributos distintos
  • Este fenómeno es similar al Write Skew de Berenson y otros
    • Como ambas funciones no pueden ver los efectos entre sí, se genera un ciclo de anti-dependencia read-write
    • Si se considera a las transaction functions como transacciones, esto se parece a la anomalía G2-item prohibida por Repeatable Read y Serializability
  • Datomic y Nubank consideran que este comportamiento no es un bug, sino el comportamiento esperado de Datomic
    • Nubank planea mantener la semántica concurrente intra-transacción de Datomic

Reforzar invariantes con entity predicate

  • Datomic ofrece mecanismos de restricción como tipo, uniqueness, predicate de atributo específico y entity predicate
    • Un entity predicate recibe un estado candidato de la DB con todos los efectos de la transacción aplicados y el ID de la entity, y devuelve true o false según si se permite el commit
    • Aunque se llama entity predicate, puede acceder al estado completo de la DB, por lo que también puede expresar restricciones globales más allá de una entity específica
  • En el ejemplo de grant, se puede usar el predicate valid-grant? para evitar que approved-at y denied-at existan al mismo tiempo
(defn valid-grant?
  [db eid]
  (let [{:grant/keys [approved-at denied-at]}
        (d/pull db '[:grant/approved-at
                     :grant/denied-at]
                   eid)]
   (not (and approved-at denied-at))))
  • En el schema se agrega un entity spec para referenciar este predicate
    • El entity predicate vinculado a un entity spec no se aplica automáticamente a todas las transacciones
    • Datomic considera que decidir si se aplica un entity spec es una decisión de dominio, por lo que cada transacción debe solicitarlo explícitamente
  • Las funciones approve y deny pueden solicitar la aplicación del entity spec después de agregar los atributos mediante el virtual datom :db/ensure
(defn approve
  [db id]
  [[:db/add id :grant/approved-at (Date.)]
   [:db/add id :db/ensure :grant/valid?]])
  • Con este entity spec, si se intenta ejecutar approve y deny juntos dentro de la misma transacción, se produce un error de entity predicate y el invariante se preserva
    • El error incluye :db.error/entity-pred y :db.error/pred-return false

Cambios en la documentación y recomendaciones para usuarios

  • Datomic modificó ampliamente su documentación tras colaborar con Jepsen
    • La documentación de seguridad de transacciones ahora refleja las garantías más fuertes que Datomic considera que realmente ofrece
    • Especifica serializabilidad global, monotonicidad por peer y Strict Serializability para escrituras o lecturas usando sync
    • El argumento de “single-writer” fue eliminado de la documentación de seguridad
  • El documento de sintaxis y semántica de transacciones cubre de forma integral la estructura de las transaction request, las reglas de expansión de la forma map y de las transaction function, y el proceso de aplicación de transacciones
  • También se revisó la documentación de transaction functions
    • Explica varios mecanismos que garantizan consistencia, la creación e invocación de funciones y el comportamiento de las funciones integradas
    • Se eliminaron expresiones como que una transaction function puede “atomically analyze and transform database values” o que garantiza “atomic read-modify-write processing”
  • Datomic planea llamar en adelante transaction request a la estructura de datos que se pasa a d/transact, en vez de “transaction”
    • Sus elementos pasarían a llamarse “data” en lugar de “statements” u “operations”
    • [:db/add ...] y [:db/retract ...] son, respectivamente, una assertion request y una retraction request
    • Esto ayuda a distinguir entre un assertion datom real y una assertion request incompleta dentro de una transaction request
  • Las precauciones necesarias para los usuarios son claras
    • La serializabilidad entre transacciones de Datomic es confiable
    • Como la semántica de ejecución concurrente dentro de una transacción es una elección poco común, hay que tener cuidado al invocar varias transaction function en la misma transacción
    • En particular, hay que prestar atención cuando el read set se superpone y el write set está separado
    • Varios incrementos pueden colapsar silenciosamente en una sola actualización
    • Se pueden usar attribute predicate y entity spec, pero entity spec debe solicitarse explícitamente en todas las transacciones necesarias
  • Desde el punto de vista operativo, también hay que considerar los reinicios del transactor y la variación de red
    • El transactor de Datomic se cierra por sí solo si no puede comunicarse con el almacenamiento durante algunos minutos
    • Jepsen recomienda agregar un retry loop al transactor para que resista mejor la variación de red

Limitaciones y preguntas para futuras investigaciones

  • Esta prueba aún dejó elementos fuera del alcance de la evaluación
    • No se evaluaron excision ni historical query
    • Tampoco se investigó la biblioteca cliente de Datomic, aunque Jepsen cree que su comportamiento probablemente sea similar al del peer usado en las pruebas
    • Solo se usó DynamoDB como motor de almacenamiento
    • Tampoco se evaluó Datomic Cloud, que usa una arquitectura ligeramente distinta
  • Jepsen señala que casi no conoce sistemas o formalizaciones que ofrezcan al mismo tiempo serializabilidad entre transacciones y semántica concurrente dentro de la transacción
  • El modelo de Datomic abre varias preguntas de investigación
    • Si una transaction de Datomic puede verse como el dual de una transacción tradicional o como un modelo de “co-transaction”
    • Si las ventajas y desventajas de ese modelo pueden mitigarse con análisis estático, verificaciones en tiempo de ejecución o extensiones de API
    • Qué tan probable es que usuarios reales escriban transacciones que rompan invariantes
  • Como puntos de comparación se mencionan el proyecto de investigación de Datalog temporal Alvaro’s Dedalus y Fauna
    • Dedalus también hace que las transacciones ocurran “all at once”, como Datomic
    • Fauna es una base de datos temporal que además soporta Strong Serializability y, a diferencia de Datomic, parece ofrecer ejecución serial y efectos laterales incrementales dentro de la transacción
  • La similitud entre el end-of-transaction conflict checker de Datomic y la regla first-committer-wins de Snapshot Isolation también queda como oportunidad de investigación
    • Qué partes de la literatura sobre Snapshot Isolation podrían aplicarse a Datomic
    • Cómo se representarían como anti-dependency edge los ciclos dentro de una transaction de Datomic
    • Sigue abierta la pregunta de si existen análogos semánticos internos a fenómenos como lost update, Fractured Read, anomalías de transacciones de solo lectura o Long Fork
  • También puede explorarse más la conexión con el teorema CALM
    • Si las transaction function lógicamente monotónicas pueden combinarse de forma segura dentro de la misma transaction de Datomic
    • Puede investigarse si los programas Datalog sin negación también son seguros bajo este modelo de ejecución

1 comentarios

 
GN⁺ 2024-05-17
Opiniones en Hacker News
  • Vi de cerca cómo avanzaba este trabajo, y fue realmente interesante seguir el proceso de discusión.
    También me sorprendió que Jepsen no encontrara bugs críticos, y el simple hecho de aclarar la documentación y los comportamientos peculiares intencionales fue un resultado muy útil.
    Considerando que estamos operando un banco sobre Datomic, fue un ejercicio totalmente valioso para generar confianza.

    • Teniendo en cuenta que es una base de datos diseñada por Rich Hickey, el resultado no sorprende tanto.
      Es un artículo realmente excelente, y cada vez que uno se siente bastante inteligente, leer un análisis de Jepsen sirve para recuperar la humildad.
    • ¿Puedo preguntar de qué banco se trata?
    • Sobre la parte de que “sorprendió que Jepsen no encontrara bugs críticos”, el informe dice que “se puede demostrar la existencia de bugs, pero no se puede demostrar la ausencia de bugs”.
    • ¿No hicieron ustedes mismos este tipo de verificación antes de operar un banco sobre eso?
  • Es la primera vez que leo en profundidad un informe de Jepsen, y me gustó la parte que explica con claridad el funcionamiento interno de las transacciones de Datomic.
    También me di cuenta de cuánto no entendía sobre la diferencia entre las transacciones de Datomic y las transacciones de bases de datos SQL.
    En particular, me llamó la atención el párrafo que dice: “Datomic antes llamaba ‘transaction’ a la estructura de datos que se pasa a d/transact, y a sus elementos los llamaba ‘statements’ u ‘operations’. De ahora en adelante, queremos llamar a esta estructura ‘transaction request’ y a sus elementos ‘data’”.
    Me pregunto qué implica esto para d/transact-async y las funcionalidades relacionadas del namespace datomic.api.
    Hace casi un año que no uso Datomic, y parece que muchas cosas cambiaron.

    • Los resultados de las pruebas de Jepsen no requirieron ningún cambio en el software Datomic.
      Todas las funciones de datomic.api siguen igual.
  • Es un informe excelente y detallado sobre una base de datos realmente buena.
    También da mucho gusto que la documentación se aclare y se actualice.
    Además, sería genial que Apple pagara un análisis de Jepsen para FoundationDB.
    Sé que Aphyr dijo que “sus pruebas probablemente son mejores”, pero si Jepsen efectivamente no encontrara problemas en FoundationDB, sería otra evidencia sólida de que es una gran base de datos.

    • No tengo ninguna intención de quitarle el sustento a Aphyr, pero me pregunto si hay alguna razón por la que un colaborador suficientemente motivado no pueda crear pruebas de Jepsen por su cuenta, o si es tan caro que ni siquiera algo “tipo GoFundMe” alcanzaría.
      No conozco bien este campo, pero cuando escucho “ojalá $foo pagara el costo”, se me paran las orejas.
      Capital sobra, pero esperar a que Apple haga algo, según mi experiencia, suele tardar mucho.
  • Jepsen encontró una situación clara que lleva a una violación de invariantes, y me pareció llamativo que la respuesta de Datomic pareciera limitarse a aclarar la documentación.
    ¿Eso significa, al final, que el equipo de Datomic acepta que estas violaciones ocurren, pero no les importa?
    El texto dice: “Desde la perspectiva de Datomic, la violación de invariantes en la carga de trabajo de grant es un error del usuario. Las funciones de transacción no se ejecutan de forma atómica en orden. Si otra operación dentro de la transacción puede invalidar sus precondiciones, no es seguro comprobar precondiciones en una función de transacción”.

    • Como verificó Jepsen, el mecanismo de imposición de invariantes de Datomic funciona según lo diseñado.
      Para ver qué significa esto desde el punto de vista del usuario, podemos pensar en los siguientes seudodatos de transacción:
      [ [Stu favorite-number 41] ;; maybe more stuff [Stu favorite-number 42] ]
      Si lo leemos operacionalmente, parece que al principio de la transacción me gustaba el 41 y que después me empezó a gustar el 42.
      Tras finalizar la transacción, un observador esperaría ver que solo me gusta el 42, y tendría que preocuparse por bajo qué condiciones podría ver el 41.
      Esta interpretación operacional de la semántica interna de una transacción es común en muchas bases de datos, pero asume que dentro de la transacción existen varios momentos.
      En Datomic no existen esos momentos, ni se desean, y se prefiere no tener que preocuparse por lo que ocurrió “a mitad de la transacción”.
      En Datomic, todos los hechos de una transacción ocurren en el mismo momento, así que esta transacción dice que empecé a gustar de ambos números simultáneamente.
      Si se lee erróneamente una transacción de Datomic como una combinación de varias operaciones, naturalmente se pueden encontrar todo tipo de “anomalías de invariantes”.
      A la inversa, si se aplica erróneamente el modelo de Datomic a una transacción SQL, también se pueden encontrar “anomalías de invariantes”.
      Debido a esta posibilidad de malentendidos, hacía falta buena documentación, y junto con Jepsen mejoramos la documentación [1] para pulir expresiones descuidadas y reducir confusiones.
      También agregamos una nota técnica que aborda directamente este malentendido específico [2].
      [1] https://docs.datomic.com/transactions/transactions.html#tran...
      [2] https://docs.datomic.com/tech-notes/comparison-with-updating...
    • En resumen, está más cerca de “es una trampa potencial, pero es consistente con la documentación y funciona según lo diseñado”.
      Que importe en la práctica depende de si el usuario escribe funciones de transacción para preservar algún invariante y esas funciones solo preservan el invariante cuando se ejecutan secuencialmente, no de forma simultánea.
      La postura de Datomic, o al menos lo que sería bueno que alguien de Datomic aclarara, es que los usuarios no suelen escribir funciones de transacción así con mucha frecuencia.
      Esta postura es defendible. La documentación decía explícitamente que las funciones de transacción no se observan entre sí, sino que observan el estado al inicio de la transacción.
      Por otro lado, la documentación también tenía una formulación que sugería que las funciones de transacción podían usarse para preservar invariantes: “[txn fns] can atomically analyze and transform database values. You can use them to ensure atomic read-modify-update processing, and integrity constraints...”
      Por esa formulación, y por el hecho de que casi todas las demás bases de datos serializables usan semántica secuencial dentro de las transacciones, el reporte dedicó bastante espacio a este tema.
      Es una pregunta compleja sin una respuesta clara, y me gustaría escuchar cómo la comunidad de bases de datos en general, y en especial los usuarios de Datomic, reciben esta semántica.
    • Creo que al final de la sección 3.1 del texto ya se da la respuesta: “Este comportamiento puede resultar sorprendente, pero en general es consistente con la documentación de Datomic. Nubank no planea cambiar este comportamiento, y nosotros no lo consideramos un bug”.
      Decir “una situación que lleva a una violación de invariantes” suena como si fuera un bug de Datomic, pero no lo es.
      Hay que entender cómo Datomic procesa las transacciones y escribir el código en consecuencia.
      No tengo relación con Nubank, pero he usado Datomic como base de datos de propósito general y no me he encontrado con una situación en la que esto fuera un problema.
    • Suena parecido a esas situaciones en algunas bases de datos relacionales en las que hay que saber que se necesita SELECT ... FOR UPDATE para actualizar dependiendo del valor que se acaba de seleccionar.
  • Para quienes no lo sepan, el nombre Jepsen es un juego de palabras tomado de Carly Rae Jepsen, la cantante de “call me maybe”.
    Me parece un nombre perfecto para un proyecto de investigación sobre sistemas distribuidos.

  • No he usado Datomic durante mucho tiempo en producción, pero es tan peculiar que me pregunto si hay algo de esto que realmente sorprenda.
    Las transacciones de Datomic son básicamente más cercanas a un batch, y como siempre pensé que eran de un solo hilo, me parece natural que no haya muchas condiciones de carrera.
    Por diseño, va por el lado lento y seguro.

    • El ejemplo de que “si incrementas x dos veces en la misma transacción, el resultado es x+1 y no x+2” me parece bastante importante.
      Parece que habría que tener mucho cuidado.
    • ¿Datomic compacta alguna vez el log de transacciones?
  • Gracias, Kyle.
    Está claro que nuestra documentación era insuficiente.
    Con Rich intentamos escribir una documentación más clara y completa sobre el modelo de transacciones de Datomic.
    Esperamos poder prevenir malentendidos comunes de antemano, y toda retroalimentación es bienvenida.
    https://docs.datomic.com/transactions/model.html

  • El modelo de datos de Datomic resulta bastante intuitivo si estás familiarizado con los almacenes de triples o RDF.
    Sin embargo, en la documentación o en las discusiones en línea no se mencionan con frecuencia estas similitudes.
    Me pregunto si es porque la gente no está familiarizada con esos conceptos, si se considera que las asociaciones con la web semántica son una distracción, o si hay alguna diferencia fundamental que se me está escapando.

  • De verdad estaba esperando este análisis.
    Últimamente estoy creando por mi cuenta un almacén de datos parecido a Datomic, así que creo que me va a resultar útil, y ahora lo estoy leyendo.
    El análisis de MongoDB también fue interesante, y definitivamente vale la pena ver otros análisis como los de Redis y RethinkDB.
    Me gustaría que algún día también hubiera análisis de rqlite/dqlite o turso/libsql.