Code: Redis para Locks e Janela de Recuo em Execução Crítica

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.

Veja também: