Monalisa
Background work

Idempotência

Efeito uma vez só sobre entrega pelo menos uma vez, com Valkey na frente e PostgreSQL como verdade.

A fila entrega cada mensagem pelo menos uma vez. Para que o efeito aconteça uma vez só, todo handler roda pelo GatedIdempotencyRunner, com o run_id como chave (ou uma chave de negócio que a task declare).

CamadaPapel
Valkey (SET NX PX)Portão: barra uma duplicata concorrente sem abrir transação e guarda o resultado para replay rápido
PostgreSQL, na transação do efeitoFonte da verdade: a chave e o resultado fazem commit ou somem junto com o efeito
Depois do commitO resultado é copiado para o Valkey
Valkey sem a chaveLê do PostgreSQL e repopula
Valkey fora do arCai para o PostgreSQL: mais lento, ainda correto

A gravação no PostgreSQL é síncrona, na mesma transação do efeito, e é dela que vem a garantia. Gravar a chave de forma assíncrona abriria dois buracos: o Valkey perder a chave antes da gravação (o efeito rodaria de novo) ou o efeito fazer rollback com a chave já marcada como concluída (o efeito se perderia).

Uma reentrega que encontra a chave em processamento volta para a fila depois do lease; um resultado completed é devolvido como replay.

Duas tabelas, por dono

Chaves de tenant ficam em idempotency_keys, com FK para tenants; chaves de trabalho acima dos tenants ficam em platform_idempotency_keys, sem coluna de tenant. Não há valor sentinela (como um tenant 0): uma chave de tenant só existe para um tenant real, e o banco recusa o resto.

A chave e a impressão digital da requisição são digests de 32 bytes. outcome é {} enquanto processing e o resultado (ou o detalhe da falha) depois; o banco garante as duas coisas por CHECK.

idempotency_keys

Ledger de idempotência das operações de tenant: uma linha por (tenant, chave), reivindicada com INSERT … ON CONFLICT DO NOTHING na mesma transação do efeito. expires_at é o prazo do lease enquanto processing e o prazo de retenção depois.

ColunaTipoNuloObservação
idbigintnãoidentity (GENERATED ALWAYS)
tenant_idbigintnão
idempotency_keybyteanão
request_fingerprintbyteanão
statustextnãopadrão 'processing'
attemptintegernãopadrão 1
outcomejsonbnãopadrão '{}'::jsonb
expires_attimestamptznão
created_attimestamptznãopadrão clock_timestamp()
updated_attimestamptznãopadrão clock_timestamp()

Chaves estrangeiras

  • tenant_id → tenants.id

Garantido pelo banco

TipoNomeDefinição
CHECKidempotency_keys_attempt_checkCHECK ((attempt > 0))
CHECKidempotency_keys_checkCHECK (((status <> 'processing') OR (outcome = '{}'::jsonb)))
CHECKidempotency_keys_idempotency_key_checkCHECK ((octet_length(idempotency_key) = 32))
CHECKidempotency_keys_outcome_checkCHECK (((jsonb_typeof(outcome) = 'object') AND (octet_length((outcome)::text) <= 4096)))
CHECKidempotency_keys_request_fingerprint_checkCHECK ((octet_length(request_fingerprint) = 32))
CHECKidempotency_keys_status_checkCHECK ((status IN ('processing', 'completed', 'failed_retryable', 'failed_permanent')))
PKidempotency_keys_pkeyPRIMARY KEY (id)
UNIQUEidempotency_keys_owner_keyUNIQUE (tenant_id, idempotency_key)

Fora do banco

  • A limpeza de chaves vencidas é feita pela task maintenance.purge.

platform_idempotency_keys

O mesmo ledger para trabalho acima dos tenants, sem coluna de tenant: nenhuma chave de tenant usa valor sentinela.

ColunaTipoNuloObservação
idbigintnãoidentity (GENERATED ALWAYS)
idempotency_keybyteanão
request_fingerprintbyteanão
statustextnãopadrão 'processing'
attemptintegernãopadrão 1
outcomejsonbnãopadrão '{}'::jsonb
expires_attimestamptznão
created_attimestamptznãopadrão clock_timestamp()
updated_attimestamptznãopadrão clock_timestamp()

Chaves estrangeiras

  • nenhuma

Garantido pelo banco

TipoNomeDefinição
CHECKplatform_idempotency_keys_attempt_checkCHECK ((attempt > 0))
CHECKplatform_idempotency_keys_checkCHECK (((status <> 'processing') OR (outcome = '{}'::jsonb)))
CHECKplatform_idempotency_keys_idempotency_key_checkCHECK ((octet_length(idempotency_key) = 32))
CHECKplatform_idempotency_keys_outcome_checkCHECK (((jsonb_typeof(outcome) = 'object') AND (octet_length((outcome)::text) <= 4096)))
CHECKplatform_idempotency_keys_request_fingerprint_checkCHECK ((octet_length(request_fingerprint) = 32))
CHECKplatform_idempotency_keys_status_checkCHECK ((status IN ('processing', 'completed', 'failed_retryable', 'failed_permanent')))
PKplatform_idempotency_keys_pkeyPRIMARY KEY (id)
UNIQUEplatform_idempotency_keys_keyUNIQUE (idempotency_key)

Fora do banco

  • A limpeza de chaves vencidas é feita pela task maintenance.purge.

Nesta página