- Lors des tests, Datomic Pro 1.0.7075 a semblé offrir une sécurité entre transactions plus forte que ce qu’affirme la documentation, mais sa sémantique à l’intérieur d’une transaction différait fortement du modèle courant d’exécution sérielle
- Tous les historiques de test semblaient Serializable ; une session peer unique était Strong Session Serializable, et les écritures ainsi que les lectures avec
d/sync étaient proches de Strong Serializable
- Dans Datomic, les add, retract et transaction functions ne s’exécutent pas en s’accumulant séquentiellement au sein d’une transaction : chaque fonction ne voit que l’état de la DB au moment du démarrage
- Si, dans une même transaction, on combine des transaction functions indépendamment sûres comme
approve et deny, le résultat composé peut créer une violation d’invariant
- Lorsque plusieurs transaction functions sont placées dans une même transaction, il faut vérifier les relations entre read set et write set, et utiliser en parallèle des contraintes explicites comme entity predicate, attribute predicate et entity spec
Modèle et architecture de Datomic Pro
- Datomic est une base de données OLTP Entity-Attribute-Value qui modélise explicitement la notion de temps
- L’état de la DB à un instant donné est représenté par un ensemble de datoms de la forme
[entity, attribute, value]
- Chaque datom conserve aussi la transaction qui l’a ajouté ou rétracté
- Un datom complet est un 5-uplet de la forme
[entity, attribute, value, transaction, asserted-or-retracted?]
- Datomic est une temporal database : on peut donc demander des snapshots non seulement de l’état actuel, mais aussi d’un instant logique passé ou d’un instant wall-clock
- Une vue de l’historique complet permet aussi de demander si un fait particulier a existé dans le passé
- Comme modes de requête, il fournit une API de style Datalog, une API de parcours de graphe et un type
Entity de style ODM
- Datomic Pro est la version que les utilisateurs peuvent exploiter eux-mêmes, tandis que Datomic Cloud fonctionne sur AWS et présente une architecture en partie différente
- Datomic Pro repose sur plusieurs composants qui coopèrent
- Le Transactor est chargé d’exécuter les transactions d’écriture, de maintenir les index et d’écrire dans le stockage
- Le Peer est un client lourd intégrant une bibliothèque JVM ; il soumet les transactions, interroge les cibles de stockage en lecture et gère le cache
- Pour les applications écrites dans d’autres langages, Datomic propose aussi un thin client et un modèle client-serveur basé sur un peer server
- En interne, Datomic ajoute chaque transaction à un log chronologique et maintient quatre index triés selon les combinaisons entity·attribute·value·time
- Le log et les index sont stockés sous forme d’arbres persistants et immuables dans un stockage comme Cassandra ou DynamoDB
- Comme les nœuds de l’arbre sont immuables, le backing storage n’a besoin de garantir que l’eventual consistency
- Au commit, le transactor enregistre de nouveaux nœuds d’arbre immuables, puis avance le pointeur racine par compare-and-set (CaS) ; ce CaS nécessite une sequential consistency
- Le CaS séquentiel garantit l’ordre global des transactions, mais limite le débit d’écriture à la vitesse d’un seul transactor
- Datomic ne dispose généralement que d’un seul transactor actif à la fois, et déploie plusieurs transactors pour la tolérance aux pannes
- Les peers se connectent directement au stockage et au transactor, et chacun conserve sa propre copie monotone croissante du pointeur racine
- Les lectures peuvent mettre en cache les nœuds d’arbre immuables ; en augmentant le nombre de peers, on obtient donc une montée en charge en lecture presque linéaire
Modèle de transaction de Datomic
- Datomic ne fournit pas d’interactive transaction comme les bases de données OLTP classiques
- Ce n’est pas un modèle où l’on démarre une transaction, reçoit le résultat d’une opération, soumet l’opération suivante, puis effectue le commit à la fin
- Il existe des transaction functions proches de stored procedures, mais elles ne peuvent pas renvoyer de valeur arbitraire à l’appelant
- Les chemins de lecture et d’écriture sont strictement séparés
db renvoie l’état le plus récent de la DB connu du peer
d/sync se synchronise avec le transactor afin d’obtenir l’état le plus récent du point de vue de tous les peers, ou un état postérieur à un instant donné
d/as-of obtient l’état de la DB à un instant passé
- Comme les états de DB sont immuables, plusieurs requêtes sur un même état s’exécutent exactement au même instant logique
- Une transaction d’écriture est représentée sous forme d’ordered list d’operations
- Les exemples incluent
:db/add, :db/retract, db/cas et les appels à des transaction functions définies par l’utilisateur
- Une transaction function reçoit l’état de la DB au début de la transaction ainsi que des arguments, puis renvoie un nouvel ensemble d’operations
- Les appels de fonctions sont développés récursivement jusqu’à ne plus laisser que des assertions et des rétractions
- Une transaction function peut effectuer des lectures en interne pour décider d’écritures conditionnelles, mais ne renvoie pas directement à l’appelant de
transact les résultats de lecture ni des informations arbitraires
transact renvoie l’état de la DB juste avant la transaction, l’état de la DB résultant de la transaction et l’ensemble développé de datoms
- L’appelant peut utiliser le pre-state et le post-state pour déterminer si une écriture conditionnelle a eu lieu
- Datomic est un modèle conçu pour résoudre les problèmes autour de snapshots de DB peu coûteux et transmissibles
- Nubank est l’entreprise qui développe actuellement Datomic ; elle fournit des services financiers à environ 94 millions d’utilisateurs et traite en moyenne 2,3 milliards de transactions utilisateur par jour
- La quasi-totalité des produits de Nubank utilisent Datomic comme system of record
Revendications de cohérence et conception des tests
- La documentation de Datomic revendique des transactions ACID et considère que chaque transaction est enregistrée dans le stockage sous la forme d’une unique écriture atomique, et que chaque peer observe, dans un ordre total, toutes les transactions terminées jusqu’à un instant donné
- Avant l’acknowledgement au client, la transaction est flushée vers un stockage durable
- Au début de l’analyse, début janvier 2024, la documentation affirmait officieusement que les transactions d’écriture étaient Serializable
- La documentation décrivait aussi Datomic comme un système « single-writer », mais Jepsen considère cette description inexacte pour deux raisons
- Pour la tolérance aux pannes, plusieurs transactors peuvent être exploités, et comme un détecteur de pannes ne peut pas être parfait, il peut exister une fenêtre pendant laquelle plusieurs transactors se considèrent simultanément comme actifs
- Même avec un seul transactor, les délais réseau peuvent faire que les messages de stockage s’intercalent avec les messages d’autres transactors
- La sûreté de Datomic est évaluée comme provenant non pas d’une logique « single-writer », mais de la cohérence séquentielle de l’opération CaS du stockage
- Même en présence de plusieurs transactors concurrents, CaS doit assurer la sûreté
- Les tests utilisent la suite de tests Datomic, écrite avec la bibliothèque de test Jepsen
- Datomic Pro 1.0.7075 est installé sur un cluster de nœuds Debian Bookworm
- Le stockage utilise une table AWS DynamoDB
- Deux nœuds exécutent des transactors, et les autres exécutent des peers
- Le peer était un petit programme Clojure utilisant la bibliothèque peer de Datomic, et exposait une API HTTP pour les opérations de test
- Les tests exécutent à la fois un mode avec lectures potentiellement obsolètes utilisant
d/db et un mode garantissant la fraîcheur avec d/sync
- L’injection de fautes est appliquée à la fois aux transactors et aux peers
- Des pauses de processus, des crashs et des erreurs d’horloge sont injectés
- Des partitions réseau sont créées entre transactors et peers, ainsi qu’entre les nœuds et le stockage
- La garbage collection de Datomic est également demandée
- Le transactor s’arrête de lui-même s’il ne parvient pas à maintenir une connexion stable au stockage
- Depuis des nœuds hors d’AWS, avec le timeout par défaut de 5 secondes, il s’arrêtait toutes les quelques minutes même en raison de variations réseau normales
- Même dans un environnement de test EC2, avec un timeout de 1 seconde, il s’arrêtait toutes les 10 à 20 minutes
- Datomic recommande que l’opérateur redémarre le transactor avec un daemon superviseur, et les tests utilisent un service systemd configuré avec
Restart=on-failure
Quatre workloads
-
List Append
- Le workload List Append est utilisé avec le vérificateur de transactions Elle
- Il manipule logiquement des listes d’entiers, chaque liste étant identifiée par une clé primaire entière
- Les clients exécutent des transactions aléatoires composées de lectures de listes ou d’append d’éléments uniques
- Elle vérifie les aborted reads, intermediate reads, violations de cohérence interne et incohérences d’ordre des éléments, puis recherche des cycles dans le graphe de dépendances pour déterminer les violations du modèle de cohérence
- Dans Datomic, une liste est encodée comme une entité avec deux attributs
append/key joue le rôle de clé primaire
append/elements est un attribut multivalué qui stocke les éléments entiers de la liste
- Comme un attribut multivalué est un set non ordonné, Jepsen trie les éléments selon le timestamp de transaction de chaque datom afin d’obtenir l’ordre requis par Elle
- La contrainte d’absence de transactions mixed read-write est contournée via une transaction function et le calcul du pre-state
- Les écritures sont effectuées par une transaction function
- Le pre-state renvoyé par
transact est utilisé pour calculer ce que les lectures internes à la transaction auraient vu
- La même fonction est exécutée une fois dans
transact, puis une fois côté peer pour compléter les lectures fondées sur le pre-state
-
List Append with CaS
- Le workload List Append with CaS utilise le pattern
db/cas
- L’utilisateur peut lire l’état courant avec
d/db et soumettre, par exemple, [:db/cas 123 :counter/value 4 5], qui ne remplace la valeur par 5 que si elle vaut 4
- Utiliser
db/cas pour toutes les écritures permet de construire une Snapshot Isolation ad hoc au-dessus d’une « transaction utilisateur » logique
- Ce workload stocke les listes non pas sous forme multivaluée, mais sous forme de chaîne de caractères single-valued séparée par des virgules
- Au début de la transaction, l’état est lu ; les lectures et écritures sont appliquées localement, puis une transaction CaS est construite pour garantir que les valeurs lues n’ont pas changé depuis
-
Internal
- Le workload Internal mesure directement la cohérence interne des transactions
- Le cas où l’on assert 1 puis 2 sur le même attribut d’entité
- Le cas où l’on assert puis retract un fact dans la même transaction
- Le cas où l’on assert une valeur avant d’essayer de la modifier avec CaS
- Le cas où l’on effectue plusieurs CaS comme 1→2, 2→3
- Le cas où l’on crée une entité puis la modifie via une lookup ref
- Le cas où l’on tente d’incrémenter deux fois une valeur avec une transaction function
-
Grant
- Le workload Grant vérifie qu’une transaction function préserve les invariants de la fonction
- Un grant est encodé comme une seule entité dotée de trois attributs :
created-at, approved-at et denied-at
- Un grant ne doit pas être simultanément dans les états approved et denied
- Les fonctions
approve et deny vérifient d’abord si le grant est déjà approved ou denied, et abort si nécessaire
- Le test vérifie, pour plusieurs combinaisons de transaction boundaries, si un grant peut devenir simultanément approved et denied
Résultats des tests : la sûreté entre transactions semble forte
- Jepsen n’a trouvé aucun comportement contredisant les principales revendications de sûreté de Datomic
- Les transactions semblaient appliquées selon un ordre total
- Cet ordre correspondait à l’ordre local des opérations sur chaque peer
- Les historiques limités aux seules transactions d’écriture, ainsi que les historiques utilisant
(d/sync conn) pour les lectures, correspondaient à l’ordre temps réel
- Jepsen considère que cela paraît Strict Serializable
- Si l’on interprète les sessions comme liées à un seul peer, Datomic semble garantir la Strong Session Serializability
- L’historique des transactions est indiscernable d’un historique exécuté dans un certain ordre total
- Cet ordre correspond à l’ordre observé sur chaque peer
d/db renvoie une copie de la DB mise à jour de façon asynchrone, ce qui permet des lectures obsolètes
- La documentation de Datomic précise également que les lectures peer peuvent ne pas observer certaines transactions récemment commit
d/sync se synchronise avec le transactor pour éviter les lectures obsolètes
- La validation expérimentale conserve des limites
- Il est possible de prouver l’existence de bugs, mais pas leur absence
- Une erreur de correctness dans le système de stockage dont dépend Datomic peut entraîner une violation des garanties de Datomic
- Datomic sur DynamoDB est aussi sûr que l’opération compare-and-set de DynamoDB
Sémantique intra-transaction : concurrence, pas ordre
- La plupart des bases de données et des principales formalisations de l’isolation des transactions offrent une sémantique d’exécution sérielle à l’intérieur d’une transaction
- Avec
set x = 1; read x;, le read voit généralement 1
- Les formalisations d’Adya, de Cerone·Bernardi·Gotsman, de Crooks·Alvisi·Pu·Clement, etc., explicitent l’ordre des opérations au sein d’une transaction et la propriété selon laquelle « un read ultérieur observe un write antérieur »
- Une transaction request de Datomic est une liste ordonnée, mais l’exécution ne préserve pas cet ordre
- Les add, retract et transaction functions se comportent comme s’ils s’exécutaient simultanément
- Une transaction function observe toujours l’état de la base au moment du début de la transaction
- Elle ne voit pas les effets des assertions, retractions ou transaction functions précédentes
- Si l’on met deux fois le même CaS sur une entity dont la valeur actuelle est
0, dans Datomic les deux voient l’état initial 0 et réussissent
[[:db/cas 123 :internal/value 0 1]
[:db/cas 123 :internal/value 0 1]]
- Dans un modèle sériel, le premier CaS devrait changer la valeur en
1 et le second CaS devrait échouer
- Dans Datomic, les deux CaS créent des assertions en double et la valeur finale devient
1
- Deux transaction functions d’increment produisent aussi un résultat différent du modèle sériel
[['internal/increment "x"]
['internal/increment "x"]]
- Si la valeur initiale est
0, le résultat du modèle sériel est 2
- Dans Datomic, les deux fonctions voient l’état initial
0 et la valeur finale devient 1
- Une transaction function ne voit pas non plus une assertion précédente
[[:db/add id-of-x :internal/value 1]
['internal/increment "x"]]
- Dans Datomic, la valeur finale devient
1, pas 2
- Les lookup refs utilisent aussi l’état de la base au moment du début de la transaction
- Après avoir ajouté une entity dans la même transaction, on ne peut pas la référencer via une lookup ref
- Dans ce cas, la transaction est abortée avec une erreur
Unable to resolve entity
Détection des conflits et pseudo write skew
- Si Datomic reçoit, au sein d’une même transaction, des assertions de valeurs différentes pour un attribut à cardinalité unique, il abort avec
:db.error/datoms-conflict
- Pour une valeur initiale
0, si l’on assert la valeur 2 et qu’en même temps une fonction d’increment crée une assertion de valeur 1, il y a conflit
- Cette détection des conflits peut empêcher de nombreux résultats surprenants dus à une mauvaise composition des transaction functions
- Si le write set porte sur des paires
[entity, attribute] différentes, il est difficile de préserver les invariants avec la seule détection des conflits
- La workload grant illustre cette situation
approve et deny vérifient chacun que le grant n’est encore ni approved ni denied, puis ajoutent des attributs différents
- Si
approve et deny sont appelées dans des transactions différentes, les transactions Serializable de Datomic garantissent l’invariant
- Mais si les deux sont appelées ensemble dans une même transaction, elles voient toutes les deux l’état initial et réussissent
[['grant/approve id]
['grant/deny id]]
- Le grant obtenu possède à la fois
approved-at et denied-at
- L’invariant « un grant ne doit pas être approved et denied en même temps » est violé
- Le vérificateur de conflits intra-transaction de Datomic ne l’empêche pas, car les deux fonctions ont créé des assertions sur des attributs différents
- Ce phénomène ressemble au Write Skew de Berenson et al.
- Les deux fonctions ne voient pas leurs effets respectifs, ce qui crée un cycle de read-write anti-dependency
- Si l’on considère les transaction functions comme des transactions, cela ressemble à une anomalie G2-item interdite par Repeatable Read et Serializability
- Datomic et Nubank considèrent ce comportement comme le comportement attendu de Datomic, et non comme un bug
- Nubank prévoit de conserver la sémantique intra-transaction concurrente de Datomic
Renforcer les invariants avec un entity predicate
- Datomic fournit des mécanismes de contraintes comme les types, l’unicité, certains attribute predicates et les entity predicates
- Un entity predicate reçoit l’état candidat de la base après application de tous les effets de la transaction ainsi qu’un entity ID, puis renvoie
true ou false selon que le commit doit être autorisé
- Malgré son nom d’entity predicate, il peut accéder à l’état complet de la base, ce qui permet aussi d’exprimer des contraintes globales au-delà d’une entity donnée
- Dans l’exemple grant, le predicate
valid-grant? peut empêcher approved-at et denied-at d’exister simultanément
(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))))
- Le schema ajoute une entity spec qui référence ce predicate
- Les entity predicates associées à une entity spec ne sont pas appliquées automatiquement à toutes les transactions
- Datomic considère que l’application d’une entity spec est une domain decision et estime que chaque transaction doit la demander explicitement
- Les fonctions
approve et deny peuvent ajouter l’attribut puis demander l’application de l’entity spec avec le virtual datom :db/ensure
(defn approve
[db id]
[[:db/add id :grant/approved-at (Date.)]
[:db/add id :db/ensure :grant/valid?]])
- Avec cette entity spec, si l’on tente approve et deny ensemble dans une même transaction, une erreur d’entity predicate se produit et l’invariant est préservé
- L’erreur contient
:db.error/entity-pred et :db.error/pred-return false
Évolution de la documentation et recommandations aux utilisateurs
- Datomic a fortement révisé sa documentation après sa collaboration avec Jepsen
- La documentation sur la sûreté des transactions reflète les garanties plus fortes que Datomic considère effectivement fournir
- Elle précise la sérialisabilité globale, la monotonicité par peer et la sérialisabilité stricte pour les lectures utilisant une écriture ou
sync
- L’argument du « single-writer » a été retiré de la documentation sur la sûreté
- La documentation sur la syntaxe et la sémantique des transactions couvre de manière exhaustive la structure des transaction requests, les règles d’expansion de la forme map et des fonctions de transaction, ainsi que le processus d’application des transactions
- La documentation sur les fonctions de transaction a également été modifiée
- Elle explique plusieurs mécanismes garantissant la cohérence, la création et l’appel de fonctions, ainsi que le comportement des fonctions intégrées
- Les formulations indiquant qu’une fonction de transaction peut « atomically analyze and transform database values » ou garantit un « atomic read-modify-write processing » ont été retirées
- Datomic souhaite désormais appeler les structures de données passées à
d/transact des transaction requests, et non des « transactions »
- Leurs éléments seraient appelés « data » plutôt que « statements » ou « operations »
[:db/add ...] et [:db/retract ...] sont respectivement des assertion requests et des retraction requests
- Cela aide à distinguer les datoms d’assertion réels des assertion requests incomplètes présentes dans une transaction request
- Les précautions à prendre pour les utilisateurs sont claires
- La sérialisabilité entre transactions de Datomic est fiable
- La sémantique d’exécution concurrente à l’intérieur d’une transaction est un choix inhabituel ; il faut donc être prudent lorsque plusieurs fonctions de transaction sont appelées dans une même transaction
- Il faut notamment se méfier des cas où les read sets se recouvrent et où les write sets sont disjoints
- Plusieurs incréments peuvent être silencieusement fusionnés en une seule update
- On peut utiliser des attribute predicates et des entity specs, mais les entity specs doivent être explicitement demandées dans toutes les transactions nécessaires
- Du point de vue opérationnel, il faut aussi tenir compte des redémarrages du transactor et des variations réseau
- Le transactor Datomic s’arrête de lui-même s’il ne peut pas communiquer avec le stockage pendant quelques minutes
- Jepsen recommande d’ajouter une retry loop au transactor afin de mieux résister aux variations réseau
Limites et questions de recherche futures
- Certains éléments sont restés hors du périmètre de cette évaluation
- L’excision et les requêtes historiques n’ont pas été évaluées
- La bibliothèque cliente Datomic n’a pas non plus été étudiée, mais Jepsen estime que son comportement est probablement similaire à celui du peer utilisé pour les tests
- Un seul moteur de stockage a été utilisé : DynamoDB
- Datomic Cloud n’a pas non plus été évalué, et Datomic Cloud utilise une architecture légèrement différente
- Jepsen indique connaître peu de systèmes ou de formalisations offrant à la fois la sérialisabilité entre transactions et une sémantique concurrente à l’intérieur des transactions
- Le modèle de Datomic soulève plusieurs questions de recherche
- Peut-on voir une transaction Datomic comme le dual d’une transaction traditionnelle, ou comme un modèle de « co-transaction » ?
- Les avantages et inconvénients de ce modèle peuvent-ils être atténués par de l’analyse statique, des contrôles à l’exécution ou des extensions d’API ?
- Quelle est la probabilité que des utilisateurs réels écrivent des transactions qui brisent des invariants ?
- Sont mentionnés comme points de comparaison le projet de recherche temporal Datalog Alvaro’s Dedalus et Fauna
- Dedalus, comme Datomic, exécute lui aussi les transactions « all at once »
- Fauna est une base de données temporelle qui prend aussi en charge la Strong Serializability et, contrairement à Datomic, semble fournir une exécution sérielle et des effets de bord incrémentaux à l’intérieur d’une transaction
- La similarité entre le vérificateur de conflits de fin de transaction de Datomic et la règle first-committer-wins de Snapshot Isolation reste également une piste de recherche
- Quelles parties de la littérature sur Snapshot Isolation peuvent être appliquées à Datomic ?
- Par quels arcs d’anti-dépendance les cycles internes à une transaction Datomic peuvent-ils être représentés ?
- Existe-t-il, dans la sémantique interne, des analogues à des phénomènes comme lost update, Fractured Read, read-only transaction anomaly ou Long Fork ?
- Le lien avec le théorème CALM pourrait aussi être exploré davantage
- Des fonctions de transaction logiquement monotones peuvent-elles être combinées sans risque au sein d’une même transaction Datomic ?
- On peut étudier si les programmes Datalog sans négation restent sûrs dans ce modèle d’exécution
1 commentaires
Commentaires sur Hacker News
J’ai suivi ce travail de près, et il était vraiment fascinant d’observer le processus de discussion
Le fait que Jepsen n’ait pas trouvé de bug critique était aussi surprenant, et le simple fait d’avoir clarifié la documentation et les comportements particuliers prévus était déjà un résultat très utile
Sachant qu’ils font tourner une banque sur Datomic, c’était largement un exercice qui valait la peine pour renforcer la confiance
C’est un excellent article, et chaque fois que je commence à me croire assez intelligent, lire une analyse Jepsen me remet humblement à ma place
C’est la première fois que je lis un rapport Jepsen en profondeur, et j’ai aimé la manière dont il explique clairement le fonctionnement interne des transactions de Datomic
Cela m’a aussi fait réaliser à quel point je comprenais mal la différence entre les transactions Datomic et celles des bases de données SQL
J’ai particulièrement remarqué le passage : « Datomic appelait auparavant la structure de données passée à
d/transactune “transaction”, et ses éléments des “statements” ou des “operations”. À l’avenir, nous appellerons cette structure une “transaction request” et ses éléments des “data”. »Je me demande ce que cela implique pour d/transact-async et les fonctionnalités associées dans l’espace de noms
datomic.apiCela fait presque un an que je n’ai pas utilisé Datomic, et on dirait que beaucoup de choses ont changé
Toutes les fonctions de
datomic.apirestent inchangéesUn excellent rapport, détaillé, sur une très bonne base de données
C’est aussi très appréciable de voir la documentation clarifiée et mise à jour
Au passage, j’aimerais vraiment qu’Apple finance une analyse Jepsen de FoundationDB
Je sais qu’Aphyr a dit que « leurs tests sont probablement meilleurs », mais si Jepsen ne trouvait effectivement pas de problème sur FoundationDB, ce serait un argument très fort pour dire que c’est une autre excellente base de données
Je ne connais pas très bien ce domaine, mais quand je lis « j’aimerais que $foo finance ça », ça m’interpelle
Le capital ne manque pas, mais attendre qu’Apple fasse quelque chose prend souvent très longtemps, d’après mon expérience
J’ai trouvé impressionnant que Jepsen ait mis en évidence un cas clair menant à une violation d’invariant, et que la réponse de Datomic semble s’être limitée à clarifier la documentation.
Au final, cela signifie-t-il que l’équipe Datomic admet que ce type de violation se produit, mais considère que ce n’est pas important ?
L’article dit en substance : « Du point de vue de Datomic, une violation d’invariant dans la charge de travail grant est une erreur utilisateur. Les fonctions de transaction ne s’exécutent pas atomiquement de façon séquentielle. Si d’autres opérations dans la transaction peuvent invalider ces préconditions, il n’est pas sûr de vérifier des préconditions dans une fonction de transaction. »
Pour voir ce que cela signifie du point de vue de l’utilisateur, on peut imaginer des pseudo-données de transaction comme celles-ci :
[ [Stu favorite-number 41] ;; maybe more stuff [Stu favorite-number 42] ]En lecture opérationnelle, cela semble dire qu’au début de la transaction j’aimais 41, puis qu’ensuite je me suis mis à préférer 42.
Après la fin de la transaction, un observateur s’attendrait à voir que je n’aime plus que 42, et pourrait se demander dans quelles conditions 41 pourrait être visible.
Cette interprétation opérationnelle de la sémantique interne des transactions est courante dans de nombreuses bases de données, mais elle suppose l’existence de plusieurs moments distincts à l’intérieur d’une transaction.
Datomic n’a pas de tels moments, n’en veut pas, et préfère justement qu’on n’ait pas à se soucier de ce qui s’est passé « au milieu de la transaction ».
Dans Datomic, tous les faits d’une transaction se produisent au même instant, donc cette transaction dit que j’ai commencé à aimer les deux nombres simultanément.
Si on lit à tort une transaction Datomic comme la combinaison de plusieurs opérations, on peut évidemment y trouver toutes sortes d’« anomalies d’invariant ».
À l’inverse, on peut aussi trouver des « anomalies d’invariant » si l’on plaque à tort le modèle de Datomic sur une transaction SQL.
C’est à cause de ce risque de malentendu qu’une bonne documentation était nécessaire, et nous avons travaillé avec Jepsen pour améliorer la documentation [1], en corrigeant des formulations imprécises et en réduisant les ambiguïtés.
Nous avons aussi ajouté une note technique qui traite directement de ce malentendu précis [2].
[1] https://docs.datomic.com/transactions/transactions.html#tran...
[2] https://docs.datomic.com/tech-notes/comparison-with-updating...
La question de savoir si c’est réellement important dépend du fait que l’utilisateur écrive des fonctions de transaction pour préserver certains invariants, et que ces fonctions ne préservent ces invariants que dans le cas d’une exécution séquentielle, et non simultanée.
La position de Datomic — ou en tout cas j’aimerais que quelqu’un de chez Datomic intervienne pour la formuler — est que les utilisateurs n’écrivent probablement pas si souvent ce genre de fonctions de transaction.
Cette position est défendable. La documentation indiquait explicitement que les fonctions de transaction n’observent pas les autres, mais observent l’état au début de la transaction.
En revanche, la documentation contenait aussi des formulations laissant entendre que les fonctions de transaction peuvent servir à préserver des invariants : « [txn fns] can atomically analyze and transform database values. You can use them to ensure atomic read-modify-update processing, and integrity constraints... »
À cause de cette formulation, et du fait que presque toutes les autres bases de données sérialisables utilisent une sémantique séquentielle à l’intérieur de la transaction, le rapport a consacré beaucoup d’espace à ce sujet.
C’est une question complexe, sans réponse évidente, et j’aimerais savoir comment la communauté des bases de données en général, et les utilisateurs de Datomic en particulier, reçoivent cette sémantique.
Dire qu’il existe « une situation menant à une violation d’invariant » donne l’impression qu’il s’agit d’un bug de Datomic, mais ce n’est pas le cas.
Il faut comprendre la manière dont Datomic traite les transactions et écrire son code en conséquence.
Cela n’a rien à voir avec Nubank, mais en utilisant Datomic comme base de données généraliste, je n’ai jamais rencontré de situation où cela posait problème.
Pour ceux qui ne le sauraient pas, le nom Jepsen est un jeu de mots tiré de Carly Rae Jepsen, qui chante « call me maybe ».
Je trouve que c’est un nom parfait pour un projet de recherche sur les systèmes distribués.
Le premier commit date de 2013.
https://github.com/jepsen-io/jepsen/tree/4b112e7046a20efa80a...
La chanson est sortie en 2011, le temps passe vraiment vite.
Je n’ai pas une longue expérience de Datomic en production, mais c’est un système tellement atypique que je me demande s’il y a vraiment quelque chose de surprenant ici.
Une transaction Datomic est fondamentalement proche d’un batch, et comme je l’ai toujours vue comme monothread, il me semble normal qu’il n’y ait pas beaucoup de conditions de concurrence.
Par conception, c’est lent mais sûr.
Il semble qu’il faille être particulièrement prudent.
Merci Kyle.
Il est clair que notre documentation était insuffisante.
Avec Rich, nous avons voulu écrire une documentation plus claire et plus complète sur le modèle de transaction de Datomic.
Nous espérons qu’elle permettra de prévenir les malentendus les plus fréquents, et tous les retours sont les bienvenus.
https://docs.datomic.com/transactions/model.html
Le modèle de données de Datomic est assez intuitif si l’on connaît les triple stores ou RDF
Pourtant, cette parenté est rarement mentionnée dans la documentation ou les discussions en ligne
Je me demande si c’est parce que les gens ne sont pas familiers avec ces concepts, parce qu’ils considèrent les associations avec le web sémantique comme un obstacle, ou s’il existe une différence fondamentale qui m’échappe
J’attendais vraiment cette analyse
Je construis moi-même en ce moment un stockage de données similaire à Datomic, donc ça devrait m’être utile, et je suis en train de la lire
L’analyse de MongoDB était aussi intéressante, et j’aimerais vraiment voir d’autres analyses sur Redis, RethinkDB, etc.
Ce serait bien aussi d’avoir un jour une analyse de rqlite/dqlite ou de turso/libsql
Il y a eu une analyse de style Jepsen de rqlite il y a deux ans [1]
https://www.philipotoole.com/testing-rqlite-read-consistency...
Le rapport lui-même est ici : https://github.com/wildarch/jepsen.rqlite/blob/main/doc/blog...
[1] https://www.rqlite.io