- O Datomic Pro 1.0.7075 pareceu ter, nos testes, garantias entre transações mais fortes do que o alegado na documentação, mas a semântica interna da transação diferia bastante do modelo comum de execução serial
- Todo o histórico de testes pareceu Serializable, e uma única sessão de peer foi Strong Session Serializable, enquanto escritas e leituras com
d/sync chegaram perto de Strong Serializable
- As operações add, retract e transaction function do Datomic não são executadas de forma cumulativa e em ordem dentro da transação; cada função opera vendo apenas o estado do DB no momento inicial
- Se transaction functions como
approve e deny, que isoladamente são seguras, forem combinadas na mesma transação, o resultado composto pode gerar violação de invariantes
- Ao colocar várias transaction functions em uma transação, é preciso verificar a relação entre read set e write set e usar junto restrições explícitas como entity predicate, attribute predicate e entity spec
Modelo e arquitetura do Datomic Pro
- O Datomic é um banco de dados OLTP Entity-Attribute-Value que modela explicitamente o conceito de tempo
- O estado do DB em um instante específico é representado como um conjunto de datoms no formato
[entity, attribute, value]
- Cada datom também preserva qual transação o adicionou ou retraiu
- O conjunto completo de datoms é uma 5-tupla no formato
[entity, attribute, value, transaction, asserted-or-retracted?]
- O Datomic é um banco de dados temporal, então é possível pedir snapshots não só do presente, mas também com base em um instante lógico passado ou em um horário de wall-clock
- Também é possível consultar, pela visão de histórico completo, se um fato específico existiu no passado
- Como formas de consulta, ele oferece uma API em estilo Datalog, uma API de travessia de grafo e o tipo
Entity em estilo ODM
- Datomic Pro é a versão que o usuário pode operar diretamente, enquanto o Datomic Cloud roda na AWS e tem uma arquitetura parcialmente diferente
- O Datomic Pro tem uma estrutura em que vários componentes cooperam
- O Transactor é responsável por executar transações de escrita, manter índices e gravar no armazenamento
- O Peer é um cliente pesado com biblioteca JVM embutida, responsável por enviar transações, consultar leituras no armazenamento e fazer cache
- Para aplicações em outras linguagens, também há um modelo cliente-servidor baseado em thin client e peer server
- Internamente, o Datomic faz append de cada transação em um log cronológico e mantém quatro índices ordenados pela combinação de entity, attribute, value e time
- O log e os índices são armazenados como árvores persistentes e imutáveis em storages como Cassandra ou DynamoDB
- Como os nós da árvore são imutáveis, o armazenamento subjacente só precisa garantir eventual consistency
- No commit, o transactor grava novos nós imutáveis da árvore e então avança o ponteiro da raiz com compare-and-set (CaS); esse CaS exige sequential consistency
- O CaS sequencial garante a ordem global das transações, mas limita a taxa de escrita à velocidade de um único transactor
- O Datomic normalmente mantém apenas um transactor ativo por vez e distribui vários transactors para tolerância a falhas
- O peer se conecta diretamente ao storage e ao transactor e mantém sua própria cópia monotonicamente crescente do ponteiro da raiz
- Como leituras podem fazer cache dos nós imutáveis da árvore, aumentar o número de peers permite escalabilidade de leitura quase linear
Modelo de transações do Datomic
- O Datomic não oferece interactive transaction como um banco de dados OLTP comum
- Não é o modelo em que você inicia uma transação, recebe o resultado de uma operação, envia a próxima e faz commit no final
- Existem transaction functions parecidas com stored procedures, mas elas não podem retornar valores arbitrários ao chamador
- Os caminhos de leitura e escrita são rigidamente separados
db retorna o estado mais recente do DB conhecido pelo peer
d/sync sincroniza com o transactor para obter o estado mais recente considerando todos os peers, ou o estado após um instante específico
d/as-of obtém o estado do DB em um ponto do passado
- Como o estado do DB é imutável, várias consultas sobre o mesmo estado são executadas exatamente no mesmo instante lógico
- As transações de escrita são representadas como uma lista ordenada de operações
- Os exemplos incluem
:db/add, :db/retract, db/cas e chamadas de transaction functions definidas pelo usuário
- Uma transaction function recebe o estado do DB no início da transação e seus argumentos, e retorna um novo conjunto de operações
- As chamadas de função são expandidas recursivamente até restarem apenas assertions e retractions
- A transaction function pode fazer leituras internamente para decidir escritas condicionais, mas não retorna diretamente ao chamador de
transact os resultados da leitura nem informações arbitrárias
transact retorna o estado do DB imediatamente antes da transação, o estado resultante após a transação e o conjunto expandido de datoms
- O chamador pode usar o estado anterior e o posterior para determinar se a escrita condicional aconteceu
- O Datomic foi projetado para resolver problemas com foco em snapshots de DB baratos e transportáveis
- O Nubank é a atual empresa desenvolvedora do Datomic e oferece serviços financeiros para cerca de 94 milhões de usuários, processando em média 2,3 bilhões de transações de usuários por dia
- Quase todos os produtos do Nubank usam o Datomic como system of record
Alegações de consistência e desenho dos testes
- A documentação do Datomic afirma transações ACID e considera que as transações são gravadas no armazenamento como uma única escrita atômica, e que cada peer observa, em ordem total, todas as transações concluídas até um determinado ponto no tempo
- Antes do acknowledgement ao cliente, a transação é flushada para o armazenamento durável
- A documentação, no momento em que a análise começou no início de janeiro de 2024, afirmava informalmente que as transações de escrita eram Serializable
- A documentação também descrevia o Datomic como um sistema “single-writer”, mas o Jepsen considera essa descrição imprecisa por dois motivos
- É possível operar vários transactors para tolerância a falhas e, como detectores de falha não podem ser perfeitos, pode surgir uma janela em que vários transactors sejam considerados ativos ao mesmo tempo
- Mesmo com apenas um transactor, a latência de rede pode fazer com que mensagens para o armazenamento se intercalem com mensagens de outros transactors
- A avaliação é que a segurança do Datomic não vem da lógica de “single-writer”, mas da consistência sequencial da operação de CaS do armazenamento
- Mesmo com vários transactors concorrentes, o CaS precisa fornecer a segurança
- Os testes usaram a Datomic test suite, escrita com a biblioteca de testes Jepsen
- O Datomic Pro 1.0.7075 foi instalado em um cluster de nós Debian Bookworm
- O armazenamento usou uma tabela do AWS DynamoDB
- Dois nós executavam o transactor, e os demais executavam peers
- O peer era um pequeno programa em Clojure usando a biblioteca Datomic peer, e expunha uma API HTTP para operações de teste
- Os testes executaram tanto o modo com possibilidade de stale read usando
d/db quanto o modo com atualidade garantida usando d/sync
- A injeção de falhas foi aplicada tanto aos transactors quanto aos peers
- Foram injetados pause de processo, crash e erro de relógio
- Foram criadas partições de rede entre transactor e peer, e entre nó e armazenamento
- O garbage collection do Datomic também foi solicitado
- O transactor se encerra sozinho se não conseguir manter uma conexão estável com o armazenamento
- Em nós fora da AWS, com o timeout padrão de 5 segundos, ele encerrava a cada poucos minutos mesmo com variações normais de rede
- Mesmo no ambiente de teste em EC2, com timeout de 1 segundo, ele encerrava a cada 10 a 20 minutos
- O Datomic recomenda que o operador reinicie o transactor com um daemon supervisor, e os testes usaram um serviço systemd com
Restart=on-failure
Quatro workloads
-
List Append
- O workload List Append é usado com o verificador de transações Elle
- Ele lida logicamente com listas de elementos inteiros, e cada lista é identificada por uma chave primária inteira
- Os clientes executam transações aleatórias compostas por leitura de lista ou append de elemento único
- O Elle verifica aborted read, intermediate read, violações de consistência interna, divergências na ordem dos elementos e determina violações do modelo de consistência ao encontrar ciclos no grafo de dependências
- No Datomic, as listas são codificadas com uma entidade e dois atributos
append/key funciona como chave primária
append/elements armazena os elementos inteiros da lista como atributo multivalorado
- Como atributos multivalorados são sets sem ordem, o Jepsen ordena os elementos pelo transaction timestamp de cada datom para obter a ordem necessária ao Elle
- A restrição de não haver mixed read-write transaction é contornada com transaction function e cálculo do pre-state
- As escritas são feitas por transaction function
- O pre-state retornado por
transact é usado para calcular o que a leitura interna da transação teria visto
- A mesma função é executada uma vez em
transact e outra no peer para complementar a leitura baseada em pre-state
-
List Append with CaS
- O workload List Append with CaS usa o padrão
db/cas
- O usuário pode ler o estado atual com
d/db e enviar, por exemplo, [:db/cas 123 :counter/value 4 5] para trocar o valor para 5 somente se ele for 4
- Usar
db/cas em todas as escritas permite construir um Snapshot Isolation ad hoc sobre a “user transaction” lógica
- Neste workload, a lista é armazenada não como multivalorada, mas como uma string de valor único separada por vírgulas
- Ele lê no início da transação, aplica leituras e escritas localmente e então constrói uma transação CaS que garante que nada mudou desde a leitura
-
Internal
- O workload Internal mede diretamente a consistência interna da transação
- Inclui casos em que se faz assert de 1 e depois de 2 no mesmo atributo de entidade
- Casos em que um fact é asserted e depois retracted na mesma transação
- Casos em que se faz assert de um valor e depois se tenta trocá-lo com CaS
- Casos com vários CaS, como 1→2 e 2→3
- Casos em que uma entidade é criada e depois modificada por lookup ref
- E casos em que uma transaction function tenta incrementar um valor duas vezes
-
Grant
- O workload Grant verifica se a transaction function preserva invariantes da função
- Um grant é codificado como uma única entidade com três atributos:
created-at, approved-at e denied-at
- Um grant não pode ficar simultaneamente nos estados approved e denied
- As funções
approve e deny primeiro verificam se o grant já foi approved ou denied e, se necessário, fazem abort
- O teste verifica, em várias combinações de transaction boundary, se um grant pode se tornar approved e denied ao mesmo tempo
Resultados dos testes: a segurança entre transações parece forte
- O Jepsen não encontrou comportamentos que contradissessem as alegações centrais de segurança do Datomic
- As transações pareciam ser aplicadas em ordem total
- Essa ordem era consistente com a ordem local das operações em cada peer
- Os históricos limitados apenas a transações de escrita, e os históricos de leitura com uso de
(d/sync conn), foram consistentes com a ordem em tempo real
- O Jepsen considera que isso parece Strict Serializable
- Ao interpretar sessões como vinculadas a um único peer, o Datomic parece garantir Strong Session Serializability
- O histórico de transações é indistinguível de um histórico executado em alguma ordem total
- Essa ordem é consistente com a ordem observada em cada peer
d/db retorna uma cópia do banco atualizada de forma assíncrona, então stale reads são possíveis
- A própria documentação do Datomic explicita que leituras em peer podem não observar algumas transações recentemente commitadas
d/sync sincroniza com o transactor para evitar stale reads
- A validação experimental ainda tem limitações
- É possível provar a existência de bugs, mas não sua ausência
- Erros de corretude no sistema de armazenamento do qual o Datomic depende podem levar a violações das garantias do Datomic
- O Datomic sobre DynamoDB é tão seguro quanto a operação compare-and-set do DynamoDB
Semântica interna de transações: concorrência, não ordem
- A maioria dos bancos de dados e das principais formalizações de isolamento transacional oferece semântica de execução serial dentro de uma transação
- Se for
set x = 1; read x;, o read normalmente vê 1
- Formalizações como as de Adya, Cerone·Bernardi·Gotsman e Crooks·Alvisi·Pu·Clement explicitam a ordem das operações dentro da transação e a propriedade de que “um read posterior observa um write anterior”
- A transaction request do Datomic é uma lista ordenada, mas a execução não preserva essa ordem
add, retract e transaction function se comportam como se fossem executadas simultaneamente entre si
- A
transaction function sempre observa o estado do DB no momento em que a transação começa
- Ela não vê os efeitos de assertion, retraction ou transaction function anteriores
- Se o mesmo CaS for inserido duas vezes para uma entity cujo valor atual é
0, no Datomic ambos veem o estado inicial 0 e têm sucesso
[[:db/cas 123 :internal/value 0 1]
[:db/cas 123 :internal/value 0 1]]
- Em um modelo serial, o primeiro CaS mudaria o valor para
1 e o segundo CaS deveria falhar
- No Datomic, os dois CaS criam assertions duplicadas e o valor final fica em
1
- Duas
increment transaction function também produzem um resultado diferente do modelo serial
[['internal/increment "x"]
['internal/increment "x"]]
- Se o valor inicial for
0, o resultado no modelo serial é 2
- No Datomic, as duas funções veem o estado inicial
0 e o valor final fica em 1
- A
transaction function também não vê assertions anteriores
[[:db/add id-of-x :internal/value 1]
['internal/increment "x"]]
- No Datomic, o valor final fica em
1, não 2
lookup ref também usa o estado do DB no início da transação
- Não é possível adicionar uma entity na mesma transação e depois referenciá-la por
lookup ref
- Nesse caso, a transação é abortada com o erro
Unable to resolve entity
Detecção de conflitos e pseudo write skew
- O Datomic aborta com
:db.error/datoms-conflict se, dentro da mesma transação, forem feitas assertions de valores diferentes para um atributo de cardinalidade única
- Se for feito um assertion do valor
2 sobre um valor inicial 0, e ao mesmo tempo uma increment function gerar uma assertion do valor 1, ocorre conflito
- Essa detecção de conflitos pode evitar muitos resultados surpreendentes causados pela composição incorreta de
transaction function
- Quando o write set contém pares
[entity, attribute] diferentes, só a detecção de conflitos dificulta preservar invariantes
- A carga de trabalho de grant mostra esse caso
approve e deny verificam, cada um, se o grant ainda não foi approved nem denied e então adicionam atributos diferentes
- Em transações diferentes, chamar
approve e deny faz com que as transações Serializable do Datomic garantam o invariante
- Mas, se ambos forem chamados juntos na mesma transação, os dois veem o estado inicial e têm sucesso
[['grant/approve id]
['grant/deny id]]
- Como resultado, o grant passa a ter
approved-at e denied-at ao mesmo tempo
- O invariante “um grant não pode ser approved e denied ao mesmo tempo” é quebrado
- O verificador de conflitos em transação do Datomic não bloqueia isso porque as duas funções geraram assertions para atributos diferentes
- Esse fenômeno é semelhante ao Write Skew de Berenson e outros
- Como as duas funções não veem os efeitos uma da outra, surge um ciclo de anti-dependência read-write
- Se tratarmos
transaction function como transações, isso é semelhante à anomalia G2-item proibida por Repeatable Read e Serializability
- Datomic e Nubank consideram esse comportamento não como bug, mas como comportamento esperado do Datomic
- A Nubank planeja manter a semântica concorrente intra-transação do Datomic
Reforçando invariantes com entity predicate
- O Datomic oferece mecanismos de restrição como tipo, uniqueness, predicate de atributo específico e entity predicate
- entity predicate recebe o estado candidato do DB com todos os efeitos da transação aplicados e o ID da entity, e retorna
true ou false para indicar se o commit deve ser permitido
- Apesar do nome entity predicate, ele pode acessar o estado completo do DB, então também permite expressar restrições globais além de uma entity específica
- No exemplo do grant, o predicate
valid-grant? pode ser usado para impedir que approved-at e denied-at existam ao mesmo tempo
(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))))
- No schema, é possível adicionar uma entity spec para referenciar esse predicate
- O entity predicate ligado a uma entity spec não é aplicado automaticamente a todas as transações
- O Datomic considera a aplicação de entity spec uma decisão de domínio, e entende que cada transação deve solicitá-la explicitamente
- As funções
approve e deny podem solicitar a aplicação da entity spec após adicionar os atributos, usando o virtual datom :db/ensure
(defn approve
[db id]
[[:db/add id :grant/approved-at (Date.)]
[:db/add id :db/ensure :grant/valid?]])
- Com essa entity spec, ao tentar
approve e deny juntos na mesma transação, ocorre um erro de entity predicate e o invariante é preservado
- O erro inclui
:db.error/entity-pred e :db.error/pred-return false
Mudanças na documentação e recomendações aos usuários
- A Datomic revisou significativamente a documentação após colaborar com o Jepsen
- A documentação de segurança de transações passa a refletir as garantias de segurança mais fortes que a Datomic considera de fato oferecer
- Especifica serializabilidade global, monotonicidade por peer e strict serializability para escritas ou leituras com
sync
- O argumento de “single-writer” foi removido da documentação de segurança
- A documentação de sintaxe e semântica de transações agora cobre de forma abrangente a estrutura de uma transaction request, as regras de expansão da forma de mapa e das transaction functions, e o processo de aplicação da transação
- A documentação de transaction functions também foi revisada
- Explica vários mecanismos que garantem consistência, a criação e invocação de funções, e o comportamento das funções embutidas
- Foram removidas as afirmações de que transaction functions podem “atomically analyze and transform database values” ou garantir “atomic read-modify-write processing”
- A Datomic pretende passar a chamar a estrutura de dados enviada a
d/transact de transaction request, e não de “transaction”
- Seus elementos devem ser chamados de “data”, em vez de “statements” ou “operations”
[:db/add ...] e [:db/retract ...] são, respectivamente, uma assertion request e uma retraction request
- Isso ajuda a distinguir entre um assertion datom real e uma assertion request incompleta dentro de uma transaction request
- Os cuidados necessários para os usuários são claros
- A serializabilidade entre transações da Datomic é confiável
- A semântica de execução concorrente dentro de uma transação é uma escolha incomum, então é preciso cuidado ao chamar várias transaction functions na mesma transação
- Em especial, deve-se ter cuidado quando o conjunto de leitura se sobrepõe e o conjunto de escrita é separado
- Vários increments podem silenciosamente colapsar em uma única update
- É possível usar attribute predicates e entity specs, mas entity specs precisam ser solicitadas explicitamente em todas as transações necessárias
- Do ponto de vista operacional, também é preciso considerar reinicializações do transactor e variações de rede
- O transactor da Datomic encerra a si mesmo se não conseguir se comunicar com o storage por alguns minutos
- O Jepsen recomenda adicionar um loop de retry ao transactor para torná-lo mais resiliente a variações de rede
Limitações e questões para pesquisas futuras
- Este teste ainda deixou itens fora do escopo de avaliação
- Excision e historical query não foram avaliados
- A client library da Datomic também não foi investigada, mas o Jepsen considera provável que seu comportamento seja semelhante ao dos peers usados nos testes
- Apenas o DynamoDB foi usado como mecanismo de storage
- O Datomic Cloud também não foi avaliado, e o Datomic Cloud usa uma arquitetura um pouco diferente
- O Jepsen afirma conhecer pouquíssimos sistemas ou formalizações que ofereçam ao mesmo tempo serializabilidade entre transações e semântica concorrente dentro da transação
- O modelo da Datomic levanta várias questões de pesquisa
- Se uma transaction da Datomic pode ser vista como o dual de uma transação tradicional, ou como um modelo de “co-transaction”
- Se as vantagens e desvantagens desse modelo podem ser mitigadas com static analysis, runtime checks ou extensões de API
- Qual é a probabilidade de usuários reais escreverem transações que violem invariantes
- Como comparação, são citados o projeto de pesquisa de Datalog temporal Alvaro’s Dedalus e a Fauna
- O Dedalus, assim como a Datomic, faz com que as transações ocorram “all at once”
- A Fauna é um banco de dados temporal que também oferece strong serializability e, ao contrário da Datomic, aparentemente fornece execução serial e efeitos colaterais incrementais dentro da transação
- A semelhança entre o verificador de conflitos no fim da transação da Datomic e a regra first-committer-wins do Snapshot Isolation também continua sendo uma oportunidade de pesquisa
- Que partes da literatura sobre Snapshot Isolation podem ser aplicadas à Datomic
- Por quais anti-dependency edges os ciclos dentro de transações da Datomic são representados
- Ainda permanece a questão de se existem análogos na semântica interna para fenômenos como lost update, Fractured Read, read-only transaction anomaly e Long Fork
- A conexão com o teorema CALM também pode ser explorada mais a fundo
- Se transaction functions logicamente monotônicas podem ser combinadas com segurança dentro da mesma transaction da Datomic
- Também é possível investigar se programas Datalog sem negação permanecem seguros nesse modelo de execução
1 comentários
Comentários do Hacker News
Eu acompanhei esse trabalho de perto, e foi realmente fascinante ver o processo de discussão
Também foi surpreendente que o Jepsen não tenha encontrado nenhum bug crítico, e só o fato de ter deixado a documentação e os comportamentos intencionalmente peculiares mais claros já foi um resultado muito útil
Considerando que eles operam um banco usando Datomic, isso já valeu a pena como exercício de construção de confiança
É um texto realmente excelente, e sempre que começo a me achar muito inteligente, ler uma análise do Jepsen é uma boa forma de recuperar a humildade
Foi a primeira vez que li um relatório do Jepsen com profundidade, e gostei da parte que explica com clareza o funcionamento interno das transações do Datomic
Também percebi o quanto eu não entendia a diferença entre transações do Datomic e transações de bancos de dados SQL
Em especial, chamou minha atenção o trecho: “No passado, o Datomic chamava a estrutura de dados passada para
d/transactde ‘transaction’, e seus elementos de ‘statements’ ou ‘operations’. No futuro, pretendemos chamar essa estrutura de ‘transaction request’ e seus elementos de ‘data’”Fico curioso sobre o que isso significa para o d/transact-async e funcionalidades relacionadas no namespace
datomic.apiFaz quase um ano que não uso Datomic, e parece que muita coisa mudou
Todas as funções de
datomic.apicontinuam as mesmasUm relatório excelente e detalhado sobre um banco de dados realmente muito bom
Também é muito bem-vindo ver a documentação ficar mais clara e ser atualizada
Além disso, eu realmente adoraria que a Apple pagasse por uma análise do Jepsen sobre o FoundationDB
Sei que o Aphyr disse que “os testes deles provavelmente são melhores”, mas se o Jepsen de fato não encontrar problemas no FoundationDB, isso também seria uma forte evidência de que ele é outro banco de dados excelente
Não conheço bem essa área, mas quando vejo alguém dizer “seria ótimo se $foo pagasse por isso”, meus ouvidos se levantam
Há capital de sobra por aí, mas pela minha experiência esperar a Apple fazer alguma coisa costuma demorar muito
Foi impressionante que o Jepsen tenha encontrado uma situação clara que leva a uma violação de invariantes, e a resposta do lado do Datomic pareça ter sido apenas esclarecer a documentação
No fim, isso significa que a equipe do Datomic aceita que esse tipo de violação acontece, mas não se importa?
O texto diz: “na perspectiva do Datomic, a violação de invariantes no workload de grant é erro do usuário. Funções de transação não são executadas de forma atômica em sequência. Se outras operações na transação puderem invalidar essas pré-condições, não é seguro verificar pré-condições em funções de transação”
Para ver o que isso significa do ponto de vista do usuário, dá para pensar nos seguintes pseudodados de transação
[ [Stu favorite-number 41] ;; maybe more stuff [Stu favorite-number 42] ]Lendo isso de forma operacional, parece que no começo da transação eu gostava de 41 e depois passei a gostar de 42
Espera-se que, após o fim da transação, um observador veja que eu gosto apenas de 42, e seria preciso se preocupar em quais condições 41 poderia ser visto
Essa interpretação operacional da semântica interna da transação é comum em vários bancos de dados, mas pressupõe que existam vários momentos dentro da transação
O Datomic não tem esses momentos nem os quer, e prefere que você não precise se preocupar com o que aconteceu “no meio da transação”
No Datomic, todos os fatos de uma transação acontecem no mesmo instante, então essa transação diz que eu comecei a gostar dos dois números ao mesmo tempo
Se você ler uma transação do Datomic incorretamente como uma combinação de várias operações, naturalmente poderá encontrar todo tipo de “anomalia de invariantes”
Por outro lado, se você também aplicar incorretamente o modelo do Datomic a uma transação SQL, poderá encontrar “anomalias de invariantes”
Por causa desse potencial de mal-entendido, era necessária uma boa documentação, e junto com o Jepsen tentaram melhorar a documentação [1] para ajustar expressões descuidadas e reduzir ambiguidades
Também adicionaram uma nota técnica tratando diretamente desse mal-entendido específico [2]
[1] https://docs.datomic.com/transactions/transactions.html#tran...
[2] https://docs.datomic.com/tech-notes/comparison-with-updating...
O quanto isso importa na prática depende de o usuário estar escrevendo funções de transação para preservar algum invariante e de esse invariante só ser preservado quando a função é executada sequencialmente, e não de forma simultânea
A posição do Datomic — ou seria bom se alguém do lado do Datomic interviesse para dizer — é que os usuários não costumam escrever esse tipo de função de transação com tanta frequência
Essa posição é defensável. A documentação já declarava que funções de transação observam o estado no início da transação, e não umas às outras
Por outro lado, a documentação também tinha uma formulação que sugeria que funções de transação poderiam ser usadas 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 causa dessa formulação e do fato de que quase todos os outros bancos de dados serializáveis usam semântica sequencial dentro da transação, o relatório dedicou bastante espaço a esse tema
É uma questão complexa e não há uma resposta clara; gostaria de ouvir como a comunidade geral de bancos de dados e especialmente os usuários do Datomic entendem essa semântica
Se você disser “uma situação que leva à violação de invariantes”, parece um bug do Datomic, mas não é isso
É preciso entender como o Datomic processa transações e escrever o código de acordo com isso
Não tem relação com a Nubank, mas usando o Datomic como banco de dados de uso geral nunca passei por uma situação em que isso tenha sido um problema
Para quem não sabe, o nome Jepsen é um trocadilho vindo de Carly Rae Jepsen, que canta “call me maybe”
Acho perfeito como nome de um projeto de pesquisa em sistemas distribuídos
O primeiro commit é de 2013
https://github.com/jepsen-io/jepsen/tree/4b112e7046a20efa80a...
A música saiu em 2011, e o tempo realmente voa
Não usei Datomic por muito tempo em produção, mas ele é tão peculiar que fico me perguntando se há algo realmente surpreendente nisso tudo
Transações do Datomic são basicamente mais próximas de um batch, e como eu sempre pensei nelas como single-threaded, parece natural que não haja muitas race conditions
Por projeto, ele fica mais para o lado lento e seguro
Parece algo com que seria preciso ter bastante cuidado
Obrigado, Kyle
Está claro que nossa documentação era insuficiente
Tentamos escrever, junto com o Rich, uma documentação mais clara e abrangente sobre o modelo de transações do Datomic
Esperamos conseguir evitar mal-entendidos comuns de antemão, e todo feedback é bem-vindo
https://docs.datomic.com/transactions/model.html
O modelo de dados do Datomic parece bastante intuitivo se você já está acostumado com armazenamento em triplas ou RDF
Mas essa semelhança não é mencionada com frequência na documentação nem nas discussões online
Fico curioso se isso é porque as pessoas não estão familiarizadas com esses conceitos, ou porque veem as associações com a web semântica como algo que atrapalha, ou se existe alguma diferença fundamental que eu deixei passar
Eu estava realmente esperando por esta análise
Ultimamente estou criando por conta própria um armazenamento de dados parecido com o Datomic, então acho que vai ser útil, e estou lendo agora
A análise do MongoDB também foi interessante, e seria ótimo ver outras análises como Redis, RethinkDB etc.
Em algum momento, também seria legal ter uma análise sobre rqlite/dqlite ou turso/libsql
Houve uma análise no estilo Jepsen sobre o rqlite há 2 anos [1]
O artigo está aqui: https://www.philipotoole.com/testing-rqlite-read-consistency...
O relatório em si está aqui: https://github.com/wildarch/jepsen.rqlite/blob/main/doc/blog...
[1] https://www.rqlite.io