Em Agentic AI, o problema raramente é “o modelo errou uma frase”.
O problema é a Execução: duas instâncias do agente rodando ao mesmo tempo, retries legítimos, filas que reentregam, timeouts que cortam a confirmação.
Se a ação tem Efeito Colateral (cobrar, cancelar, agendar, bloquear, reembolsar), concorrência vira multiplicador de dano.
Contexto — o mundo real reprocessa, seu fluxo não pode duplicar o mundo
Redis entra aqui como um Portão de Execução: você cria um Lock por Recurso (pedido, cliente, reserva, cobrança) e dá ao sistema uma forma explícita de dizer “só uma execução pode ganhar o direito de agir agora”. Isso não resolve tudo, mas muda a natureza do incidente: de duplicidade caótica para contenção governável.
O desafio — Lock sozinho não basta sem Janela de Recuo e Trilha de Execução
O padrão frágil é tratar Lock como “segurança” e esquecer que o Lock pode expirar, o processo pode cair, e o mundo externo pode ter sido alterado parcialmente. Aí você ganha o pior dos dois mundos: não sabe se executou, não sabe se deve reexecutar, e não tem um caminho curto para desfazer.
Por isso, em Execução Crítica, Lock precisa andar junto com duas ideias: uma Janela de Recuo (um período em que desfazer é permitido e esperado) e uma Trilha de Execução mínima (para afirmar o que foi tentado, o que foi feito e em que estado ficou).
Sem isso, Redis vira só um “semaforo” que reduz concorrência, mas não protege a cauda.
O cenário — duas execuções legítimas, um efeito colateral duplicado
Um agente recebe o mesmo evento duas vezes (reentrega), ou duas instâncias processam a mesma fila (paralelismo), ou uma ação dá timeout e o sistema tenta de novo (retry). Se não houver Lock, as duas execuções avançam. Se houver Lock, uma delas deveria parar.
O problema é quando a execução “ganha o Lock”, dispara a ação externa, e cai antes de confirmar. O Lock expira e uma segunda execução assume. Sem Trilha de Execução e sem Janela de Recuo, a operação entra no modo mais caro: arqueologia e mutirão.
Implicações — do Lock simples ao Lock com recuo governável
Abaixo está um Lock simples (bom como primeiro degrau) para serializar Execução por Recurso e reduzir duplicidade. Ele não é uma “garantia universal”, mas já impede o cenário mais comum: duas execuções simultâneas aplicarem o mesmo Efeito Colateral.
# redis-py
import time
import uuid
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
LOCK_TTL_MS = 15_000 # curto: o lock existe para conter concorrência, não para "segurar estado"
# Release seguro (só quem tem o token libera)
RELEASE_LUA = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
def acquire_lock(lock_key: str, ttl_ms: int = LOCK_TTL_MS) -> str | None:
token = str(uuid.uuid4())
ok = r.set(lock_key, token, nx=True, px=ttl_ms)
return token if ok else None
def release_lock(lock_key: str, token: str) -> bool:
return r.eval(RELEASE_LUA, 1, lock_key, token) == 1
def run_critical(resource_id: str):
lock_key = f"lock:{resource_id}"
token = acquire_lock(lock_key)
if not token:
return {"ok": False, "action": "pause_or_retry_later", "reason": "Lock Busy"}
try:
# Ação Crítica (Efeito Colateral) deve acontecer aqui
# Ex.: disparar cobrança, criar reserva, cancelar pedido, conceder reembolso
return {"ok": True, "status": "done"}
finally:
release_lock(lock_key, token)
Esse padrão reduz concorrência, mas ainda deixa uma pergunta perigosa em falha parcial: “executou e caiu, ou não executou?”. É aqui que entra a Janela de Recuo: você não quer só impedir duas execuções; você quer tornar a Execução contestável e reversível dentro de um período definido, com estado explícito.
Abaixo, um desenho mais completo: ele adiciona Token de Fencing (para evitar que uma execução antiga “volte” e escreva depois), registra uma Operação com Status e Deadline de Recuo, e trata Reversão como um caminho legítimo quando algo saiu do esperado.
# redis-py (padrão mais completo)
import time
import uuid
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
LOCK_TTL_MS = 15_000
UNDO_WINDOW_SEC = 10 * 60 # Janela de Recuo: 10 minutos (exemplo)
RELEASE_LUA = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
def now_sec() -> int:
return int(time.time())
def acquire_with_fencing(resource_id: str) -> tuple[str, int] | None:
lock_key = f"lock:{resource_id}"
token = str(uuid.uuid4())
ok = r.set(lock_key, token, nx=True, px=LOCK_TTL_MS)
if not ok:
return None
# Token de Fencing: monotônico por recurso (evita execução antiga "ganhar depois")
fence = int(r.incr(f"fence:{resource_id}"))
return token, fence
def release(resource_id: str, token: str) -> None:
r.eval(RELEASE_LUA, 1, f"lock:{resource_id}", token)
def start_operation(resource_id: str, fence: int, kind: str, evidence_min: dict) -> str:
op_id = str(uuid.uuid4())
op_key = f"op:{op_id}"
r.hset(op_key, mapping={
"resource_id": resource_id,
"fence": fence,
"kind": kind,
"status": "PENDING",
"created_at": now_sec(),
"undo_deadline": now_sec() + UNDO_WINDOW_SEC,
"evidence": str(evidence_min), # mantenha sem dados sensíveis
})
r.expire(op_key, UNDO_WINDOW_SEC + 3600)
return op_id
def mark_done(op_id: str, outcome: dict) -> None:
r.hset(f"op:{op_id}", mapping={
"status": "DONE",
"done_at": now_sec(),
"outcome": str(outcome),
})
def request_undo(op_id: str) -> dict:
op_key = f"op:{op_id}"
op = r.hgetall(op_key)
if not op:
return {"ok": False, "reason": "Unknown Operation"}
deadline = int(op["undo_deadline"])
if now_sec() > deadline:
return {"ok": False, "reason": "Undo Window Expired", "action": "escalate"}
# Idempotência de Reversão: só uma reversão pode “ganhar”
undo_key = f"undo:{op_id}"
if not r.set(undo_key, "1", nx=True, ex=UNDO_WINDOW_SEC):
return {"ok": True, "status": "already_undone"}
# Compensação (exemplo): aqui você chamaria a operação reversa
# Ex.: estornar cobrança, cancelar reserva, reverter status
r.hset(op_key, mapping={"status": "UNDONE", "undone_at": now_sec()})
return {"ok": True, "status": "undone"}
def execute_critical(resource_id: str, kind: str, evidence_min: dict) -> dict:
acquired = acquire_with_fencing(resource_id)
if not acquired:
return {"ok": False, "action": "pause_or_retry_later", "reason": "Lock Busy"}
token, fence = acquired
try:
op_id = start_operation(resource_id, fence, kind, evidence_min)
# Ação Crítica: idealmente, o downstream valida fence (Write Barrier) para evitar reordenação.
# Ex.: incluir fence no comando; o destino rejeita fence menor que o último aplicado ao resource_id.
result = {"op_id": op_id, "status": "executed"}
mark_done(op_id, outcome=result)
return {"ok": True, **result}
finally:
release(resource_id, token)
O ganho operacional desse segundo padrão é que ele transforma falha parcial em um estado governável: você não depende de “memória do time” para decidir se reprocessa, se pausa, ou se reverte. Existe uma Operação com Status, uma Janela de Recuo explícita e um caminho de Compensação idempotente.
Síntese final — Lock reduz concorrência; Janela de Recuo reduz crise
Redis Lock é um primeiro degrau para impedir que duas execuções “ganhem” ao mesmo tempo. Mas Execução Crítica pede mais do que exclusão mútua: pede um modo de declarar o que foi tentado e um modo legítimo de desfazer dentro de uma janela.
Em Agentic AI, isso vira uma diferença prática: o agente pode agir com menos medo quando existe contenção, Trilha de Execução e Recuo praticável. Sem isso, a operação só descobre o custo da autonomia quando a cauda cobra.
O que ainda poderia melhorar — sinais de próxima maturidade
O próximo degrau aparece quando o Token de Fencing é validado no destino como Write Barrier, quando Operações críticas carregam Evidência mínima consistente, quando Reversão é possível para classes claras de Efeito Colateral, quando existe Kill Switch para Modo Seguro sob incidente, quando a organização mede quanto entrou em Pausa e por quê, e quando recuo deixa de ser heroísmo porque a Janela de Recuo é parte do contrato do fluxo.