# PRD — Separação: throughput da fila (hot path do "próximo item")

## Problema / dor

Origem: avaliação (2026-06-05) do artigo de engenharia da Shopify *["Scaling Inventory Reservations"](https://shopify.engineering/scaling-inventory-reservations)* contra o nosso mecanismo de fila da separação (`MissaoLogistica`). A pergunta era "dá pra usar isso pra melhorar a fila da separação?". A resposta, depois de ler o código real:

- **A trava de concorrência na atribuição de item já está no estado da arte.** `sp_reservar_item_da_lista.sql` usa `FOR UPDATE SKIP LOCKED` (linha 55) + `pg_advisory_xact_lock(usuario_id)` (linha 23) + idempotência (linhas 27-37). É *mais* defensivo que a lógica de atribuição do artigo. **Não há ganho em mexer aqui.**
- **A tuning de `READ COMMITTED` / gap-lock do artigo é específica de MySQL/InnoDB** — não se aplica ao nosso Postgres. Não copiar.
- **A lição #1 do artigo ("o gargalo está em código que você não está olhando — meça, não adivinhe") bate em cheio.** A trava não é o problema; o **fan-out por pull** é.
- **A ideia mais profunda do artigo (pré-computar um pool reivindicável pra que o hot path seja um grab trivial) é a única que vale roubar.** A Shopify tirou trabalho do hot path. O nosso hot path **re-deriva a ordem de coleta inteira a cada pull.**

> **Nota de domínio (travada com o dono em 2026-06-05): múltiplos operadores na mesma missão é NORMAL e intencional.** Missão criada com operadores nasce `DESIGNADA` (time de N operadores + `lider_id`), gerenciada pelo satélite `MissoesOperadores` (adicionar/remover/transferir operador, `sincronizarEquipe`); criada sem operadores nasce `FILA_ABERTA` e é "pega solo" — o 1º que dá `aceitarMissao` reivindica e ela sai do pool (`MissoesFila::buscarDisponiveis` filtra `NOT EXISTS vínculo ativo`). O `SKIP LOCKED` da SP existe justamente pra **vários operadores compartilharem a mesma missão sem pegar o mesmo item**. Uma versão anterior deste PRD tratava "dois operadores na mesma missão" como bug e propunha índice único no vínculo — **descartado**: seria nocivo, proibiria o caso normal. Este PRD **não toca** em `aceitarMissao`/`buscarDisponiveis` nem na contagem de operadores por missão.

Achados concretos de **throughput** no código de hoje (a dor real):

1. **`obterProximoItem` segura uma conexão por ~8-12 queries por pick.** O caminho (`Satelites/MissoesExecucao.php:524-555`) faz: `buscarItemEmAndamento` (1) → `buscarCandidatos` (que busca **todos** os pendentes/pulados, default `limit=2000`, `Inteligencias/MissaoLogisticaItem.php:92`) → ordena tudo em PHP via Strategy → chama a SP. No sucesso ainda: lookup de "item anterior" (`MissoesExecucao.php:610-628`), `registrarExtrato` (insert), `atualizarInicioMissao` (→ `MissoesStatus`, mais queries) e `calcularProgresso`. Sob carga não estamos *lock-bound*, estamos segurando conexão ~10 queries por pick — o cenário "low CPU + high queueing" do artigo. Com vários operadores na mesma missão (caso normal), esse custo se multiplica.

2. **`calcularProgresso` dispara 3 `COUNT(*)` separados por resposta** (`MissoesExecucao.php:700-710`), e é chamado em **toda** resposta de `montarRespostaItem` / `responderSemItens` / `tentarReservarItemNoBanco`. Ou seja: 3 counts × cada interação × cada separador, numa tabela quente.

3. **A ordenação é re-derivada por completo a cada pull — incluindo I/O externo.** `BaseExecucaoStrategy::ordenarCandidatosParaExecucao` (`Strategies/Execucao/BaseExecucaoStrategy.php:223`) chama `enriquecerItens` (linha 230) → na `SeparacaoRotaStrategy` isso é `enriquecerItensFila` → **`buscarRoteirizacao($romaneioId)`** (`Strategies/Execucao/SeparacaoRotaStrategy.php:199`), que faz **2 queries no subsistema de romaneios** (`RomaneiosVeiculos` + `RomaneiosPedidos`) — e depois `aplicarOrdenacao` **re-ordena todos os candidatos** por prioridade de rota (LIFO). Tudo isso roda **em cada pull**, só pra escolher o item #1. A rota é **constante durante a missão**: recalcular a cada grab é desperdício puro. E é por isso que **não dá pra "janelar" os candidatos ingenuamente** — a ordem não é `ordem`, é route-LIFO computada em runtime.

4. **`atualizarInicioMissao` chama `MissoesStatus::iniciarMissao` em todo pull** (`MissoesExecucao.php:637, 756-766`), mesmo a missão já estando iniciada — paga round-trip que só deveria importar no primeiro item.

5. **`moverItemParaFinalDaFila` faz read-modify-write de `MAX(ordem)+1` sem trava** (`MissoesExecucao.php:397-408`) — duas ocorrências simultâneas podem gerar a mesma `novaOrdem`. Baixo impacto (colisão de ordenação, não de correção), mas é padrão de hot-row.

Escala alvo informada pelo dono: **missões médias, ~50-500 itens**, com **muitos separadores simultâneos por missão**. Nesse tamanho o re-fetch de roteirização + re-enrich + re-sort por pull (achado 3) é o maior ofensor de throughput, seguido do overhead fixo dos 3 COUNTs (achado 2) multiplicado por operador.

A solução é organizada em frentes independentes, do mais barato/seguro pro arquitetural, podendo virar PRs sequenciais sem parar a separação em produção.

## Personas (alto nível)

- **Separador** puxando "próximo item" no app — sente a latência de cada pull (hoje ~10 queries + I/O de romaneio por item). É quem ganha com throughput.
- **Líder / gestor de missão** — quer a fila andando rápido para o time inteiro; com vários operadores na mesma missão, cada melhoria de pull se multiplica.
- **Dev Genesis** mantendo `MissaoLogistica` — precisa que o hot path fique legível e instrumentado, não um emaranhado de strategy + I/O por pull.
- **Tech Lead** aprovando o plano — precisa decidir o escopo da Frente C (materializar ordenação) que depende de uma auditoria de quais Strategies têm ordem estável vs. dinâmica por operador.

## O que precisa fazer (alto nível)

Organizado em 3 frentes. Recomendação de ordem: **A** (medir) → **B** (quick wins) → **C** (arquitetural) depois de ter números.

### Frente A — Instrumentar o hot path (medir antes de otimizar)

Lição #1 do artigo, versão Genesis-sized. Não otimizar no escuro.

- Instrumentar `MissoesExecucao::obterProximoItem` (e idealmente o caminho de bipagem) com **contagem de queries + wall-clock** do pull inteiro.
- Logar quando passar de um limiar (ex.: > N queries ou > X ms), pra capturar baseline em dev/prod-like antes e depois das Frentes B/C.
- TL decide o mecanismo: log estruturado (`error_log`/canal de log), payload no `missao_logistica_extrato`, ou tabela de métricas dedicada — e se é scaffolding temporário ou instrumentação permanente com amostragem.

### Frente B — Quick wins do hot path (sem mudança de comportamento)

- **Colapsar `calcularProgresso`** (`MissoesExecucao.php:696-718`): os 3 `COUNT(*)` viram **uma** query com `COUNT(*) FILTER (WHERE ...)` (total / concluídos / pendentes-ativos). Valores idênticos aos de hoje.
- **Guardar `atualizarInicioMissao`** (`MissoesExecucao.php:637, 756-766`): no-op barato quando a missão já está iniciada, evitando o round-trip ao `MissoesStatus` em todo pull.
- **Remover/adiar o lookup de "item anterior"** (`MissoesExecucao.php:610-628`): hoje é só decoração no payload do extrato; tirar do caminho síncrono do pull.
- Critério: `obterProximoItem` reduz contagem de queries por pull sem alterar nenhuma resposta visível (item retornado e `progresso` idênticos).

### Frente C — Tirar a ordenação do hot path (a grande alavanca, requer auditoria)

Equivalente correto do "pool pré-computado" da Shopify: o hot path vira um grab trivial.

- Hoje, missão por rota re-roda `buscarRoteirizacao` (2 queries de romaneio) + re-enrich + re-sort de 50-500 candidatos **por pull**, só pra pegar o item #1. A ordem de rota é **constante na missão**.
- **Pré-computar uma vez** (ao iniciar a missão e/ou quando a rota é re-otimizada) e **materializar a ordem de coleta** numa coluna persistida (reusar `ordem` **ou** nova coluna `ordem_execucao` — decisão de design, ver dúvidas).
- Com a ordem materializada, `sp_reservar_item_da_lista` colapsa pra `... ORDER BY ordem_execucao ... FOR UPDATE SKIP LOCKED LIMIT 1` — **zero** busca de candidatos, **zero** sort em PHP, **zero** re-fetch de roteirização por pull. (A trava `SKIP LOCKED` continua intacta — é ela que mantém vários operadores na mesma missão sem colisão.)
- **Fronteira / risco:** só vale pra ordens **estáveis entre pulls e não-por-operador**. Strategies que ordenam em função do usuário que pede precisam **manter o caminho dinâmico**. Logo, Frente C exige uma **auditoria por Strategy** + um flag de capacidade ("materializável" vs "dinâmica") no contrato da Strategy.

## Fora do escopo

- **Qualquer mudança no claim/contagem de operadores por missão.** Múltiplos operadores na mesma missão é normal e intencional (DESIGNADA + `MissoesOperadores`; FILA_ABERTA single-claim é proposital). **Não adicionar índice único / trava no vínculo missão-operador**, não mexer em `MissoesFila::aceitarMissao` nem em `buscarDisponiveis`. (Decisão do dono 2026-06-05 — "não mexer na fila aberta".)
- **Reserva de estoque / anti-oversell.** O *título* do artigo Shopify é reserva de inventário. O dono **excluiu explicitamente** esse eixo: a separação hoje não reserva nem decrementa estoque, e isso continua assim. Estoque vive no ERP legado. Se um dia a dor virar "duas missões consumindo o mesmo estoque físico", vira PRD próprio — **não é este**.
- **Reescrever a trava da SP** (`SKIP LOCKED` + advisory + idempotência). Está correta e mais defensiva que o artigo. Não tocar na semântica de lock.
- **Tuning estilo `READ COMMITTED` / gap-lock do artigo.** É MySQL/InnoDB; nosso Postgres com `SKIP LOCKED` já faz a coisa certa.
- **Materializar ordem das Strategies dinâmicas/por-operador.** Essas mantêm o caminho de sort em runtime; Frente C só cobre as estáveis.
- **Migrar `MissaoLogistica` / `MissaoLogisticaItem` de V1 (Inteligencias) pra Sondas V2.** São V1 hoje; este PRD adiciona coluna/índice via migration mas **não** faz a migração de modelo. Dívida do sinal, fora daqui.
- **Mexer no `moverItemParaFinalDaFila` (achado 5).** Baixo impacto; só registrar como dívida a não ser que a Frente A mostre que importa.
- **Otimizar o caminho de bipagem além da instrumentação.** Frente A pode medir, mas a otimização da bipagem é PRD separado.

## Restrições / dependências conhecidas

- **Hot path em produção.** O app de separação está em uso ativo. Cada frente vira PR testável em isolamento; Frentes B e C mexem no caminho do `obterProximoItem` (crítico) — exigem teste de não-regressão de ordem e de `progresso`.
- **Memory `feedback_no_raw_sql`**: "nunca raw SQL via `Connect::getInstance` pra writes; usar `->update()` do model". A Frente B colapsa `calcularProgresso` num **read** (`COUNT FILTER`) — a regra é sobre *writes/batch*, mas o TL precisa definir se isso passa pelo query builder do model ou por um método dedicado, sem violar a regra.
- **Memory `modelinteligencia_save_gotchas` + colunas de tenant**: a SP usa `DatabaseControl` justamente pra contornar o `customerControl` do `MissaoLogisticaItem`; já `calcularProgresso` conta via model (com `customerControl`). Qualquer query nova precisa respeitar `customerControl`/`companyControl` e `block`. A migration da Frente C segue `genesis-sondas-db` (colunas `block_*` quando aplicável, sem `DEFAULT 1`).
- **Frente C depende de auditoria de Strategies.** Lista a auditar: `SeparacaoExecucaoStrategy`, `SeparacaoRotaStrategy`, `SeparacaoEnderecamentoStrategy`, `CarregamentoExecucaoStrategy`, `CarregamentoAlfabeticoStrategy`, `PesagemExecucaoStrategy`, `InventarioExecucaoStrategy`, `RetornoEntregasExecucaoStrategy`, `AjusteDadosExecucaoStrategy`. Pra cada uma: a ordem depende do `usuario_id` que pede? Depende de estado que muda durante a missão? Se não → materializável.
- **Gatilho de (re)materialização** (Frente C): a ordem de rota muda quando a roteirização é re-otimizada (integração com `Edicaorota`/romaneios). Precisa de um hook pra invalidar/recomputar a `ordem_execucao` quando a rota mudar — senão a fila materializada fica defasada.
- **Testes de integração** rodam só em `dev_gx_goldie` com bootstrap de sessão Galaxia (memory `integration_test_session_groups`).

## Métricas de sucesso

- **Frente A**: instrumentação ativa em dev mostrando contagem de queries + duração por `obterProximoItem`; baseline capturado **antes** das Frentes B/C pra comparação.
- **Frente B**: `calcularProgresso` = **1** query (era 3); contagem de queries do `obterProximoItem` cai mensuravelmente (alvo definido após baseline da Frente A); `progresso` e item retornado **idênticos** aos de hoje (teste golden).
- **Frente C**: para missão por rota, `obterProximoItem` **não** chama mais `buscarRoteirizacao` por pull; contagem de queries por pull despenca (de ~N pra ~M, número fechado após baseline); **ordem de coleta idêntica** à atual nas Strategies materializáveis (teste golden comparando a sequência de itens entregues do começo ao fim da missão).
- **Zero regressão funcional**: fluxo do separador (pedir próximo → bipar → concluir/pular/ocorrência) operacional em dev e prod após cada PR; com vários operadores na mesma missão, distribuição de itens sem colisão (garantida pelo `SKIP LOCKED` intacto).

## Inspirações / referências

- **Artigo Shopify "Scaling Inventory Reservations"** — fonte da provocação. O que adotamos: a lição "meça o caminho inteiro, o gargalo não é a query que você está olhando" (Frente A) e "pré-compute o pool reivindicável pra que o hot path seja um grab trivial" (Frente C). O que **não** adotamos: o modelo de reserva de estoque (fora de escopo) e a tuning de gap-lock de MySQL.
- **`sp_reservar_item_da_lista.sql`** — a parte que já está certa; serve de âncora pro que a Frente C quer alcançar (SP virar um `ORDER BY ... SKIP LOCKED LIMIT 1` puro).
- **`BaseExecucaoStrategy` + `SeparacaoRotaStrategy`** — onde mora o custo por pull (achado 3); o contrato da Strategy é onde entra o flag "materializável".
- **`MissoesOperadores`** — referência do modelo multi-operador (equipe + líder), que confirma por que não se mexe no vínculo missão-operador.
- **Skills `genesis-sondas-db`** (migration + colunas de tenant), **`genesis-tests`** (teste de não-regressão/integração em `dev_gx_goldie`).

## Dúvidas em aberto pro PO

(Decisões de escopo já travadas pelo dono em 2026-06-05: reserva de estoque/anti-oversell **fora**; claim/fila aberta **não se mexe**; foco em throughput do pull.)

1. **Onde materializar a ordem (Frente C)?** Reusar a coluna `ordem` (risco: `ordem` já é mexida por `moverItemParaFinalDaFila` e pela criação da missão) **ou** criar coluna nova `ordem_execucao` dedicada (mais limpo, mas migration + popular em missões existentes)? Default recomendado: `ordem_execucao` nova.
2. **Quando (re)materializar?** Só ao iniciar a missão, ou também a cada re-otimização de rota (hook no fluxo de `Edicaorota`/romaneios)? Se a rota muda no meio e não recomputamos, a fila materializada fica errada. Precisa definir o gatilho de invalidação.
3. **Quais Strategies são materializáveis?** Depende da auditoria. O PO/TL precisa confirmar caso a caso se alguma ordena por `usuario_id` (ex.: zona/região por separador) — essas mantêm o caminho dinâmico. Sem essa lista, Frente C não fecha escopo.
4. **Instrumentação da Frente A é permanente ou temporária?** Scaffolding pra um diagnóstico pontual, ou métrica permanente com amostragem/limiar? Onde grava (log, extrato, tabela)?
5. **O colapso dos 3 COUNTs (Frente B) pode ser uma query SELECT "crua" via query builder**, ou tem que passar por método do model pra respeitar a regra `feedback_no_raw_sql`? (A regra mira writes; reads de agregação precisam de uma posição explícita do TL.)
6. **Ordem de execução das frentes**: recomendação A → B → C. Frente C só depois de A dar números e da auditoria de Strategies. Confirma?
