# Auditoria do `conn_limited`: causa-raiz, evidencias e analise de lacunas

**Data:** 2026-07-25
**Ambiente:** producao (`monetarie-greenfield-prod`), conta 990933657879, sa-east-1
**Fonte:** CloudWatch `/ecs/monetarie/prod/pix-api` e `/ecs/monetarie/homolog/pix-api`,
estado vivo do Redis e do Postgres por RPC no release em execucao, e leitura do codigo.
**Metodo:** so entra aqui o que foi medido. O que e hipotese esta marcado como hipotese.

---

## 1. Sumario executivo

O `conn_limited` **nao e o BACEN nos recusando conexao**. E o **nosso proprio
semaforo Redis** negando permit (`client.ex:513`). O alarme estava certo; a leitura
que se fazia dele e que estava errada.

A causa-raiz e um **split-brain da eleicao de lider do ICOM**: o `Coordinator`
toma um `pg_try_advisory_lock` numa conexao do **pool Ecto compartilhado**, e
**nunca mais verifica se ainda o detem**. Quando o Postgrex derruba aquela conexao
(o que acontece ~10 vezes por hora em producao), o Postgres solta o lock sozinho,
mas o Coordinator segue se achando lider com seus 6 workers vivos. Um standby entao
adquire o lock e vira lider tambem. Cada lider extra soma **+6 workers** disputando
os **mesmos 6 permits**.

Isso e **cronico e recorrente**, nao um incidente isolado: em 48h houve pelo menos
5 episodios no canal CPM, um deles com **3 lideres simultaneos por cerca de 8 horas**.

O `conn_limited` e o **sintoma saudavel**: foi o semaforo fazendo o trabalho dele e
impedindo que 18 workers abrissem 18 conexoes ao BACEN. **Sem ele, teriamos levado
429 em massa e o inbound morreria** (incidente ja documentado no codigo,
`application.ex:57`).

---

## 2. Retratacao: o que o `conn_limited` NAO e

A memoria [[monetarie-icom-6canal-leitura-nao-envio-0724]] tratou o `conn_limited`
como consequencia do teto de 6 do BACEN (§2.2.2.10) sendo drenado pelo envio, e deu
o assunto por RESOLVIDO apos separar os pools de leitura e envio.

**Aquele fix estava certo e continua valendo** (os pools de envio hoje marcam
`in_use=0` e nao roubam leitura). Mas ele **nao era a unica fonte** do
`conn_limited`, e a parte que sobrou e maior.

Prova de que o `conn_limited` e local, nao do BACEN — `apps/shared/lib/shared/bacen/client.ex:512`:

```elixir
case ConnSemaphore.with_slot(sem_canal, ispb, opts, fun) do
  {:error, :no_slot} -> {:error, :conn_limited}
```

`:no_slot` vem do nosso Lua no Redis quando os 6 slots estao tomados, apos esperar
`acquire_timeout_ms` (3s). **Nenhuma requisicao chega a sair para o BACEN nesse
caminho.** Ou seja: durante todo o pico, o BACEN nunca nos recusou nada.

---

## 3. Causa-raiz provada: split-brain da lideranca

### 3.1 O defeito no codigo

`apps/spi_service/lib/spi_service/icom/cpm/coordinator.ex`:

```elixir
defp do_acquire_lock(%{lock_acquired: true} = state), do: state
```

Uma vez lider, **nunca reverifica**. Nao ha renovacao, nao ha heartbeat de
lideranca, nao ha reconciliacao. O unico caminho de volta para standby e o
`worker_supervisor` morrer.

O proprio moduledoc ja registrava o risco, mas o classificou como aceitavel:

> Advisory locks are tied to the PG session (=connection). The Ecto connection pool
> may hand a different conn to consecutive queries from the same Elixir process.

O que passou batido e que **o lock nao precisa de um unlock errado para vazar**:
basta a conexao que o segura **cair**. Ai o Postgres libera, e ninguem no lado da
aplicacao fica sabendo.

### 3.2 Estado vivo que confirma o mecanismo

Lido agora em PRD (`pg_locks` x `pg_stat_activity`):

```
1850724349 (csm) | granted | pid 46170 | state "idle" | monetarie-pix-shared | 10.50.1.64
 395190724 (cpm) | granted | pid 46168 | state "idle" | monetarie-pix-shared | 10.50.1.64
```

Os dois locks estao presos a conexoes em estado **`idle`** dentro do pool
compartilhado `monetarie-pix-shared` (`pool_size` default 8, `runtime.exs:313`).
Uma conexao ociosa do pool e exatamente a candidata a ser derrubada sob pressao.

### 3.3 O gatilho, medido

Disconnects de Postgrex em PRD nas ultimas 6h: **~10 por hora**, do tipo

```
client ... timed out because it queued and checked out the connection
for longer than 10000ms
```

**Agravante de desenho:** `config/runtime.exs:262-273` reduziu
`queue_target` para 1000ms e `queue_interval` para 2000ms deliberadamente, para
"FAIL-FAST do pool sob rajada". Isso protege o pool, mas **aumenta a frequencia com
que conexoes sao descartadas** — e cada descarte e um sorteio que pode levar junto o
advisory lock. A protecao de um subsistema virou gatilho de outro.

### 3.4 Correlacao no minuto

| horario (UTC) | evento medido |
|---|---|
| 00:22:15 e 00:22:30 | 2 disconnects de Postgrex |
| **00:22:33** | `Cpm.Coordinator leader acquired` na task `91a2f1581d4d` |
| **00:22** | primeiro minuto com `conn_limited` (antes: zero) |
| **00:59:46** | `leader acquired` na task `be32a0150edf` (terceiro lider) |

Nenhum `lock released` do CPM entre 21:31:12 e 02:34:05. A task `164c130bd6d2`, que
virou lider as 21:31:12, **so soltou o lock as 02:39:02** — ou seja, seguiu se
achando lider durante todo o periodo em que outras duas tambem eram lideres.

### 3.5 A dose bate com o efeito

| lideres CPM simultaneos | workers | excedentes sobre 6 slots | `conn_limited`/hora |
|---|---|---|---|
| 1 (saudavel) | 6 | 0 | ~0 a 150 |
| 2 | 12 | 6 | **2.622** (00Z) |
| 3 | 18 | 12 | **8.157** (01Z) |

Dobrou o excedente, dobrou o volume. O teto observado (~8.400/h) e consistente com
12 workers re-tentando a cada ~3s de `acquire_timeout`.

---

## 4. E cronico: 48 horas de historico

Episodios de `leader acquired` **sem** `lock released` previo, canal CPM:

| inicio (UTC) | task que entrou | lider anterior so soltou em | duracao aproximada |
|---|---|---|---|
| 07-23 20:16:32 | `a8fcf7de0562` | 21:29:55 | ~1h13 |
| 07-24 08:19:15 | `7c41f3d23883` | 18:01:31 | **~9h** |
| 07-24 08:35:41 | `25cfc2a4385e` (3o) | 16:25:45 | **~8h** |
| 07-24 17:59:12 | `f22ec328d3c3` | 18:01:31 | ~2min |
| 07-25 00:22:33 | `91a2f1581d4d` | 02:34:05 | ~2h |
| 07-25 00:59:46 | `be32a0150edf` (3o) | 02:34:07 | ~1h35 |

O canal CSM tem o mesmo padrao (07-24 14:05:24 e 17:59:12).

**Validacao cruzada com o volume de `conn_limited` por hora:**

| janela | `conn_limited` | lideranca |
|---|---|---|
| 07-24 07Z | 11 | 1 lider |
| 07-24 08Z | **4.848** | 2o e 3o lideres entram (08:19, 08:35) |
| 07-24 09Z-15Z | **~8.450/h** | 3 lideres |
| 07-24 16Z | 5.445 | extras soltam as 16:25 |
| 07-24 17Z | **5** | 1 lider |
| 07-25 00Z | 2.622 | 2 lideres |
| 07-25 01Z | 8.157 | 3 lideres |
| 07-25 02Z+ | **0** | deploy recriou tudo, 1 lider |

A correlacao e 1:1 em **todos** os episodios mensuraveis, nos dois sentidos
(sobe quando entra lider extra, cai quando sai).

**HML nao apresenta o problema:** todos os `leader acquired` de 48h estao pareados
com um `lock released`. Consistente com o gatilho ser pressao no pool de banco, que
HML nao tem. Isso **nao** significa que HML esta imune: o defeito de codigo e o
mesmo, so falta o gatilho.

---

## 5. Impacto: o que de fato aconteceu e o que quase aconteceu

### 5.1 O que NAO aconteceu (medido, para nao alarmar errado)

- **O money-path nao parou.** `icom_received` manteve ~550 msg/h constantes durante
  todo o pico. Os 6 permits seguiram servidos; sobrou fila, nao falta de servico.
- **O BACEN nunca nos recusou.** Nenhuma das tentativas barradas chegou a sair.
- **Nao houve perda de mensagem atribuivel a isso.** A ausencia de pacs.008 desde
  22:00Z e madrugada de sabado.
- **O lag ICOM alto (p95/p99 de 10s a 25s) NAO foi atribuido a isto.** Ele ja estava
  em 25.758ms as 21:00Z, antes do split-brain comecar. E problema separado, em
  aberto, provavelmente ligado a [[monetarie-hsm-rtm-504-latencia-0723]].

### 5.2 O que aconteceu

- **Desperdicio sustentado:** ~8.400 tentativas/hora falhando, por horas seguidas.
  Custo de CPU, de round-trip Redis e de ingestao CloudWatch.
- **Ruido que mascara defeito real:** 8.400 linhas `[error]` por hora tornam o log
  do money-path inutilizavel para diagnostico, e treinam a equipe a ignorar `[error]`.
- **Capacidade de recepcao reduzida na pratica:** 18 workers concorrendo por 6
  permits com `acquire_timeout` de 3s significa que um worker que perde a disputa
  fica 3s parado antes de voltar. A recepcao continuou, mas com menos margem.

### 5.3 O risco latente, que e o ponto mais serio

O `ConnSemaphore` e **fail-OPEN** por decisao explicita (`conn_semaphore.ex`,
`with_slot`): se o Redis ficar indisponivel, a funcao **roda mesmo assim**, apoiada
na premissa de que o pool Finch por pod serve de "cap residual".

**Essa premissa nao se sustenta.** O pool Finch do SPI e `size: 12, count: 1` **por
pod** (`application.ex:101,103`). Com 3 pods, o cap residual real e **36 conexoes
por canal**, contra um teto BACEN de **6**.

Portanto: **split-brain + Redis indisponivel = ate 18 workers abrindo conexao
simultanea, sem nenhuma barreira efetiva** — exatamente o cenario que o comentario
em `application.ex:57` descreve como ja vivido:

> o BACEN respondeu 429 em TODO long-poll — inbound morto

Hoje isso depende de duas falhas simultaneas. Mas uma delas (split-brain) esta
**permanentemente presente** varias horas por dia. O que nos separa do incidente e
apenas o Redis nunca ter piscado numa dessas janelas.

---

## 6. Analise de lacunas

| # | Lacuna | Evidencia | Gravidade |
|---|---|---|---|
| 1 | Lideranca nunca e reverificada apos adquirida | `coordinator.ex` `do_acquire_lock(%{lock_acquired: true}) -> state` | **Critica** |
| 2 | Advisory lock vive em conexao do pool compartilhado, sujeita a descarte | `pg_stat_activity` mostra os 2 locks em conexoes `idle` de `monetarie-pix-shared` | **Critica** |
| 3 | Cap residual do fail-open nao existe de fato (Finch 12/pod x 3 pods = 36 contra teto 6) | `application.ex:101,103` vs `conn_semaphore.ex` | **Critica** |
| 4 | Nenhum alarme para lider duplicado | 5 episodios em 48h, um de ~9h, nenhum detectado ate esta auditoria | **Alta** |
| 5 | Nenhum alarme por taxa de `conn_limited` | 8.400/h por 8h seguidas sem acionar ninguem | **Alta** |
| 6 | `in_use/2` existe mas nao e publicado como metrica | `conn_semaphore.ex` tem a funcao; nada a exporta | **Media** |
| 7 | `HealthMonitor` cobre slot preso, mas nao lideranca | `coordinator.ex start_health_monitor` cuida so dos workers | **Media** |
| 8 | `queue_target`/`queue_interval` agressivos aumentam o gatilho | `runtime.exs:262-273` | **Media** |
| 9 | Sem fencing: worker de lider deposto continua operando | nada no `Worker` valida epoca/geracao | **Media** |
| 10 | O teste do semaforo cobre o cap, mas nao ha teste de lider duplicado | `conn_semaphore_test.exs` nao tem cenario de 2 coordinators | **Media** |

---

## 7. Recomendacoes, em ordem de retorno

### P0 — fecham a causa-raiz

1. **Reverificar a lideranca periodicamente.** No tick do Coordinator (5s), quando
   `lock_acquired: true`, confirmar a posse com
   `pg_advisory_lock_owner`/`SELECT ... FROM pg_locks` ou simplesmente re-tentar o
   `pg_try_advisory_lock`: se voltar `false` **e nao formos o dono**, derrubar o
   `worker_supervisor` e voltar a standby. Isso sozinho encerra o split-brain, porque
   o lider deposto sai em ate 5s.

2. **Conexao dedicada para o lock.** Subir um `Postgrex` proprio do Coordinator
   (fora do pool Ecto), de forma que a vida do lock seja a vida daquele processo, e
   nao de uma conexao emprestada. Elimina a classe inteira do problema, em vez de
   detectar depois.

3. **Cap residual real no fail-open.** Ou reduzir o pool Finch do SPI para caber no
   teto mesmo com N pods, ou trocar o fail-open por "fail-open degradado" (permitir
   so 1 conexao por pod quando o Redis some). Do jeito atual, o unico guarda-corpo
   contra o 429 em massa e um Redis 100% disponivel.

### P1 — para nunca mais descobrir isso por acaso

4. **Alarme de lider duplicado.** Metrica de lideres por `(canal, ispb)`; alarme se
   `> 1` por mais de 30s. A telemetria `:leader_acquired` ja e emitida
   (`coordinator.ex`), falta agregar e alarmar.
5. **Alarme por taxa de `conn_limited`.** Metric filter no log group; alarme acima de
   ~60/min sustentados por 5 min. Teria disparado as 08:20 de 24/07.
6. **Publicar `ConnSemaphore.in_use/2`** como telemetria periodica por pool. Com essa
   serie, o split-brain teria sido visivel como fila crescente, nao como erro.

### P2 — endurecimento

7. **Fencing por epoca:** o Coordinator carimba uma geracao ao virar lider; os
   workers levam a geracao na aquisicao do permit e a sessao/slot rejeita geracao
   antiga. Protege mesmo se a deteccao falhar.
8. **Teste de regressao:** dois Coordinators contra o mesmo `(canal, ispb)` com perda
   forcada do lock, provando que o deposto derruba os workers. Cobre a lacuna #10.
9. **Rever `queue_target`/`queue_interval`** a luz da lacuna #2: enquanto o lock
   morar no pool, fail-fast agressivo e um gerador de split-brain.

---

## 8. O que esta provado e o que nao esta

**Provado com medicao:**
- O `conn_limited` e emitido pelo nosso semaforo, nao pelo BACEN.
- Houve 3 lideres CPM simultaneos, identificados por task, sem `lock released`.
- O inicio do `conn_limited` coincide no minuto com o 2o lider, e o volume dobra com
  o 3o.
- A correlacao se repete em todos os episodios de 48h, nos dois sentidos.
- Os locks vivem hoje em conexoes `idle` do pool compartilhado.
- O cap residual do fail-open e 36 por canal, contra teto de 6.

**Hipotese, ainda nao provada diretamente:**
- Que **cada** perda de lock especifica foi causada por **aquele** disconnect de
  Postgrex. A correlacao temporal e forte (18s no episodio das 00:22) e o mecanismo e
  o unico compativel com "lock solto sem unlock", mas nao instrumentamos a conexao
  para amarrar cada evento. A recomendacao P0.2 torna a questao irrelevante.

**Fora de escopo desta auditoria, e continua aberto:**
- O lag ICOM p95/p99 de 10s a 25s, que **antecede** o split-brain e nao se explica
  por ele.

---

## 9. Execução: o que foi corrigido e provado (2026-07-25, mesma madrugada)

Commits `88d06db0` e `27f5a09b`. Suite completa do pix: **4533 testes, 0 falhas**.
Deployado `pix-api` HML td **213** / PRD td **83**, imagem
`27f5a09b-splitbrain-20260725`, digest `sha256:3d05b403`, guard MATCH.

### P0.2 — conexão dedicada (`Shared.Postgres.AdvisoryLock`, novo)

O advisory lock passou a morar numa conexão Postgrex própria, `pool_size: 1`,
fora do pool Ecto. `held?/3` consulta `pg_locks` restrito a `pg_backend_pid()` e
compara o backend PID observado na aquisição, porque `pg_try_advisory_lock` é
**reentrante** e por isso não serve para verificar posse.

8 testes, incluindo o cenário do incidente: a sessão é derrubada por fora
(`pg_terminate_backend`) e o dono antigo **tem** de enxergar que perdeu.

### P0.1 — reconfirmação da liderança (`Cpm`/`Csm.Coordinator`)

A posse é relida a cada 5s direto do Postgres. Ao perder, os 6 workers são
derrubados **antes** de qualquer outra coisa (enquanto vivos, consomem permits
sem direito), o lock é solto e o pod volta a standby. Queda da conexão dedicada
sai da liderança na hora, sem esperar o tick. Erro ao **ler** a posse conta como
**perda** (fail-closed). 8 testes, cobrindo os dois canais.

### P0.3 — fail-open degradado (`ConnSemaphore`)

Sem Redis, passa a valer um teto **local por pod** (default 2, de modo que
`2 x desired=3 = 6`). Continua fail-open, mas com número. 4 testes.

### P1 — observabilidade

`SpiService.Icom.SlotMetrics` publica a série de permits por pool. Metric
filters criados nos dois ambientes (`Monetarie/PIX/{prod,homolog}`):
`IcomConnLimited`, `IcomLeadershipLost`, `IcomLeaderAcquired`.

**Correção de rota, feita a partir do dado vivo de HML:** a primeira versão
emitia um flag `saturado` em `in_use >= max`. O dado mostrou
`pool=cpm in_use=6 max=6` como **regime normal** — são 6 workers de long-poll
para 6 permits. Esse alarme dispararia 100% do tempo e viraria exatamente o ruído
que este relatório condena. O campo virou `no_teto` (factual) e o sinal de alarme
passou a ser a **recusa** (`[:shared, :bacen, :conn_semaphore, :denied]`): recusa
significa que alguém precisou de permit e não havia, o que com 6 workers para 6
permits só ocorre quando há worker a mais — a assinatura do split-brain.

### Pool do banco (pedido do dono: folgar antes do volume)

Medido antes de mexer: `max_connections = 2000`, **165** conexões em uso, todas
`idle` em `ClientRead`. Defaults por task de 8/6/8/6 (28) para 16/16/24/16 (72);
`spi` leva o maior por ser o hot path do ICOM sob ANS de 1600ms. Verificado vivo
em PRD depois do deploy: **279 conexões de 2000 (14%)**, que é
`88/task x 3 tasks + 2 conexões dedicadas por task`.

Pool apertado se manifesta como checkout longo, que derruba conexão, que era o
gatilho do split-brain: folgar ataca o gatilho por baixo. O teste de contrato do
dimensionamento (`prod_runtime_config_test.exs`) foi atualizado junto e segue
sendo o guarda-corpo.

**Limite conhecido:** o teto de 2000 vem do parameter group, não da memória no
piso de escala. O cluster é Serverless v2 com **min de 0.5 ACU**. Antes de outro
aumento de pool, subir o **min de ACU**, não só o pool.

### Verificação em produção depois do deploy

- Advisory locks: **1 detentor por canal** (2 no total), ambos da mesma task.
- `conn_limited`: apenas durante o rolling (03:45, 03:46, 03:50, quando task nova
  e velha coexistem, o que é transitório e esperado) e **zero desde 03:51**.
- ICOM seguiu recebendo normalmente durante todo o deploy.

### O que segue aberto

1. **Alarme com destino.** Os metric filters existem, mas os SNS topics da conta
   são de VPN e de eventos SES. Falta decidir o destino (e-mail, Slack) e criar o
   alarme — recomendado: `IcomConnLimited > 60/min por 5 min` e
   `IcomLeadershipLost >= 1`.
2. **P2 (fencing por época, teste de dois Coordinators concorrentes, revisão de
   `queue_target`/`queue_interval`)** não foi implementado.
3. **`application_name` da conexão dedicada** herda `monetarie-pix-shared`, o que
   dificulta distingui-la do pool em `pg_stat_activity`. Cosmético, mas útil.
4. **Seq scans pesados** achados de passagem e não tratados: `oban_jobs`
   (6,9 bilhões de tuplas lidas), `xml_audit_logs` (565 MB, 337 M),
   `icom_received` (147 MB, 455 M). São candidatos ao próximo checkout longo.
5. O **lag ICOM p95/p99 de 10 a 25s** segue aberto e não é deste defeito.

---

## 10. Segunda rodada (mesmo dia): P2, DLQ, pool e ACU

Commits `7470ebfc`, `b5953f7b`, `d512b39c`. Core **8895 testes / 0 falhas**, pix
**4537 / 0**. Deployado: core-api HML td217 / PRD td**106**
(`sha256:eccf026a`), pix-api HML td214 / PRD td**84** (`sha256:54fea570`), guard
MATCH nos dois.

### Aurora (item 6)

O piso nunca era o problema: o cluster **nunca descia a 0.5** (minimo real 2.0),
mas **batia o teto de 16 em metade das janelas de 5 min**. PRD passou a
**min 2 / max 32**; HML a **max 8** (piso mantido em 0.5 para nao gerar custo
ocioso).

### Lag ICOM (item 7) — retratacao

O relatorio dizia que o lag "NAO foi atribuido a isto". **Errado.** Aquilo veio
de uma amostra pontual; a serie por hora mostra o contrario:

| hora UTC | alertas de lag |
|---|---|
| 24/07 13Z-21Z | 5 a 20/h (baseline) |
| **00Z / 01Z / 02Z** | **37 / 100 / 79** (split-brain) |
| 04Z-08Z | **0** |
| 09Z | 2 |

Havia baseline, entao o split-brain nao era a unica causa — mas **multiplicava o
lag por 5 a 10x**, e o fix derrubou de 100/h para ~0,3/h.

### DLQ (item 8) — nao era lixo, era defeito

As 20 mensagens eram notificacoes `STATUS_UPDATED` (`processing -> settled`) de
**PIX de saida ja liquidados, R$ 631.732,71**. Cadeia do defeito: o evento nao
carrega `amount`; `parse_amount_value(nil)` devolve **0**;
`AccountResolver.resolve/1` respondia `{:ok, amount_cents: 0}`, afirmando ter
resolvido um valor inexistente; esse 0 chegava a `Wallet.withdraw/4`, cujos
guards exigem `> 0`; nenhuma clausula casava e o consumer levantava.

**O dinheiro estava consistente**: as operacoes correspondentes estao todas
`accepted` no Core (conferidas por valor e janela, ja que os E2E divergem por
outro defeito conhecido) e nao existe **nenhum** PIX-out fora de accepted. O
evento era redundante.

Fix (`b5953f7b`): `resolve/1` recusa com `{:error, :missing_amount}`, e os tres
callers traduzem isso em **SKIP**, nunca crash. O contrato de skip ja existia no
estorno ("espelho do skip da materializacao") e faltava na materializacao — foi
um teste existente que quebrou e apontou a resposta certa.

**Purga, so depois do fix deployado.** PRD: 20 removidas, 0 restantes, escopo
limitado ao subject e ate a seq 62. HML: 21 removidas e **16 PRESERVADAS de
proposito** — sao defeitos DIFERENTES, ainda nao investigados, e a mensagem na
DLQ e a unica evidencia deles:

| quantidade | subject | erro |
|---|---|---|
| 7 | `poison.settlement-scheduler` | `not_null_violation` |
| 3 | `poison.settlement-scheduler` | `undefined_column` |
| 4 | `spb.failed` | (erro vazio) |
| 2 | `spb.failed` | `string_data_right_truncation` |

### Seq scans (item 5)

Com `stats_reset` nulo (estatisticas cumulativas) e bloat de 0 a 5,7% em todas as
tabelas — ou seja, nao e falta de vacuum:

- **`oban_jobs`**, o maior consumidor do banco: 326.595 scans lendo 22.539 tuplas
  cada, **7,36 bilhoes** no total. Nao e falta de indice (tem 6): a tabela
  guardava **29.048 jobs `completed`** e toda varredura pagava por esse lixo.
  Pruner de 7 para **2 dias**, com piso no `pix_in_orphan_lookback_hours` (24h).
- **`camt060_requests`**: quase full scan por consulta. Origem localizada em
  `list_recent/1` — `order_by sent_at desc, limit 50` **sem WHERE**, e
  `(status, sent_at)` nao serve para ordenacao pura. Indice em `sent_at`, criado
  `CONCURRENTLY`.
- **`icom_sessions` NAO foi tocada de proposito**: 2,2 milhoes de scans, mas 40
  tuplas cada numa tabela de 44 linhas. Ali o planner acerta e indice pioraria.

### P2 (item 4): o que foi feito e o que eu recomendo NAO fazer

**Feito — teste de dois Coordinators concorrentes (lacuna #10)**, `d512b39c`,
com advisory lock REAL: (a) exatamente um vira lider e so ele tem workers;
(b) o standby nao assume enquanto o lider mantem a sessao; (c) derrubando a
sessao do lider por fora, ele derruba os workers e sai, e o standby assume;
(d) apos shutdown limpo a chave fica livre.

**Recomendo NAO mexer no `queue_target`/`queue_interval` (lacuna #8).** Aquela
lacuna existia porque o descarte de conexao levava o advisory lock junto. Com a
conexao dedicada (P0.2) **esse acoplamento deixou de existir**, entao mexer agora
seria enfraquecer uma protecao documentada (pos-incidente 2026-07-09) sem ganho.

**Recomendo adiar o fencing por epoca (lacuna #9).** Com o step-down em ate 5s, a
conexao dedicada e o teste concorrente, ele vira defesa em profundidade com custo
real no hot path do money-path. Ha achados novos de maior retorno na fila.

### Verificacao em producao

Sequencia de lideranca no rolling, por task: **todo `leader acquired` precedido
de `lock released`** (14:10:28 -> 14:10:30 -> 14:11:20 -> 14:11:21 -> 14:11:38 ->
14:11:40), que e o oposto do padrao do incidente. `conn_limited` restrito aos
minutos do rolling (54 e 61) e **zero depois**.

### Achado novo, nao tratado

`[PixInOrphanRecon]` reporta **orfaos de PIX-in** em PRD: camt.054 CRDT sem
credito confirmado, com `reason=no_outbox_event` e `reason=message_not_credited`,
em datas de 10, 13, 17, 22 e 24/07 (valores de R$ 1,00 a R$ 4.200,00). E o
monitor fazendo o trabalho dele, e antecede esta sessao — mas e money-path e
merece frente propria.
