Concorrência otimista
Dois comandos decididos sobre a mesma versão: um grava, o outro recebe AggregateVersionError — e tenta de novo.
Dois saques de 60 chegam juntos numa conta com saldo 100. Os dois leem saldo 100, os dois decidem que podem, os dois gravam — e a conta fica com −20, embora decidir recuse saldo insuficiente. A regra estava certa; o problema é que as duas decisões foram tomadas sobre o mesmo estado.
anexar(fluxo, eventos, versao_esperada) fecha isso: grava só se o fluxo ainda está na versão que foi lida. O segundo recebe AggregateVersionError, relê, e decide de novo — agora vendo o saldo 40.
dataforge
adopt Arcane.Dominio as D
// evoluir: o estado depois de um fato. Pura, e nunca recusa nada.
aplicar := {
"ContaAberta": lambda s, d: {"titular": d["titular"], "saldo": 0, "aberta": yes},
"Depositado": lambda s, d: {...s, "saldo": s["saldo"] + d["valor"]},
"Sacado": lambda s, d: {...s, "saldo": s["saldo"] - d["valor"]},
"ContaEncerrada": lambda s, d: {...s, "aberta": no}
}
// decidir: os fatos que um comando produz — ou o motivo da recusa.
action decidir(estado, comando):
tipo := comando["tipo"]
given tipo is "abrir":
yield [{"nome": "ContaAberta", "dados": {"titular": comando["titular"]}}]
given not (estado["aberta"] ?? no):
trigger "a conta não está aberta"
given tipo is "depositar":
yield [{"nome": "Depositado", "dados": {"valor": comando["valor"]}}]
given tipo is "sacar":
given comando["valor"] bigger estado["saldo"]:
trigger $"saldo {estado["saldo"]} não cobre {comando["valor"]}"
yield [{"nome": "Sacado", "dados": {"valor": comando["valor"]}}]
trigger $"comando desconhecido: {tipo}"
armazem := D.armazem()
armazem.anexar("c", [{"nome": "ContaAberta", "dados": {"titular": "Ana"}},
{"nome": "Depositado", "dados": {"valor": 100}}])
// os dois leram a MESMA versão
lido := armazem.versao("c")
estado := D.reconstituir(armazem.ler("c"), aplicar)
saque_a := decidir(estado, {"tipo": "sacar", "valor": 60})
saque_b := decidir(estado, {"tipo": "sacar", "valor": 60})
armazem.anexar("c", saque_a, lido) // o primeiro grava
recusado := no
monitor:
armazem.anexar("c", saque_b, lido) // o segundo, não
handle AggregateVersionError:
recusado := yes
assert recusado
assert D.reconstituir(armazem.ler("c"), aplicar)["saldo"] is 40Tentar de novo — com teto#
dataforge
adopt Arcane.Dominio as D
// evoluir: o estado depois de um fato. Pura, e nunca recusa nada.
aplicar := {
"ContaAberta": lambda s, d: {"titular": d["titular"], "saldo": 0, "aberta": yes},
"Depositado": lambda s, d: {...s, "saldo": s["saldo"] + d["valor"]},
"Sacado": lambda s, d: {...s, "saldo": s["saldo"] - d["valor"]},
"ContaEncerrada": lambda s, d: {...s, "aberta": no}
}
// decidir: os fatos que um comando produz — ou o motivo da recusa.
action decidir(estado, comando):
tipo := comando["tipo"]
given tipo is "abrir":
yield [{"nome": "ContaAberta", "dados": {"titular": comando["titular"]}}]
given not (estado["aberta"] ?? no):
trigger "a conta não está aberta"
given tipo is "depositar":
yield [{"nome": "Depositado", "dados": {"valor": comando["valor"]}}]
given tipo is "sacar":
given comando["valor"] bigger estado["saldo"]:
trigger $"saldo {estado["saldo"]} não cobre {comando["valor"]}"
yield [{"nome": "Sacado", "dados": {"valor": comando["valor"]}}]
trigger $"comando desconhecido: {tipo}"
armazem := D.armazem()
armazem.anexar("c", [{"nome": "ContaAberta", "dados": {"titular": "Ana"}}])
action com_retentativa(conta, comando, tentativas := 3):
cycle i from 1 to tentativas:
historia := armazem.ler(conta)
novos := decidir(D.reconstituir(historia, aplicar), comando)
monitor:
yield armazem.anexar(conta, novos, len(historia))
handle AggregateVersionError:
skip
trigger $"'{conta}' mudou {tentativas} vezes seguidas; desisti"
assert com_retentativa("c", {"tipo": "depositar", "valor": 10}) is 2- Releia e decida de novo. Regravar os mesmos eventos ignoraria o que mudou — é exatamente o erro que o conflito existe para impedir.
- Tenha teto. Um fluxo disputado demais é sinal de agregado grande demais; retentar para sempre esconde isso.
- Otimista, porque conflito é raro. Travar a conta a cada leitura seria pagar sempre por algo que quase nunca acontece.
É o mesmo mecanismo do If-Match numa API: ver Edição concorrente.