Central de Artigos https://rickytech.com.br/ Artigos em um só lugar. Tue, 04 Aug 2026 02:56:08 +0000 pt-BR hourly 1 https://wordpress.org/?v=7.0.2 https://rickytech.com.br/wp-content/uploads/2026/06/ricky-mark-transparent-150x150.png Central de Artigos https://rickytech.com.br/ 32 32 Azure DevOps Pipelines: a mesma pipeline abre o PR, amarra o Work Item e completa sozinha (Parte 2, final) https://rickytech.com.br/azure-devops-pipelines-a-mesma-pipeline-abre-o-pr-amarra-o-work-item-e-completa-sozinha-parte-2-final/ https://rickytech.com.br/azure-devops-pipelines-a-mesma-pipeline-abre-o-pr-amarra-o-work-item-e-completa-sozinha-parte-2-final/#respond Tue, 04 Aug 2026 02:55:46 +0000 https://rickytech.com.br/azure-devops-pipelines-a-mesma-pipeline-abre-o-pr-amarra-o-work-item-e-completa-sozinha-parte-2-final/ Segunda e última parte da série: a pipeline que criava a branch agora abre o Pull Request, vincula o Work Item, liga o auto-complete em nome de quem executou — e cria as branch policies que fazem tudo isso significar alguma coisa.

O post Azure DevOps Pipelines: a mesma pipeline abre o PR, amarra o Work Item e completa sozinha (Parte 2, final) apareceu primeiro em Central de Artigos.

]]>
Na Parte 1 a gente resolveu o nome da branch. Um clique, parâmetros tipados, feature/squad-alpha/checkout-4821 criada no padrão certo. Governança ganha, ninguém mais inventa ajuste-final-2.

E aí o problema muda de lugar.

A branch nasce padronizada e some. Duas semanas depois o board tem uma atividade “Em desenvolvimento” que ninguém sabe onde está, e o repositório tem dezessete branches órfãs. Padronizar o nome resolveu a rastreabilidade da criação — não a do ciclo de vida. Automatizar o começo e deixar o resto na mão é trocar um trabalho manual por outro.

Este é o post que fecha a série. Vamos completar o circuito: a mesma pipeline vai abrir o Pull Request, amarrar o Work Item, ligar o auto-complete e — a parte que a maioria dos tutoriais pula — criar as políticas de branch que fazem o auto-complete significar alguma coisa.

Auto-complete sem branch policy é merge automático com nome bonito. A política não é burocracia acessória: é o que transforma “completa sozinho” em “completa sozinho depois que passou“.

Um parâmetro a mais muda tudo

A Parte 1 pedia atividade como texto livre. Para fechar o circuito, esse campo precisa virar o ID do Work Item. Parece detalhe, mas é a chave: com o ID em mãos, a pipeline consegue nomear a branch, criar o PR e amarrar os dois ao item do board — tudo a partir de uma única entrada do usuário.

parameters:
  - name: workItemId
    displayName: ID do Work Item (ex.: 4821)
    type: number

  - name: projeto
    displayName: Projeto
    type: string
    default: meu-projeto

  - name: squad
    displayName: Squad responsavel
    type: string
    values:
      - squad-alpha
      - squad-bravo
      - squad-charlie

  - name: branchDestino
    displayName: Branch de destino do PR
    type: string
    default: main

  - name: autoComplete
    displayName: Completar o PR automaticamente quando as politicas passarem
    type: boolean
    default: true

trigger: none
pr: none

Repare no type: number e no type: boolean. Parâmetro tipado não é preciosismo — o Azure Pipelines valida antes de enfileirar, então “4821a” nunca chega ao curl. É validação de graça, na fronteira certa.

Passo 1 — ler o Work Item para nomear a branch

Antes de criar qualquer coisa, buscamos o título do Work Item. Ele vai virar o nome da branch e o título do PR, o que elimina a divergência clássica entre “o que a branch diz” e “o que a tarefa era”.

  - script: |
      set -euo pipefail

      WI=$(curl -sf \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/wit/workitems/${{ parameters.workItemId }}?api-version=7.1")

      TITULO=$(echo "$WI" | jq -r '.fields["System.Title"]')
      echo "Work Item: $TITULO"

      # slug: minusculas, sem acento, hifens no lugar de espaco, 40 chars
      SLUG=$(echo "$TITULO" \
        | iconv -f utf8 -t ascii//TRANSLIT \
        | tr '[:upper:]' '[:lower:]' \
        | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' \
        | cut -c1-40 | sed -E 's/-+$//')

      BRANCH="feature/${{ parameters.squad }}/${{ parameters.workItemId }}-$SLUG"

      # exporta para os proximos steps do job
      echo "##vso[task.setvariable variable=novaBranch]$BRANCH"
      echo "##vso[task.setvariable variable=tituloWI]$TITULO"
    displayName: "Ler Work Item e montar o nome da branch"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Duas coisas aqui merecem atenção.

O ##vso[task.setvariable] é o mecanismo de logging command do Azure Pipelines: um echo num formato especial que o agente intercepta e converte em variável para os passos seguintes. É a única forma de um script devolver valor para o resto do job — variável de shell morre no fim do passo.

E o set -euo pipefail não é enfeite. Sem o -e, um curl que falha não derruba o passo, o jq recebe vazio, e você acaba com uma branch chamada feature/squad-alpha/4821-null criada com sucesso. Pipeline que falha silenciosamente é pior que pipeline que quebra.

Passo 2 — criar a branch e abrir o Pull Request

A criação da branch é a mesma da Parte 1. O que muda é o que vem logo depois: um POST em /pullrequests.

  - checkout: self
    persistCredentials: true

  - script: |
      set -euo pipefail

      # --- cria e publica a branch (igual a Parte 1) ---
      git checkout -b "$(novaBranch)"
      git push origin "$(novaBranch)"

      # --- abre o Pull Request ---
      BODY=$(jq -n \
        --arg src  "refs/heads/$(novaBranch)" \
        --arg dst  "refs/heads/${{ parameters.branchDestino }}" \
        --arg tit  "AB#${{ parameters.workItemId }} $(tituloWI)" \
        --arg desc "PR aberto automaticamente pela pipeline de criacao de branch." \
        '{sourceRefName:$src, targetRefName:$dst, title:$tit, description:$desc}')

      PR=$(curl -sf -X POST \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        -H "Content-Type: application/json" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests?api-version=7.1" \
        -d "$BODY")

      PR_ID=$(echo "$PR" | jq -r '.pullRequestId')
      echo "Pull Request #$PR_ID criado"
      echo "##vso[task.setvariable variable=prId]$PR_ID"
    displayName: "Criar branch e abrir o Pull Request"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Três detalhes que economizam uma tarde de depuração:

  • Use Build.Repository.ID, não Build.Repository.Name. A API aceita os dois, mas o nome quebra quando alguém renomeia o repositório — e o ID, segundo a própria documentação de variáveis predefinidas, não muda nunca.
  • sourceRefName e targetRefName exigem o caminho completo (refs/heads/main, não main). Passar o nome curto devolve um 400 com mensagem pouco útil.
  • Monte o JSON com jq -n, não com echo e aspas. Título de Work Item vem com aspas, acento, cedilha e barra. Concatenar string à mão funciona até o primeiro item chamado Corrigir “N/A” no relatório — e aí você tem um JSON inválido e um erro que não aponta para a causa.

Por que o prefixo AB# no título

O Azure DevOps reconhece a sintaxe AB#1234 em títulos, descrições e mensagens de commit e a transforma em link para o Work Item. É um detalhe de dois caracteres que faz o PR ficar navegável na direção contrária — de quem está no código para quem está no board.

Passo 3 — amarrar o Work Item de verdade

A API de criação de PR aceita um campo workItemRefs, e é tentador resolver tudo ali. Na prática, a ligação que aparece no board — na aba Development do Work Item, que é onde o time olha — é uma relação do tipo ArtifactLink no lado do Work Item. Fazer o vínculo explícito é uma chamada a mais e elimina a categoria inteira de “o PR existe mas o board não sabe”.

  - script: |
      set -euo pipefail

      # formato do artefato: vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{prId}
      ARTIFACT="vstfs:///Git/PullRequestId/$(System.TeamProjectId)%2F$(Build.Repository.ID)%2F$(prId)"

      PATCH=$(jq -n --arg url "$ARTIFACT" \
        '[{op:"add", path:"/relations/-", value:{
            rel:"ArtifactLink",
            url:$url,
            attributes:{name:"Pull Request"}
          }}]')

      curl -sf -X PATCH \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        -H "Content-Type: application/json-patch+json" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/wit/workitems/${{ parameters.workItemId }}?api-version=7.1" \
        -d "$PATCH"

      echo "Work Item ${{ parameters.workItemId }} vinculado ao PR $(prId)"
    displayName: "Vincular Work Item ao Pull Request"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Dois pontos costumam derrubar esse passo na primeira execução. O Content-Type é application/json-patch+json, não application/json — a API de Work Items fala JSON Patch (RFC 6902), e o corpo é um array de operações. E as barras dentro do identificador do artefato precisam vir codificadas como %2F: o valor inteiro é um único token, não um caminho de URL.

Passo 4 — auto-complete em nome de quem rodou

Aqui está a decisão de design mais interessante desta pipeline.

Ligar o auto-complete é um PATCH no PR informando autoCompleteSetBy — e esse campo pede uma identidade. O reflexo é usar a identidade de build. Mas a build identity é um robô: se ela é quem “pediu” a conclusão automática, o histórico do PR fica sem dono e as políticas que dependem de voto de pessoa ficam sem sentido.

A resposta elegante é Build.RequestedForId — o GUID de quem clicou em Run pipeline. O PR passa a se auto-completar em nome da pessoa que iniciou a atividade, que é exatamente a semântica certa:

  - ${{ if eq(parameters.autoComplete, true) }}:
    - script: |
        set -euo pipefail

        BODY=$(jq -n --arg quem "$(Build.RequestedForId)" \
          '{autoCompleteSetBy:{id:$quem},
            completionOptions:{
              mergeStrategy:"squash",
              deleteSourceBranch:true,
              transitionWorkItems:true,
              bypassPolicy:false
            }}')

        curl -sf -X PATCH \
          -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
          -H "Content-Type: application/json" \
          "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests/$(prId)?api-version=7.1" \
          -d "$BODY"

        echo "Auto-complete ligado por $(Build.RequestedFor)"
      displayName: "Ligar auto-complete no PR"
      env:
        SYSTEM_ACCESSTOKEN: $(System.AccessToken)

As completionOptions merecem leitura atenta, porque cada uma é uma decisão de processo disfarçada de campo JSON:

  • mergeStrategy: "squash". Um commit por PR no histórico da main. Se o seu time prefere preservar os commits individuais, use noFastForward.
  • deleteSourceBranch: true. É o que resolve as dezessete branches órfãs do começo do texto. Sem isso, você automatizou a criação e não a limpeza.
  • transitionWorkItems: true. Move o Work Item vinculado para o estado seguinte quando o PR completa. É o fechamento do circuito: a atividade sai de “Em desenvolvimento” sozinha.
  • bypassPolicy: false. Deixe explícito, mesmo sendo o padrão. Ver false escrito no YAML é o que impede alguém de trocar para true “só para destravar” e nunca mais voltar atrás.

Passo 5 — as políticas, que é onde isso vira engenharia

Auto-complete só faz sentido se existir algo a esperar. Sem política nenhuma, “completar quando tudo passar” completa imediatamente — e você construiu uma máquina de dar merge direto na main.

Políticas de branch também são API. Dá para versionar a configuração do repositório junto com o código, o que acaba com o “alguém desligou a validação no console e ninguém viu”.

POST $(System.CollectionUri)$(System.TeamProject)/_apis/policy/configurations?api-version=7.1
Content-Type: application/json

{
  "isEnabled": true,
  "isBlocking": true,
  "type": { "id": "0609b952-1397-4640-95ec-e00a01b2c241" },
  "settings": {
    "buildDefinitionId": 42,
    "displayName": "Build de validacao",
    "queueOnSourceUpdateOnly": true,
    "manualQueueOnly": false,
    "validDuration": 720,
    "scope": [
      { "repositoryId": "<repo-id>", "refName": "refs/heads/main", "matchKind": "exact" }
    ]
  }
}

O type.id é o que define qual política você está criando. Os identificadores são fixos e globais:

  • Build de validação0609b952-1397-4640-95ec-e00a01b2c241
  • Número mínimo de aprovaçõesfa4e907d-c16b-4a4c-9dfa-4906e5d171dd
  • Vínculo com Work Item40e92b44-2fe1-4dd6-b3d8-74a9c21d0c6e
  • Revisores obrigatóriosfd2167ab-b0be-447a-8ec8-39368250530e
  • Estratégia de mergefa4e907d-c16b-4a4c-9dfa-4916e5d171ab

Não decore essa lista — e principalmente não a copie de um post antigo. A fonte confiável é a própria instalação:

curl -sf -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
  "$(System.CollectionUri)$(System.TeamProject)/_apis/policy/types?api-version=7.1" \
  | jq -r '.value[] | "\(.id)  \(.displayName)"'

Uma combinação que funciona bem na prática: exigir uma aprovação com creatorVoteCounts: false (quem abriu não aprova o próprio PR), build de validação bloqueante e vínculo com Work Item obrigatório. Essa terceira política tem um efeito colateral simpático: como a nossa pipeline já vincula o item no passo 3, o PR nasce em conformidade — a política vira uma rede de proteção contra o PR aberto na mão, não um obstáculo ao fluxo automatizado.

As permissões — onde isso quebra na primeira execução

Praticamente todo mundo tropeça aqui, e o erro raramente diz a verdade. Três coisas precisam estar certas para a identidade de build fazer esse trajeto inteiro:

  1. Contribute no repositório — para o git push da branch. Sem isso o push falha ou a REST API devolve createBranchPermissionRequired.
  2. Contribute to pull requests — permissão separada da anterior. Criar branch e criar PR são direitos distintos, e é comum liberar só o primeiro e passar meia hora depurando um 403 no POST /pullrequests.
  3. Escopo de autorização do job — se a organização tem Limit job authorization scope to current project ativado (e deveria ter), o System.AccessToken só alcança recursos do projeto atual. Repositório em outro projeto exige token próprio.

A identidade a procurar nas permissões do repositório é <Nome do Projeto> Build Service (<organização>). Para escrever no Work Item, ela também precisa de Edit work items in this node na área correspondente.

Armadilha de rodapé: a branch nova pode disparar a sua CI, que dispara a build de validação, que roda de novo a cada push. Se a pipeline de criação de branch estiver no mesmo arquivo da CI sem trigger: none, você monta um laço que consome agente até alguém perceber a conta.

O circuito fechado

Vale olhar o que a série construiu, de ponta a ponta. Uma pessoa abre o Azure DevOps, clica em Run pipeline, informa o ID do Work Item e a squad. A partir daí:

  1. A pipeline lê o Work Item e monta o nome padronizado da branch.
  2. Cria e publica a branch.
  3. Abre o Pull Request com título derivado da atividade.
  4. Vincula o PR ao Work Item — visível dos dois lados.
  5. Liga o auto-complete em nome de quem executou.
  6. Quando a build passa e a aprovação sai, o merge acontece, a branch é apagada e o Work Item avança de estado.

Um clique no início, um clique de aprovação no fim. Entre os dois, nenhuma etapa depende de alguém lembrar de fazer.

A lição que fica não é sobre Azure Pipelines. É sobre onde a automação começa a valer: automatizar uma etapa de um processo de seis normalmente só empurra o trabalho manual para a etapa seguinte. O ganho aparece quando o circuito fecha — quando não sobra nenhum ponto em que o fluxo depende de disciplina humana para continuar. Padronizar o nome da branch foi bom. Fazer o board se atualizar sozinho é o que muda a rotina do time.

Perguntas frequentes

Preciso mesmo criar as políticas pela API?

Não. Configurar pelo console funciona igual. A vantagem da API é que a configuração vira código versionado, revisado em PR e replicável em outro repositório — em vez de um estado invisível que alguém pode alterar sem deixar rastro.

O auto-complete pode dar merge sem ninguém revisar?

Pode, se não houver política bloqueante. Com isBlocking: true em pelo menos uma política de aprovação, o PR fica esperando. É por isso que o passo 5 não é opcional: sem política, “auto-complete” é literalmente “merge agora”.

E se o Work Item não existir?

Com set -euo pipefail e curl -sf, o passo falha com 404 antes de criar qualquer coisa — que é o comportamento desejado. O -f do curl é o que transforma resposta de erro HTTP em código de saída diferente de zero; sem ele o corpo do erro segue adiante como se fosse dado válido.

Dá para adaptar isso para GitHub Actions?

Dá, e a estrutura é a mesma: ler a issue, criar a branch, abrir o PR (gh pr create), ligar o auto-merge (gh pr merge --auto) e proteger a branch com rulesets. Muda a API, não o raciocínio — e o ponto sobre “auto-merge sem proteção é merge direto” vale igual.

Uso Jira em vez de Azure Boards. Perdi a série?

Só os passos 1 e 3. A branch continua sendo nomeada a partir da chave da issue, o PR continua sendo aberto pela mesma API do Azure Repos, e o vínculo passa a ser feito pelo smart commit do Jira ou pela integração dele com o Azure DevOps. O circuito fecha do mesmo jeito.

Fontes

O post Azure DevOps Pipelines: a mesma pipeline abre o PR, amarra o Work Item e completa sozinha (Parte 2, final) apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/azure-devops-pipelines-a-mesma-pipeline-abre-o-pr-amarra-o-work-item-e-completa-sozinha-parte-2-final/feed/ 0
Como testar Vertical Slice Architecture sem herdar a pirâmide de testes https://rickytech.com.br/como-testar-vertical-slice-architecture-sem-herdar-a-piramide-de-testes/ https://rickytech.com.br/como-testar-vertical-slice-architecture-sem-herdar-a-piramide-de-testes/#respond Tue, 04 Aug 2026 02:45:42 +0000 https://rickytech.com.br/como-testar-vertical-slice-architecture-sem-herdar-a-piramide-de-testes/ Você migrou para vertical slice mas manteve a estratégia de testes da arquitetura em camadas. O resultado são trinta testes verdes que não pegariam a migration esquecida. A slice é a unidade de teste.

O post Como testar Vertical Slice Architecture sem herdar a pirâmide de testes apareceu primeiro em Central de Artigos.

]]>
Você migrou para Vertical Slice Architecture. Cada funcionalidade virou uma pasta autocontida: o endpoint, o validador, o handler, a consulta ao banco, tudo junto. O código ficou muito mais fácil de navegar — abrir Features/Pedidos/CriarPedido e ver a feature inteira em um arquivo é libertador depois de anos caçando implementação em cinco camadas.

Aí chega a hora de testar, e o time faz o de sempre: mocka o repositório, instancia o handler, verifica se o método foi chamado. Trinta testes verdes. E o primeiro bug em produção é uma migration esquecida que nenhum deles pegaria.

O problema não é a arquitetura. É que a estratégia de testes ficou na arquitetura anterior.

Por que a pirâmide de testes atrita com vertical slice

A pirâmide clássica — muita unidade, alguma integração, pouquíssimo end-to-end — nasceu para arquiteturas em camadas. E fazia todo sentido lá: em um sistema com controller, service, repository e domínio, cada camada é uma unidade com fronteira clara. Testar a camada isolada, com as vizinhas mockadas, é barato e diz alguma coisa.

Vertical slice desmonta essa premissa. A slice é fina de propósito: ela existe justamente para não ter camadas intermediárias. Um handler típico valida a entrada, roda uma regra, grava e devolve. Se você mocka tudo em volta, o que sobra para testar é o mapeamento de DTO.

Pior: os mocks te acorrentam à implementação. Você trocou MediatR por chamada direta? Trinta testes quebram. Trocou EF Core por Dapper naquela consulta lenta? Quebram de novo. Eles não estão testando comportamento, estão testando a fiação interna — exatamente o que a slice deveria deixar você mudar livremente.

Se o teste quebra quando você refatora sem mudar comportamento, ele não é uma rede de segurança. É um imposto.

A inversão: a slice é a unidade de teste

A mudança de mentalidade cabe em uma frase: uma requisição entra por cima, um resultado observável sai por baixo.

Você não testa “o handler”. Você testa a slice inteira, de ponta a ponta, pela sua fronteira pública. E “resultado observável” significa três coisas concretas, nessa ordem de importância:

  • A resposta HTTP. Status code, corpo, cabeçalhos. É o contrato com quem consome.
  • O estado do banco. A linha foi criada, com os valores certos? O saldo foi debitado?
  • As mensagens emitidas. O evento foi para a fila, o outbox recebeu o registro?

Repare no que ficou de fora: como chegou lá. O teste não sabe se existe MediatR no meio, se a persistência é EF ou Dapper, se o validador é FluentValidation ou um if. Isso é a propriedade que você quer — refatoração interna sem tocar em teste.

Como isso fica na prática

Em .NET, a combinação é WebApplicationFactory para subir a aplicação de verdade e Testcontainers para subir um banco de verdade em Docker:

public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _db = new PostgreSqlBuilder()
        .WithImage("postgres:18-alpine")
        .Build();

    public async Task InitializeAsync()
    {
        await _db.StartAsync();
        // aplica as migrations no banco recém-criado
        await MigrarAsync(_db.GetConnectionString());
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(o =>
                o.UseNpgsql(_db.GetConnectionString()));

            // só o que é externo de verdade vira fake
            services.RemoveAll<IGatewayPagamento>();
            services.AddSingleton<IGatewayPagamento, GatewayPagamentoFake>();
        });
    }

    public new async Task DisposeAsync() => await _db.DisposeAsync();
}

E o teste em si fica curto o suficiente para caber na cabeça:

[Fact]
public async Task Criar_pedido_persiste_e_devolve_201()
{
    var client = _fixture.CreateClient();

    var resposta = await client.PostAsJsonAsync("/pedidos",
        new { ClienteId = _clienteId, Valor = 250.00m });

    Assert.Equal(HttpStatusCode.Created, resposta.StatusCode);

    // valida pelo identificador do caso, não por contagem de linhas
    var pedido = await _db.Pedidos.SingleAsync(p => p.ClienteId == _clienteId);
    Assert.Equal(250.00m, pedido.Valor);
    Assert.Equal(StatusPedido.Aberto, pedido.Status);
}

Um detalhe pequeno com consequência grande: a asserção busca SingleAsync(p => p.ClienteId == _clienteId) em vez de Count() == 1. Verificar contagem global assume que o banco está vazio, o que amarra o teste à ordem de execução e proíbe paralelismo. Buscar pelo identificador do próprio caso torna o teste indiferente a qualquer outro rodando junto.

O que continua sendo teste de unidade

Isso não é um manifesto contra teste unitário. É sobre apontá-lo para onde ele rende.

Teste unitário é caro em manutenção e barato em execução; teste de integração é o inverso. Então vale unidade onde há muitos caminhos por pouca infraestrutura: cálculo de imposto com sete faixas, máquina de estados de um pedido, política de reajuste com regras de arredondamento, parser de um formato legado.

Essa lógica não deveria estar dentro do handler de qualquer forma. Extraia para o domínio — uma classe ou função pura, sem dependência de banco ou HTTP — e cubra com um [Theory] de vinte casos. Roda em milissegundos, documenta a regra melhor que qualquer comentário, e não quebra quando você troca o ORM.

O oposto também vale: handler fino não merece teste unitário. Se ele só valida, chama uma coisa e devolve, o teste de integração já cobriu tudo que havia para cobrir. Um unitário ali só duplica cobertura e adiciona um mock para manter.

O teste que protege a arquitetura

Existe uma categoria de erro que nenhum teste funcional pega: a erosão da própria arquitetura. É quase sempre a mesma história — alguém precisa de uma função que já existe na slice vizinha, importa direto de Features/Pedidos para dentro de Features/Faturamento, e o PR passa porque tudo continua verde.

Seis meses depois, a promessa da vertical slice acabou: nenhuma feature pode mais ser alterada ou deletada sem efeito colateral, e você tem um monolito em camadas com nomes de pasta bonitos.

A defesa é um teste de arquitetura — com ArchUnitNET, NetArchTest ou uma regra própria de análise — que afirma o invariante em código:

[Fact]
public void Slices_nao_referenciam_outras_slices()
{
    var slices = Types.InAssembly(typeof(Program).Assembly)
        .That().ResideInNamespaceMatching(@"App\.Features\.(\w+)");

    var resultado = slices
        .ShouldNot().HaveDependencyOnOtherSlices()  // regra própria
        .GetResult();

    Assert.True(resultado.IsSuccessful, string.Join("\n", resultado.FailingTypeNames));
}

Compartilhamento continua permitido — só que explicitamente, por um projeto Shared ou pelo domínio. A diferença é que agora mover código para lá é uma decisão consciente, revisada em PR, e não um using que ninguém reparou.

O preço: velocidade, e como pagar menos

A objeção previsível é que teste de integração é lento. É verdade, e ignorar isso é como o time acaba desligando a suíte. Três medidas resolvem a maior parte:

  • Um contêiner por suíte, não por teste. Subir Postgres custa alguns segundos; fazer isso trezentas vezes custa a tarde. Compartilhe a fixture com ICollectionFixture e pague o preço uma vez.
  • Limpe o estado, não recrie o banco. Ferramentas como Respawn truncam as tabelas entre testes em milissegundos, contra dezenas de segundos de derrubar e reaplicar migrations.
  • Isole por dado, não por transação. Se cada teste usa seus próprios identificadores — cliente novo, pedido novo — eles não colidem, e você pode rodar em paralelo. É a otimização de maior retorno da lista.

Com isso, uma suíte de duzentos testes de slice roda em um ou dois minutos. É mais que os oito segundos da suíte só de mocks. Mas é a única das duas que teria pego a migration esquecida.

O resumo em quatro linhas

  1. A slice é a unidade. Teste pela fronteira: requisição entra, resultado observável sai.
  2. Integração é a base, não o topo. Banco real em contêiner; falsifique só o que é externo e cobra caro — gateway de pagamento, provedor de e-mail, API de terceiro.
  3. Unidade para lógica de domínio densa. Extraída da slice, pura, com muitos casos.
  4. Um teste de arquitetura impedindo que slices se referenciem — é o único que protege a decisão que te trouxe até aqui.

A pergunta que resolve qualquer caso duvidoso é sempre a mesma: este teste quebraria se eu mudasse a implementação sem mudar o comportamento? Se a resposta for sim, ele está testando a coisa errada.

Perguntas frequentes

Isso não é só chamar tudo de “teste de integração”?

A diferença está no escopo e no que se afirma. Um teste de integração tradicional exercita a colaboração entre dois componentes; aqui o alvo é a fatia vertical completa, verificada pelo contrato externo. O nome importa menos que a fronteira escolhida.

E se eu não puder usar Docker no CI?

Tente antes de descartar — quase todo runner moderno suporta. Se realmente não der, um banco compartilhado no CI com schema por execução funciona. O que não funciona é banco em memória fingindo ser Postgres: ele não reproduz constraint, tipo, colação nem comportamento transacional, e o teste passa a mentir exatamente sobre o que você queria verificar.

Qual cobertura eu devo buscar?

Cobertura por linha é a métrica errada aqui. Vale mais garantir que cada slice tem pelo menos um teste de caminho feliz e um de erro relevante — validação recusada, recurso inexistente, conflito de concorrência. Isso cobre o que quebra em produção melhor que perseguir um número.

Vale testar por HTTP ou chamar o handler direto?

Por HTTP. É mais lento em milissegundos e cobre roteamento, model binding, filtros, middleware de autenticação e serialização — a faixa onde mora uma parcela desproporcional dos bugs reais.

Fontes

O post Como testar Vertical Slice Architecture sem herdar a pirâmide de testes apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/como-testar-vertical-slice-architecture-sem-herdar-a-piramide-de-testes/feed/ 0
Cloud Run em várias regiões: o comando único, o load balancer global e a parte que ninguém resolve por você https://rickytech.com.br/cloud-run-em-varias-regioes-o-comando-unico-o-load-balancer-global-e-a-parte-que-ninguem-resolve-por-voce/ https://rickytech.com.br/cloud-run-em-varias-regioes-o-comando-unico-o-load-balancer-global-e-a-parte-que-ninguem-resolve-por-voce/#respond Tue, 04 Aug 2026 02:45:32 +0000 https://rickytech.com.br/cloud-run-em-varias-regioes-o-comando-unico-o-load-balancer-global-e-a-parte-que-ninguem-resolve-por-voce/ Serverless nunca significou sem região. Como o gcloud run multi-region-services encurta o deploy, por que ele sozinho não te dá um endereço único, e onde a dificuldade de verdade continua: o estado.

O post Cloud Run em várias regiões: o comando único, o load balancer global e a parte que ninguém resolve por você apareceu primeiro em Central de Artigos.

]]>
Toda arquitetura serverless carrega uma promessa implícita: “não se preocupe com infraestrutura”. Ela vale até o dia em que uma região inteira do provedor cai, seu serviço vai junto, e você descobre que “serverless” nunca significou “sem região”.

O Cloud Run é regional. Um serviço vive em southamerica-east1 ou em us-central1 — não nos dois. Se a região sai do ar, seu serviço sai do ar, e nenhum autoscaling do mundo resolve. A boa notícia é que o caminho para múltiplas regiões ficou consideravelmente mais curto. A má é que a parte difícil continua exatamente onde sempre esteve.

Por que multi-região, além do medo de apagão

Duas razões, e vale separá-las porque exigem soluções diferentes.

A primeira é disponibilidade. Uma região do Google Cloud é um conjunto de zonas dentro de uma mesma área geográfica. O Cloud Run já distribui suas instâncias entre zonas automaticamente — isso protege contra a queda de um datacenter, não contra um incidente que afete a região inteira (uma falha de rede regional, um problema no plano de controle, um evento físico).

A segunda é latência. Um usuário em Lisboa falando com um serviço em São Paulo paga uns 180 ms só de ida e volta na rede, antes de qualquer processamento. Nenhuma otimização de código recupera isso. A única saída é aproximar o serviço do usuário.

São problemas distintos com a mesma resposta arquitetural, mas com critérios de sucesso diferentes: disponibilidade se mede por failover funcionando; latência se mede por roteamento por proximidade. Guarde isso, porque a configuração precisa entregar as duas coisas.

O comando único: multi-region services

Historicamente, colocar o Cloud Run em três regiões significava rodar três gcloud run deploy, cada um com sua região, e manter os três em sincronia na unha — o que na prática virava um script Terraform e um pipeline com fan-out.

O gcloud run multi-region-services encurta isso para uma chamada:

gcloud run multi-region-services deploy api-pedidos \
  --image=us-docker.pkg.dev/meu-projeto/api/pedidos:1.4.2 \
  --regions=southamerica-east1,us-central1,europe-west1

Um único recurso lógico, várias regiões, deploy atômico do ponto de vista de quem opera. O grupo de comandos tem os verbos que você espera — deploy, describe, list, update, replace e delete — e o update aceita mudar a lista de regiões, o que torna adicionar ou remover um ponto de presença uma operação de um comando.

Em YAML, o equivalente é a anotação run.googleapis.com/regions. Em Terraform, o bloco multi_region_settings com a lista de regiões.

Uma armadilha que custa tempo: os serviços regionais criados por baixo do multi-region service viram recursos somente leitura. Não tente editar um deles direto pelo console para “só testar uma coisa” — a alteração ou é rejeitada ou é sobrescrita no próximo deploy.

O que o comando único não faz

Aqui está o mal-entendido mais comum, e ele derruba gente experiente: o multi-region service não te dá um endereço único. Você terminou o deploy com três serviços rodando e três URLs diferentes. Ninguém vai ser roteado para lugar nenhum sozinho.

Quem faz o roteamento é o Application Load Balancer externo global. Ele é uma peça separada, cobrada à parte, e é ele que entrega as duas coisas que você queria: um IP anycast único, anunciado de todas as bordas da rede do Google, e o roteamento automático do usuário para o datacenter mais próximo dele.

A montagem tem quatro etapas. A primeira é criar um NEG serverless (Network Endpoint Group) por região — é o adaptador que faz um serviço serverless, que não tem IP fixo nem porta, parecer um backend normal para o load balancer:

# um NEG por região
gcloud compute network-endpoint-groups create neg-pedidos-sa \
  --region=southamerica-east1 \
  --network-endpoint-type=serverless \
  --cloud-run-service=api-pedidos

gcloud compute network-endpoint-groups create neg-pedidos-us \
  --region=us-central1 \
  --network-endpoint-type=serverless \
  --cloud-run-service=api-pedidos

A segunda é criar o backend service global, que é o objeto que agrupa os NEGs e concentra a política de balanceamento:

gcloud compute backend-services create bs-pedidos --global \
  --load-balancing-scheme=EXTERNAL_MANAGED

A terceira é registrar cada NEG nesse backend. Repare que o comando pede a região do NEG explicitamente — é assim que o load balancer aprende a topologia:

gcloud compute backend-services add-backend bs-pedidos --global \
  --network-endpoint-group=neg-pedidos-sa \
  --network-endpoint-group-region=southamerica-east1

gcloud compute backend-services add-backend bs-pedidos --global \
  --network-endpoint-group=neg-pedidos-us \
  --network-endpoint-group-region=us-central1

A quarta é o encanamento de frente: certificado gerenciado, URL map, target HTTPS proxy e a forwarding rule que finalmente reserva o IP anycast. É burocrático, é sempre igual, e é exatamente o tipo de coisa que deve morar no Terraform e nunca mais ser digitada à mão.

Failover: automático de verdade ou “automático”?

Com os NEGs registrados, o load balancer distribui por proximidade. A pergunta que importa é o que acontece quando uma região começa a responder erro em vez de parar de responder.

Existem dois modos. O manual é tirar o NEG do backend service com remove-backend — funciona, e é a ferramenta certa para uma manutenção planejada, mas depende de um humano perceber o incidente e agir. O automático se apoia no Cloud Run service health: o load balancer passa a considerar a saúde reportada pelo serviço e para de mandar tráfego para a região doente sozinho.

O detalhe que decide se isso funciona é o seu health check. Um endpoint /health que devolve 200 OK incondicionalmente é decorativo: a região continua “saudável” enquanto o banco está inacessível e cada requisição real falha. O health check precisa exercitar as dependências críticas — conexão com o banco, credencial válida, fila alcançável — sem virar uma consulta cara que derruba o serviço sob carga. É um teste de fumaça, não um diagnóstico completo.

A parte difícil que ninguém resolve por você: o estado

Se você chegou até aqui, tem três cópias do seu contêiner em três continentes e um IP único na frente. Se a sua aplicação é stateless — uma API que só transforma dados, uma renderização de front, um gateway — acabou. Está pronto.

Se ela grava alguma coisa, o trabalho de verdade começa agora, e o Cloud Run não tem nada a dizer sobre ele.

Três instâncias em três regiões falando com um único banco em southamerica-east1 resolvem disponibilidade da camada de aplicação e não resolvem mais nada. A instância de europe-west1 vai pagar a travessia do Atlântico em cada consulta — trocando 180 ms na rede do usuário por 180 ms na rede do banco. E se a região do banco cair, as três instâncias caem juntas: você montou um sistema com três pontos de presença e um único ponto de falha.

As saídas existem, e nenhuma é gratuita:

  • Banco com réplicas regionais de leitura. Cloud SQL com réplicas, ou Spanner com configuração multi-região. Resolve leitura, mantém a escrita centralizada. É o caminho mais comum e o mais barato de raciocinar.
  • Particionamento por região. Cada região é dona de um subconjunto dos dados, e o roteamento leva o usuário sempre para a sua região de origem. Escrita local e rápida — ao custo de resolver “e quando o usuário viaja?” no seu código.
  • Multi-master com resolução de conflito. Escrita em qualquer lugar, convergência eventual. Poderoso e caro em complexidade: você passa a lidar com conflitos de escrita explicitamente, no domínio.

Escolher entre elas é uma decisão de produto disfarçada de decisão de infraestrutura. “Pode haver alguns segundos de atraso entre o que o usuário grava no Brasil e o que ele lê na Europa?” não é uma pergunta para o time de plataforma responder sozinho.

Uma ordem de adoção que evita retrabalho

  1. Comece pelo que é stateless. Front-end, BFF, APIs de leitura. O ganho é imediato e o risco é quase nulo.
  2. Suba o load balancer global antes de precisar dele. Com uma região só. Assim, quando a segunda entrar, é um add-backend — e não uma migração de DNS no meio de um incidente.
  3. Torne o health check honesto. Antes de configurar qualquer failover automático. Um health check mentiroso transforma failover automático em roleta.
  4. Só então ataque o dado. É a etapa cara, e é a única que exige decisão de negócio.

A promessa do serverless nunca foi “sem infraestrutura” — foi “sem servidores”. Continente ainda é uma coisa física, a velocidade da luz continua sendo o limite, e distribuir estado continua sendo o problema difícil de sistemas distribuídos. O que mudou é que a parte mecânica virou um comando. Aproveite o atalho, mas não confunda o atalho com o percurso.

Perguntas frequentes

Multi-region service substitui o load balancer global?

Não. Ele cria e sincroniza os serviços nas várias regiões; o endereço único, o roteamento por proximidade e o failover vêm do Application Load Balancer externo global. São duas peças complementares.

Dá para usar o domínio direto do Cloud Run em vez do load balancer?

Dá, mas o domínio do Cloud Run aponta para uma região específica. Você perde exatamente as duas coisas que motivaram o multi-região.

Quanto custa a mais?

Três frentes: o load balancer global (regra de encaminhamento mais tráfego processado), o egresso entre regiões se sua aplicação atravessa fronteira para falar com o banco, e o piso de instâncias mínimas multiplicado pelo número de regiões. A terceira é a que mais surpreende — se você mantém min-instances maior que zero, agora paga por região.

Preciso de três regiões?

Quase nunca. Duas já eliminam o ponto único de falha regional e cobrem a maior parte dos casos de latência. Cada região adicional multiplica custo, superfície operacional e complexidade do dado — só entre quando houver um usuário concreto do outro lado.

Fontes

O post Cloud Run em várias regiões: o comando único, o load balancer global e a parte que ninguém resolve por você apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/cloud-run-em-varias-regioes-o-comando-unico-o-load-balancer-global-e-a-parte-que-ninguem-resolve-por-voce/feed/ 0
PostgreSQL 18 quase um ano depois: o que o I/O assíncrono realmente entregou https://rickytech.com.br/postgresql-18-quase-um-ano-depois-o-que-o-i-o-assincrono-realmente-entregou/ https://rickytech.com.br/postgresql-18-quase-um-ano-depois-o-que-o-i-o-assincrono-realmente-entregou/#respond Tue, 04 Aug 2026 02:45:13 +0000 https://rickytech.com.br/postgresql-18-quase-um-ano-depois-o-que-o-i-o-assincrono-realmente-entregou/ O PG18 chegou com o I/O assíncrono como manchete. Um ano de produção depois, dá para trocar a manchete por números: onde o ganho é real, onde é zero, e o que mais vale adotar no mesmo dia.

O post PostgreSQL 18 quase um ano depois: o que o I/O assíncrono realmente entregou apareceu primeiro em Central de Artigos.

]]>
O PostgreSQL 18 chegou em setembro de 2025 com a manchete mais barulhenta dos últimos anos: I/O assíncrono. Quase um ano de produção depois, dá para trocar a manchete por números. E os números contam uma história mais interessante do que o release note.

Resumo honesto: em alguns workloads o AIO cortou horas de janela de manutenção. Em outros, o ganho foi zero. Saber em qual grupo você está — antes de mexer em postgresql.conf — é o que separa uma migração tranquila de uma tarde perdida.

O problema que o I/O assíncrono resolve

Até o PostgreSQL 17, cada processo backend lia do disco de um jeito muito simples e muito antigo: pedia um bloco ao sistema operacional e parava até o bloco chegar. Uma leitura, uma espera. Outra leitura, outra espera.

Imagine um garçom que só anota o pedido da mesa 2 depois que a cozinha entrega o prato da mesa 1. Em um restaurante vazio, ninguém percebe. Em um sequential scan de 200 GB, o processo passa a maior parte da vida bloqueado esperando o disco — e o núcleo de CPU que poderia estar processando fica ocioso.

O Postgres sempre teve um paliativo para isso: o posix_fadvise, que basicamente cutuca o kernel dizendo “vou precisar desses blocos, vá buscando”. Funciona, mas é um aviso, não um pedido. Você não controla quando volta, não sabe se voltou, e não dá para medir.

O PG18 substituiu o paliativo por infraestrutura de verdade: o backend submete várias requisições de leitura, continua trabalhando, e coleta os resultados quando ficam prontos. O garçom passou a anotar as cinco mesas antes de ir à cozinha.

Os três parâmetros que importam

A configuração central é o io_method, que define como as leituras são despachadas. São três valores:

  • sync. O comportamento antigo. Existe como escape se algo der errado.
  • worker. Processos de background dedicados executam as leituras em nome dos backends. É o padrão do PG18 e funciona em qualquer sistema — macOS, FreeBSD, contêiner com seccomp restrito, servidor gerenciado.
  • io_uring. A interface nativa de I/O assíncrono do Linux. Menos cópia, menos troca de contexto, menos CPU por operação. Exige kernel recente e permissão explícita — em muitos ambientes gerenciados e sandboxes o io_uring está bloqueado por política de segurança.

A regra prática: se você não controla o kernel da máquina onde o Postgres roda, worker é o seu backend de AIO — queira você ou não.

Os outros dois parâmetros controlam o tamanho do lote. O io_combine_limit define quantos blocos adjacentes o Postgres funde em uma única requisição ao disco, e o io_max_combine_limit é o teto rígido desse valor. Fundir leituras é o que transforma 128 idas ao disco em uma só — e é onde mora boa parte do ganho real.

No PG18 o pool de workers é estático: io_workers vale 3 por padrão, com teto de 32, e só muda com restart. Essa rigidez foi o incômodo mais citado no primeiro ano — em carga de pico faltam workers, e fora do pico eles ficam parados. O PostgreSQL 19 troca o dial único por um pool que se auto-ajusta, com io_min_workers, io_max_workers, io_worker_idle_timeout e io_worker_launch_interval. Se você está planejando a adoção agora, vale saber que essa parte ainda vai mudar.

Onde o ganho aparece de verdade

Aqui está a parte que o marketing omite. O AIO acelera leitura de dados frios em volume. Traduzindo para o que você tem em produção:

  • VACUUM em tabelas grandes. É o caso de sucesso mais consistente — relatos de janelas caindo de 8 para 5 horas na mesma tabela e no mesmo hardware.
  • Sequential scans sobre dados que não estão em cache. Relatórios, ETL, agregação em tabela histórica. Ganho substancial.
  • Bitmap heap scans. Melhora modesta, mas mensurável.
  • OLTP indexado. Praticamente nada. Se sua carga é “busca por chave primária, devolve uma linha, tudo cabe em shared_buffers“, o AIO não tem trabalho para fazer.

Faz sentido quando você olha pelo mecanismo em vez do benchmark: o AIO só ajuda enquanto houver espera de disco para esconder. Se o dado já está na memória, não há espera — e otimizar espera zero continua dando zero.

Como medir em vez de adivinhar

O PG18 trouxe a visão pg_aios, que expõe as operações de I/O assíncrono em voo, e ampliou a pg_stat_io com as colunas read_bytes, write_bytes e extend_bytes, além de linhas para o I/O de WAL. Antes de ligar qualquer coisa, tire uma foto:

-- volume real de leitura por tipo de backend e contexto
SELECT backend_type, context, object,
       reads, read_bytes, read_time
FROM pg_stat_io
WHERE reads > 0
ORDER BY read_bytes DESC;

Se read_time for irrelevante perto do tempo total das suas consultas, você acabou de descobrir que o AIO não é o seu gargalo — e economizou uma semana.

O resto do PG18 que vale adotar no mesmo dia

O I/O assíncrono roubou a cena, mas algumas mudanças menores mudam código do dia a dia.

uuidv7() nativo. UUID versão 7 embute o timestamp nos bits mais significativos, então valores gerados em sequência ficam ordenados. Isso importa porque o índice B-tree de uma chave UUIDv4 sofre com inserção aleatória: cada novo registro cai em uma página diferente, fragmentando o índice e derrubando a taxa de acerto do cache. Com UUIDv7, as inserções voltam a ser quase sequenciais — você mantém a chave globalmente única sem pagar o preço no índice. Existe também uuidv4() como alias explícito.

Skip scan em índices B-tree multicoluna. Antes, um índice em (tenant_id, criado_em) era praticamente inútil para uma consulta que filtrava só por criado_em — o planner exigia a primeira coluna. Agora ele consegue “pular” pelos valores distintos da coluna inicial e usar o índice mesmo assim. Na prática, índices que você criaria em duplicata deixam de ser necessários.

RETURNING old.* e new.*. Em UPDATE, DELETE e MERGE você agora devolve o estado anterior e o novo na mesma instrução. Auditoria e outbox deixam de precisar de um SELECT extra dentro da transação:

UPDATE pedidos
   SET status = 'enviado'
 WHERE id = $1
RETURNING old.status AS status_anterior,
          new.status AS status_atual;

Colunas geradas virtuais. O padrão do GENERATED ALWAYS AS passou a ser calcular na leitura, não na escrita. Menos disco ocupado, escrita mais barata. Se você dependia do comportamento antigo, o STORED continua lá.

pg_upgrade --swap e estatísticas preservadas. O --swap troca os diretórios em vez de copiar ou linkar arquivos — é o modo mais rápido de subir de versão. E o --preserve-optimizer-statistics, agora padrão, resolve a dor clássica do upgrade: o cluster subir sem estatísticas e o planner tomar decisões terríveis até o primeiro ANALYZE completo.

O que quebra na atualização

Três mudanças incompatíveis merecem atenção antes do pg_upgrade:

  • Checksums de dados agora vêm ligados por padrão no initdb. É o comportamento correto, mas custa um pouco de CPU. Para desligar, --no-data-checksums.
  • VACUUM e ANALYZE passam a processar as partições filhas de uma tabela particionada. Se o seu script noturno assumia o comportamento antigo, use ONLY — ou aceite uma janela bem maior do que a esperada.
  • Autenticação MD5 está formalmente depreciada, com aviso em CREATE ROLE e ALTER ROLE. Migre para SCRAM. O aviso silencia com md5_password_warnings=off, mas silenciar não é migrar.

O plano de adoção em quatro passos

Se eu fosse subir um cluster para o 18 amanhã, faria nesta ordem:

  1. Medir antes. Colete pg_stat_io por uma semana em produção. Sem essa linha de base, você não tem como afirmar que o AIO ajudou.
  2. Subir com io_method = worker. É o padrão, funciona em qualquer lugar e já entrega a maior parte do ganho.
  3. Atacar o VACUUM primeiro. É onde o retorno é mais previsível e mais fácil de defender internamente.
  4. Testar io_uring só se você controla o kernel, mediu que o gargalo é CPU de I/O, e tem ambiente de homologação para comparar. Caso contrário, o ganho marginal não paga o risco operacional.

A lição maior do primeiro ano do PG18 não é sobre I/O assíncrono. É sobre resistir à tentação de ligar o recurso da manchete antes de saber onde o seu tempo está indo. O AIO é uma peça de engenharia excelente — e completamente irrelevante para metade dos bancos que vão atualizar.

Perguntas frequentes

Preciso mudar alguma coisa para usar o I/O assíncrono?

Não. O io_method já vem em worker no PG18, então o AIO está ativo desde o primeiro boot. O ajuste fino é opcional.

io_uring é sempre melhor que worker?

Em consumo de CPU por operação, sim. Em disponibilidade, não: exige kernel Linux recente e permissão que muitos ambientes gerenciados não concedem. A escolha na prática costuma ser feita pelo ambiente, não por você.

Vale esperar o PostgreSQL 19 por causa do pool dinâmico de workers?

Não. O PG18 já entrega o ganho principal, e o pool dinâmico é refinamento operacional. Adiar um upgrade inteiro por causa de quatro GUCs é otimizar a coisa errada.

UUIDv7 resolve o problema de fragmentação de índice de vez?

Resolve a parte causada pela aleatoriedade da chave. Ele não esconde a origem do valor: como o timestamp fica visível nos bits iniciais, um UUIDv7 exposto publicamente revela quando o registro foi criado. Se isso for sensível no seu domínio, mantenha o v4 na fronteira externa.

Fontes

O post PostgreSQL 18 quase um ano depois: o que o I/O assíncrono realmente entregou apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/postgresql-18-quase-um-ano-depois-o-que-o-i-o-assincrono-realmente-entregou/feed/ 0
Fim do deadlock: as novas APIs de Process do .NET 11 que a comunidade pedia havia anos https://rickytech.com.br/fim-do-deadlock-as-novas-apis-de-process-do-net-11-que-a-comunidade-pedia-havia-anos/ https://rickytech.com.br/fim-do-deadlock-as-novas-apis-de-process-do-net-11-que-a-comunidade-pedia-havia-anos/#respond Mon, 27 Jul 2026 20:15:25 +0000 https://rickytech.com.br/?p=218 Capturar a saida de um processo externo em C# sempre foi um convite a travamentos silenciosos. O .NET 11 preview traz metodos de alto nivel como Process.RunAndCaptureText que resolvem o problema em uma linha, sem risco de deadlock.

O post Fim do deadlock: as novas APIs de Process do .NET 11 que a comunidade pedia havia anos apareceu primeiro em Central de Artigos.

]]>
Se você já rodou um processo externo em C# e o programa simplesmente congelou sem nenhum erro, você conheceu de perto um dos bugs mais antigos e traiçoeiros do .NET: o deadlock ao redirecionar a saída de um Process. O .NET 11 (preview) finalmente entrega APIs de alto nível para resolver isso de vez, um pedido que a comunidade repetia havia mais de uma década em threads intermináveis do StackOverflow.

O problema que atormentava a comunidade

Executar um processo filho e capturar o que ele imprime parece trivial. Na prática, o padrão “ingênuo” era este:

var process = new Process();
process.StartInfo.FileName = "dotnet";
process.StartInfo.Arguments = "--help";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();

process.WaitForExit();
string saida = process.StandardOutput.ReadToEnd();

O código acima funciona nos testes e trava em produção. O motivo é sutil: os pipes que ligam o processo pai ao filho têm buffer limitado (por volta de 4 KB no Windows e 64 KB no Unix). Se o processo filho gera muita saída, ele enche o buffer e fica bloqueado esperando alguém ler. Só que o pai está parado em WaitForExit(), esperando o filho terminar. Nenhum dos dois avança. Deadlock.

Os dois processos ficam se olhando: o filho não termina porque ninguém esvazia o buffer, e o pai não lê porque está esperando o filho terminar.

Havia ainda uma segunda armadilha. Ler StandardOutput até o fim antes de ler StandardError significa que, se o filho encher o buffer de erro, ele trava do mesmo jeito. A solução recomendada pela própria documentação era usar leitura assíncrona com BeginOutputReadLine, OutputDataReceived e um WaitForExit cuidadosamente ordenado, uma cerimônia verbosa que quase ninguém acertava de primeira.

Como o .NET 11 resolve

Segundo o blog oficial da Microsoft, a classe System.Diagnostics.Process recebeu no .NET 11 Preview 4 o maior conjunto de melhorias em anos. A ideia central é oferecer métodos de alto nível que fazem a coisa certa por padrão: iniciar o processo, drenar stdout e stderr ao mesmo tempo (usando multiplexação interna) e aguardar a saída, tudo numa única chamada segura contra deadlock.

Uma linha em vez de vinte

O caso mais comum, capturar a saída de texto de um comando, vira literalmente uma chamada:

using System.Diagnostics;

// Inicia, captura stdout + stderr e espera terminar, sem risco de deadlock
ProcessTextOutput resultado = Process.RunAndCaptureText("dotnet", ["--help"]);

Console.WriteLine($"Exit code: {resultado.ExitCode}");
Console.WriteLine(resultado.StandardOutput);

if (!string.IsNullOrEmpty(resultado.StandardError))
    Console.WriteLine($"Erros: {resultado.StandardError}");

Repare que os argumentos agora são passados como uma coleção (["--help"]), o que elimina outra fonte clássica de bugs: a montagem manual da string de argumentos com aspas e escapes. E existe a versão assíncrona correspondente, ideal para não bloquear a thread:

ProcessTextOutput resultado =
    await Process.RunAndCaptureTextAsync("git", ["status", "--porcelain"]);

Uma família de métodos para cada necessidade

O recurso não é um método isolado, e sim um conjunto coeso. As novas APIs (todas com variante ...Async) cobrem os cenários mais frequentes:

  • Process.RunAndCaptureText – inicia, captura saída/erro e espera, retornando texto.
  • Process.ReadAllText – drena stdout e stderr simultaneamente.
  • Process.ReadAllBytes – retorna bytes em vez de strings, útil para saída binária.
  • Process.ReadAllLines – entrega uma sequência de objetos ProcessOutputLine, distinguindo o que veio de cada canal.
  • Process.Run – inicia e espera sem capturar a saída.
  • Process.StartAndForget – dispara o processo, devolve o PID e libera os recursos imediatamente.

Além disso, o ProcessStartInfo ganhou controles finos que antes exigiam interop manual: KillOnParentExit (encerra o filho se o pai morrer, ótimo para evitar processos órfãos), InheritedHandles (controla quais handles o filho herda) e novos StandardInputHandle/StandardOutputHandle/StandardErrorHandle para redirecionar direto a um SafeFileHandle.

Percorrendo a saída linha a linha

Para logs em tempo real, ReadAllLines permite iterar sem se preocupar com a ordem de leitura dos canais:

await foreach (ProcessOutputLine linha in
    Process.ReadAllLinesAsync("npm", ["run", "build"]))
{
    // Cada linha sabe se veio do stdout ou do stderr
    var prefixo = linha.IsError ? "[ERRO] " : "[INFO] ";
    Console.WriteLine(prefixo + linha.Text);
}

Por que isso importa

Ferramentas de linha de comando, wrappers de git, chamadas a ffmpeg, build scripts, CLIs de infraestrutura: uma fatia enorme do software backend em .NET orquestra outros processos. Até agora, fazer isso de forma correta e resiliente exigia conhecer uma armadilha histórica e escrever código defensivo repetitivo. O .NET 11 transforma o caminho seguro no caminho mais fácil, que é exatamente como boas APIs deveriam funcionar.

Vale lembrar: o .NET 11 ainda está em preview, com lançamento final previsto para novembro de 2026. As assinaturas podem sofrer ajustes até a versão estável, então trate estas APIs como algo para experimentar, não para levar a produção crítica agora.

Se você mantém qualquer serviço que invoca processos externos, vale baixar o SDK de preview e testar. É uma daquelas melhorias que não aparecem nas manchetes, mas que apagam uma classe inteira de bugs difíceis de reproduzir, e a comunidade esperou por isso por muito tempo.

Fontes

O post Fim do deadlock: as novas APIs de Process do .NET 11 que a comunidade pedia havia anos apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/fim-do-deadlock-as-novas-apis-de-process-do-net-11-que-a-comunidade-pedia-havia-anos/feed/ 0
MongoDB para RAG: quais índices criar no Atlas Vector Search https://rickytech.com.br/mongodb-para-rag-quais-indices-criar-no-atlas-vector-search/ https://rickytech.com.br/mongodb-para-rag-quais-indices-criar-no-atlas-vector-search/#respond Mon, 27 Jul 2026 20:15:24 +0000 https://rickytech.com.br/?p=220 Um guia prático de backend para montar a camada de recuperação de um sistema RAG com MongoDB Atlas: como armazenar embeddings e, principalmente, quais índices criar — o índice vectorSearch, os campos de filtro para pré-filtragem de metadados e o índice de texto do Atlas Search para busca híbrida — com exemplos reais da definição JSON e do estágio $vectorSearch.

O post MongoDB para RAG: quais índices criar no Atlas Vector Search apareceu primeiro em Central de Artigos.

]]>
Quando falamos em RAG (Retrieval-Augmented Generation), a parte mais glamurosa costuma ser o LLM que gera a resposta. Mas o segredo de um RAG que funciona está na etapa de recuperação: encontrar, entre milhares de documentos, os trechos realmente relevantes para a pergunta do usuário. É aqui que o MongoDB entra com o Atlas Vector Search, permitindo guardar embeddings ao lado dos seus dados e consultá-los por similaridade semântica. Neste artigo o foco é bem concreto: quais índices você precisa criar e como escrever a consulta.

O fluxo de RAG com MongoDB Atlas

Antes dos índices, vale fixar o fluxo. Ele tem duas fases distintas:

  • Ingestão (offline): você quebra seus documentos em pedaços (chunks), gera um embedding para cada chunk usando um modelo (OpenAI, Voyage AI, Cohere etc.) e grava esse vetor dentro do documento no MongoDB, junto com o texto e metadados.
  • Consulta (online): a pergunta do usuário também vira um embedding; o Atlas busca os vetores mais próximos (vizinhos mais próximos), e os trechos recuperados são injetados no prompt do LLM como contexto.

A busca vetorial não substitui o LLM — ela alimenta o LLM com o contexto certo. Índice bom significa contexto bom; contexto bom significa resposta confiável.

Como armazenar embeddings

No MongoDB, um embedding é simplesmente um array de números (double) em um campo do documento. Um documento típico da coleção fica assim:

{
  "_id": ObjectId("..."),
  "texto": "O MongoDB Atlas Vector Search permite...",
  "embedding": [0.0123, -0.0456, 0.0789, ...],  // ex.: 1536 dimensões
  "categoria": "backend",
  "idioma": "pt-BR",
  "publicado_em": ISODate("2026-01-10")
}

Duas regras de ouro: o número de dimensões do array precisa bater exatamente com o modelo usado (por exemplo, 1536 para text-embedding-3-small da OpenAI), e todo documento indexado precisa ter esse campo preenchido.

Os índices que você precisa criar

Aqui está o coração do artigo. Para um RAG completo você pode combinar até três tipos de índice, mas apenas o primeiro é obrigatório.

1. Índice do tipo vectorSearch (obrigatório)

É o índice que habilita a busca por similaridade. Ele é definido em JSON e tem dois tipos de campo: vector (o campo do embedding) e filter (campos de metadados usados na pré-filtragem). Os parâmetros essenciais do campo vector são:

  • path: o caminho do campo que guarda o embedding.
  • numDimensions: quantas dimensões o vetor tem — precisa ser igual ao modelo de embedding.
  • similarity: a métrica de distância. Os valores válidos são euclidean, cosine e dotProduct. Para embeddings de texto normalizados, cosine é a escolha mais comum; dotProduct tende a ser mais eficiente quando os vetores já estão normalizados.

A definição do índice fica assim:

{
  "fields": [
    {
      "type": "vector",
      "path": "embedding",
      "numDimensions": 1536,
      "similarity": "cosine"
    },
    {
      "type": "filter",
      "path": "categoria"
    },
    {
      "type": "filter",
      "path": "idioma"
    }
  ]
}

Para coleções grandes, você ainda pode ativar quantização (por exemplo "quantization": { "type": "scalar" } ou "int8") para reduzir o uso de memória do índice sem perder muita precisão.

2. Índices de filtro (para pré-filtragem de metadados)

Note, no exemplo acima, que categoria e idioma aparecem como campos filter dentro do próprio índice vectorSearch. Isso é fundamental: no Atlas Vector Search, a pré-filtragem de metadados não usa índices B-tree tradicionais — os campos precisam ser declarados como filter na definição do índice vetorial para poderem ser usados no operador filter da consulta.

A pré-filtragem acontece antes da busca vetorial, restringindo o espaço de candidatos. Isso é ótimo em RAG multi-tenant ou multi-idioma: você garante que o usuário só recupere trechos da sua organização, do idioma certo ou dentro de uma janela de data, melhorando a precisão e o desempenho.

3. Índice de busca textual (Atlas Search) para busca híbrida

A busca puramente vetorial é excelente para semântica, mas às vezes o usuário procura um termo exato — um código de produto, um nome próprio, uma sigla. Nesses casos vale combinar com um índice do Atlas Search (busca full-text, baseada em Lucene) sobre o campo de texto. Isso é a chamada busca híbrida.

São dois índices separados e independentes: um índice vectorSearch sobre embedding e um índice search (full-text) sobre texto. Na consulta, você executa os dois estágios ($vectorSearch e $search) e combina os resultados com fusão de rankings — o Atlas oferece o estágio $rankFusion (Reciprocal Rank Fusion), que mescla as listas ordenadas dando peso a cada abordagem.

Exemplo de consulta com $vectorSearch

Com os índices no lugar, a recuperação é feita com um aggregation pipeline. O estágio $vectorSearch precisa ser o primeiro do pipeline. Seus campos principais:

  • index: o nome do índice vectorSearch criado.
  • path e queryVector: o campo do embedding e o vetor da pergunta.
  • numCandidates: quantos candidatos considerar na busca aproximada (ANN); a doc recomenda pelo menos 20x o limit.
  • limit: quantos documentos retornar.
  • filter: a pré-filtragem de metadados (usa os campos filter do índice).
db.artigos.aggregate([
  {
    "$vectorSearch": {
      "index": "idx_embedding",
      "path": "embedding",
      "queryVector": [0.0021, -0.0339, 0.0517, ...],
      "numCandidates": 150,
      "limit": 5,
      "filter": {
        "$and": [
          { "categoria": "backend" },
          { "idioma": "pt-BR" }
        ]
      }
    }
  },
  {
    "$project": {
      "_id": 0,
      "texto": 1,
      "categoria": 1,
      "score": { "$meta": "vectorSearchScore" }
    }
  }
])

O estágio $project traz o vectorSearchScore via $meta — a pontuação de similaridade (quanto mais perto de 1, mais similar). É esse score que você pode usar para descartar trechos fracos antes de montar o prompt.

Regra prática: comece com numCandidates em torno de 10x a 20x o limit. Se a recall estiver baixa, aumente; se a latência incomodar, reduza. Para bases pequenas, o campo exact: true faz busca exata (ENN) sem numCandidates.

Juntando tudo

Um RAG de produção com MongoDB costuma ter: um índice vectorSearch com o campo do embedding e os metadados relevantes declarados como filter; opcionalmente um índice search full-text para busca híbrida; e um pipeline que pré-filtra por tenant/idioma, recupera por similaridade e projeta o score. Os trechos retornados viram o contexto do LLM. Simples de descrever, poderoso quando bem indexado.

Fontes

O post MongoDB para RAG: quais índices criar no Atlas Vector Search apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/mongodb-para-rag-quais-indices-criar-no-atlas-vector-search/feed/ 0
Azure DevOps Pipelines: crie branches com parâmetros em uma pipeline manual (Parte 1) https://rickytech.com.br/azure-devops-pipelines-crie-branches-com-parametros-em-uma-pipeline-manual-parte-1/ https://rickytech.com.br/azure-devops-pipelines-crie-branches-com-parametros-em-uma-pipeline-manual-parte-1/#respond Mon, 27 Jul 2026 20:15:23 +0000 https://rickytech.com.br/?p=222 Primeiro post da série sobre Azure DevOps Pipelines: uma pipeline YAML executada manualmente que recebe projeto, atividade e squad como parâmetros e cria uma branch padronizada no repositório, via git CLI ou REST API.

O post Azure DevOps Pipelines: crie branches com parâmetros em uma pipeline manual (Parte 1) apareceu primeiro em Central de Artigos.

]]>
Toda equipe que trabalha com Git no Azure DevOps repete, dia após dia, o mesmo ritual: criar uma branch com o nome no padrão certo antes de começar uma atividade. Quando isso vira tarefa manual, aparecem os “feature/ajuste-final-2” da vida e a rastreabilidade some. A boa notícia é que dá para transformar essa criação de branch em uma pipeline com um clique, padronizando o nome a partir de parâmetros como projeto, atividade e squad.

Este é o primeiro post (Parte 1) de uma série de dois sobre Azure DevOps Pipelines. Aqui vamos montar uma pipeline em YAML, executada manualmente (sem gatilho automático), que recebe parâmetros de entrada e cria uma branch no repositório.

Objetivo da Parte 1: sair do “cada um cria a branch como quer” para uma pipeline que gera o nome padronizado automaticamente, com governança e histórico de quem rodou o quê.

O que são parâmetros de runtime

No Azure Pipelines, os runtime parameters permitem que quem executa a pipeline informe valores antes da execução. Diferente das variables, os parâmetros são tipados (string, number, boolean, object, entre outros), podem ter valores restritos a uma lista e são resolvidos no momento em que a pipeline é enfileirada — antes de rodar. É exatamente o que precisamos para pedir projeto, atividade e squad.

  • name: identificador usado no YAML via ${{ parameters.nome }}.
  • displayName: rótulo amigável exibido na tela de execução.
  • type: tipo do parâmetro (aqui usamos string).
  • default: valor padrão (opcional, mas recomendado).
  • values: lista fechada de opções — útil para restringir a squad a valores válidos.

Rodando sem trigger automático

Como não queremos que a pipeline dispare a cada push ou pull request, definimos trigger: none (e pr: none). Assim ela só roda quando alguém clica em Run pipeline. A própria documentação da Microsoft usa esse padrão nos exemplos de parâmetros: com trigger: none, você consegue escolher os valores manualmente na hora de executar.

A pipeline: criando a branch via git CLI

A forma mais direta de criar a branch é usar o próprio Git no agente. O passo checkout: self com persistCredentials: true mantém o token de autenticação disponível para o push. Repare como os parâmetros são interpolados com ${{ parameters.x }} para montar o nome da branch.

: # azure-pipelines.yml
: # Pipeline MANUAL que cria uma branch a partir de parametros de entrada

parameters:
  - name: projeto
    displayName: Projeto
    type: string
    default: meu-projeto
  - name: atividade
    displayName: Numero ou nome da atividade
    type: string
  - name: squad
    displayName: Squad responsavel
    type: string
    values:            # lista fechada: so aceita estas squads
      - squad-alpha
      - squad-bravo
      - squad-charlie

# Sem gatilho automatico: so roda no "Run pipeline"
trigger: none
pr: none

pool:
  vmImage: ubuntu-latest

steps:
  - checkout: self
    persistCredentials: true   # mantem o token git p/ conseguir dar push

  - script: |
      set -e
      # Monta o nome padronizado com os parametros
      NOVA_BRANCH="feature/${{ parameters.squad }}/${{ parameters.projeto }}-${{ parameters.atividade }}"
      echo "Criando a branch: $NOVA_BRANCH"

      # Cria a branch localmente e envia para o repositorio remoto
      git checkout -b "$NOVA_BRANCH"
      git push origin "$NOVA_BRANCH"
    displayName: "Criar branch via git CLI"

Alternativa: criar a branch pela REST API

Se você prefere não depender do checkout completo, dá para criar a referência (branch) direto pela REST API do Azure DevOps, autenticando com a variável predefinida System.AccessToken. Segundo a documentação, o token deve ser mapeado explicitamente como variável de ambiente usando a chave env. Para criar uma nova ref, o corpo envia oldObjectId como uma sequência de 40 zeros e o newObjectId com o commit de origem.

  - script: |
      set -e
      NOVA_BRANCH="feature/${{ parameters.squad }}/${{ parameters.projeto }}-${{ parameters.atividade }}"
      BASE_SHA=$(git rev-parse HEAD)   # commit atual como ponto de partida

      # POST /_apis/git/repositories/{repo}/refs?api-version=7.1
      curl -sf -X POST \
        -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" \
        -H "Content-Type: application/json" \
        "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.Name)/refs?api-version=7.1" \
        -d "[{\"name\":\"refs/heads/$NOVA_BRANCH\",\"oldObjectId\":\"0000000000000000000000000000000000000000\",\"newObjectId\":\"$BASE_SHA\"}]"
    displayName: "Criar branch via REST API"
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)   # obrigatorio mapear o token

As variáveis usadas são todas predefinidas pelo Azure Pipelines: System.CollectionUri (URI da organização), System.TeamProject (nome do projeto) e Build.Repository.Name (nome do repositório). Não invente nomes — confira sempre na referência de variáveis predefinidas.

Importante: para o push ou a REST API funcionarem, a identidade de build do projeto precisa ter permissão de Contribute / criar branch no repositório. Sem isso, o retorno traz status como createBranchPermissionRequired.

Como executar manualmente

Depois de commitar o arquivo YAML e criar a pipeline apontando para ele, o fluxo é:

  1. Vá em Pipelines e selecione a pipeline recém-criada.
  2. Clique em Run pipeline.
  3. O painel de execução mostra os campos Projeto, Numero ou nome da atividade e Squad responsavel — exatamente os displayName definidos. A squad aparece como uma lista suspensa por causa de values.
  4. Preencha os valores e confirme em Run.

Se você não informar um valor, o default é usado (quando existir). Vale lembrar que os parâmetros só aparecem nessa tela ao rodar a pipeline pela interface — pelo editor YAML eles não são exibidos.

Continua na Parte 2

A base está pronta — e é só metade do caminho. Padronizar o nome da branch resolve a rastreabilidade da criação, não a do ciclo de vida: a branch nasce certinha e some, o board fica desatualizado e o repositório acumula branches órfãs.

Na Parte 2, que fecha a série, a mesma pipeline passa a abrir o Pull Request automaticamente, vincular o Work Item da atividade pela relação ArtifactLink, ligar o auto-complete em nome de quem executou — e criar, também por API, as branch policies (build de validação e aprovação obrigatória) que fazem o merge automático significar alguma coisa.

Na Parte 1 saímos do zero: uma pipeline manual, com parâmetros tipados e trigger: none, capaz de criar branches padronizadas por git CLI ou pela REST API. Um clique, nome no padrão certo, rastreabilidade garantida.

Fontes

O post Azure DevOps Pipelines: crie branches com parâmetros em uma pipeline manual (Parte 1) apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/azure-devops-pipelines-crie-branches-com-parametros-em-uma-pipeline-manual-parte-1/feed/ 0
Tunelamento em tempo real no React: SSE e SignalR na prática https://rickytech.com.br/tunelamento-em-tempo-real-no-react-sse-e-signalr-na-pratica/ https://rickytech.com.br/tunelamento-em-tempo-real-no-react-sse-e-signalr-na-pratica/#respond Mon, 27 Jul 2026 20:15:22 +0000 https://rickytech.com.br/?p=224 Precisa empurrar dados do servidor para a tela sem ficar batendo em polling? Entenda a diferença entre Server-Sent Events e SignalR e veja como consumir cada um no React, com código real e confirmado na fonte.

O post Tunelamento em tempo real no React: SSE e SignalR na prática apareceu primeiro em Central de Artigos.

]]>
Interfaces modernas raramente ficam paradas esperando o usuário apertar F5. Notificações, cotações, placares, logs de deploy, tokens de um LLM chegando aos poucos — tudo isso pede que o servidor empurre os dados para o navegador assim que eles existem. É o que chamo informalmente de “tunelamento”: abrir um canal por onde o backend joga eventos direto na tela. Neste artigo comparo duas formas maduras de fazer isso no React — Server-Sent Events (SSE) e SignalR — e mostro código prático de cada uma.

Server-Sent Events: streaming unidirecional sobre HTTP

SSE é um padrão do próprio navegador. O cliente abre uma conexão HTTP persistente com a API EventSource, e o servidor mantém essa conexão aberta enviando mensagens no formato text/event-stream. É um fluxo de mão única: só do servidor para o cliente.

Dois pontos que a documentação da MDN e do web.dev deixam claros e que fazem o SSE brilhar:

  • Reconexão automática. Se a conexão cai, o navegador tenta reabrir sozinho (por volta de 3 segundos por padrão). O servidor pode ajustar esse intervalo com o campo retry: e retomar de onde parou usando o id: de cada evento.
  • Zero protocolo novo. Roda sobre HTTP comum, passa por proxies e infraestrutura HTTP sem cerimônia.

Atenção a um limite histórico: sobre HTTP/1.1, os navegadores permitem no máximo ~6 conexões SSE simultâneas por domínio (contando todas as abas). Sob HTTP/2 esse teto sobe bastante, porque as requisições são multiplexadas.

Consumindo um SSE no React

O padrão é criar o EventSource dentro de um useEffect e sempre limpar a conexão no retorno. Use onmessage para eventos sem nome e addEventListener para eventos nomeados (o campo event: enviado pelo servidor).

import { useEffect, useState } from "react";

function Cotacoes() {
  const [precos, setPrecos] = useState([]);

  useEffect(() => {
    const source = new EventSource("/api/cotacoes");

    // Evento padrão (sem campo "event:" no servidor)
    source.onmessage = (e) => {
      const dado = JSON.parse(e.data);
      setPrecos((atual) => [dado, ...atual].slice(0, 20));
    };

    // Evento nomeado: servidor envia "event: alerta"
    source.addEventListener("alerta", (e) => {
      console.warn("Alerta:", e.data);
    });

    source.onerror = (err) => {
      console.error("Falha no EventSource:", err);
      // O navegador já tenta reconectar sozinho.
    };

    return () => source.close(); // fecha ao desmontar
  }, []);

  return (
    <ul>
      {precos.map((p, i) => (
        <li key={i}>{p.ativo}: {p.valor}</li>
      ))}
    </ul>
  );
}

export default Cotacoes;

Repare que não há lógica de reconexão manual: o EventSource cuida disso. Você só fecha explicitamente com source.close() quando o componente sai de cena.

SignalR: canal bidirecional com fallback automático

SignalR é a biblioteca de tempo real do ASP.NET Core. Diferente do SSE, ela é bidirecional: o cliente chama métodos no servidor (o “hub”) e o servidor chama métodos no cliente. Por baixo, o SignalR escolhe o melhor transporte disponível — de preferência WebSockets, com fallback para Server-Sent Events e, por último, Long Polling. Ou seja: o SSE pode ser até o encanamento interno do SignalR quando WebSocket não está disponível.

No cliente JavaScript/TypeScript você usa o pacote oficial @microsoft/signalr:

npm install @microsoft/signalr

Conectando a um hub no React

Monte a conexão com HubConnectionBuilder, registre os handlers com connection.on(...) antes de chamar start() (boa prática recomendada pela Microsoft) e habilite a reconexão com withAutomaticReconnect().

import { useEffect, useRef, useState } from "react";
import * as signalR from "@microsoft/signalr";

function Chat() {
  const [mensagens, setMensagens] = useState([]);
  const conexaoRef = useRef(null);

  useEffect(() => {
    const connection = new signalR.HubConnectionBuilder()
      .withUrl("/chatHub")
      .withAutomaticReconnect() // 0, 2, 10 e 30s por padrão
      .configureLogging(signalR.LogLevel.Information)
      .build();

    // Servidor chama: Clients.All.SendAsync("ReceiveMessage", user, msg)
    connection.on("ReceiveMessage", (user, message) => {
      setMensagens((atual) => [...atual, `${user}: ${message}`]);
    });

    connection.onreconnecting(() => console.log("Reconectando..."));
    connection.onreconnected(() => console.log("Reconectado!"));

    connection
      .start()
      .then(() => console.log("SignalR conectado."))
      .catch((err) => console.error(err));

    conexaoRef.current = connection;
    return () => { connection.stop(); };
  }, []);

  const enviar = async (user, texto) => {
    try {
      // Chama um método público do hub no servidor
      await conexaoRef.current.invoke("SendMessage", user, texto);
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <ul>
      {mensagens.map((m, i) => (
        <li key={i}>{m}</li>
      ))}
    </ul>
  );
}

export default Chat;

Do lado do cliente há duas formas de falar com o servidor: invoke, que retorna uma Promise resolvida com o valor de retorno do método do hub, e send, que apenas dispara a mensagem sem esperar resposta. Para receber, sempre connection.on("NomeDoMetodo", ...).

Sobre reconexão: sem parâmetros, withAutomaticReconnect() tenta reconectar aos 0, 2, 10 e 30 segundos e desiste após quatro falhas. Você pode passar um array de intervalos ou um IRetryPolicy customizado. E atenção: ele não retenta falhas do start() inicial — isso você trata na mão.

SSE x SignalR: qual escolher?

Critério Server-Sent Events SignalR
Direção Unidirecional (servidor → cliente) Bidirecional (cliente ↔ servidor)
Transporte HTTP (text/event-stream) WebSockets, com fallback para SSE e Long Polling
Cliente Nativo (EventSource), sem lib Pacote @microsoft/signalr
Reconexão Automática pelo navegador withAutomaticReconnect() (opt-in)
Servidor Qualquer stack HTTP Acoplado ao ASP.NET Core / Azure SignalR
Melhor para Feeds, notificações, streaming de texto Chat, colaboração, jogos, RPC em tempo real

A regra prática: se o fluxo é só do servidor para a tela e você quer simplicidade máxima sem depender de uma stack específica, SSE resolve com elegância — e sem instalar nada no front. Se você precisa de via dupla, chamadas RPC entre cliente e servidor, grupos, presença e um degradê robusto de transporte, especialmente num backend .NET, o SignalR entrega tudo pronto. Não são rivais absolutos: o SignalR inclusive usa SSE como um de seus transportes internos.

Comece pelo mais simples que atende ao caso de uso. Muita gente sobe um WebSocket completo para o que um humilde EventSource resolveria em dez linhas.

Fontes

O post Tunelamento em tempo real no React: SSE e SignalR na prática apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/tunelamento-em-tempo-real-no-react-sse-e-signalr-na-pratica/feed/ 0
Orquestrando múltiplos agentes com o Microsoft Agent Framework: workflows em grafo e o padrão sequencial em C# https://rickytech.com.br/orquestrando-multiplos-agentes-com-o-microsoft-agent-framework-workflows-em-grafo-e-o-padrao-sequencial-em-c/ https://rickytech.com.br/orquestrando-multiplos-agentes-com-o-microsoft-agent-framework-workflows-em-grafo-e-o-padrao-sequencial-em-c/#respond Mon, 27 Jul 2026 20:15:21 +0000 https://rickytech.com.br/?p=228 Como o Microsoft Agent Framework coordena vários agentes de IA em pipelines sequenciais e grafos dirigidos, com exemplos reais em C# usando o SDK Microsoft.Agents.AI.

O post Orquestrando múltiplos agentes com o Microsoft Agent Framework: workflows em grafo e o padrão sequencial em C# apareceu primeiro em Central de Artigos.

]]>
Construir um agente de IA que responde a uma pergunta é a parte fácil. O desafio real aparece quando você precisa coordenar vários agentes especializados para cumprir um processo de negócio de ponta a ponta — com controle, previsibilidade e capacidade de auditoria. É exatamente esse o problema que o Microsoft Agent Framework (MAF) ataca com seu módulo de Workflows.

Agente x Workflow: duas camadas de abstração

Antes de orquestrar, vale separar dois conceitos que a documentação oficial trata como distintos:

  • Agente: guiado por um LLM, decide dinamicamente quais passos e ferramentas usar. O caminho é imprevisível por natureza.
  • Workflow: uma sequência explicitamente definida de operações, que pode ter agentes como componentes. O fluxo é controlado por você, não pelo modelo.

A ideia central: use o agente para o raciocínio aberto e o workflow para amarrar esse raciocínio a um processo com topologia conhecida.

O grafo por trás de tudo

No MAF, um workflow é um grafo dirigido. Os nós são executores (executors) — unidades de processamento que recebem uma mensagem, fazem algo e emitem outra. As arestas (edges) definem como as mensagens fluem entre eles, podendo carregar condições de roteamento. Um executor pode ser um agente de IA ou código determinístico puro (validação, formatação, chamada a uma API).

A execução segue um modelo baseado em supersteps (uma variação do modelo Pregel / Bulk Synchronous Parallel). Em cada superstep, o framework coleta as mensagens pendentes, roteia para os executores-alvo, roda todos em paralelo e só avança quando todos terminam — uma barreira de sincronização. Isso garante execução determinística e checkpoints confiáveis nas fronteiras de cada superstep.

Começando: pacotes e um agente

A orquestração vive no pacote NuGet Microsoft.Agents.AI.Workflows, complementado por Microsoft.Agents.AI e Microsoft.Extensions.AI (todos em prerelease no momento). Um agente é representado pela abstração AIAgent, cuja implementação principal é a ChatClientAgent, construída sobre um IChatClient:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

// chatClient é um IChatClient (Azure OpenAI, OpenAI, GitHub Models...)
AIAgent redator = new ChatClientAgent(
    chatClient,
    new ChatClientAgentOptions
    {
        Name = "Redator",
        Instructions = "Escreva um parágrafo conciso respondendo ao pedido."
    });

// Rodando o agente isoladamente
var resposta = await redator.RunAsync("Explique o que é orquestração de agentes.");
Console.WriteLine(resposta.Text);

Orquestração sequencial: o pipeline de agentes

O padrão sequencial é o mais direto: os agentes formam um pipeline, e a saída de um alimenta o próximo. É ideal para revisão de documentos, refinamento progressivo ou pipelines de tradução — casos em que cada etapa constrói sobre a anterior.

O MAF oferece um atalho de alto nível: AgentWorkflowBuilder.BuildSequential(...), que monta o grafo de pipeline a partir de uma coleção de agentes. Por padrão, cada agente enxerga a conversa completa do anterior; dá para restringir isso apenas às respostas.

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;

// Agentes especializados que trabalham em cadeia
ChatClientAgent pesquisador = new(chatClient,
    "Levante os pontos-chave do tema solicitado.", "Pesquisador", "Coleta fatos");

ChatClientAgent redator = new(chatClient,
    "Transforme os pontos em um texto claro.", "Redator", "Escreve o rascunho");

ChatClientAgent revisor = new(chatClient,
    "Revise gramática e clareza do texto anterior.", "Revisor", "Revisa");

// Constrói o workflow sequencial: pesquisador -> redator -> revisor
var workflow = AgentWorkflowBuilder.BuildSequential(
    [pesquisador, redator, revisor]);

// Executa em modo streaming
var mensagens = new List<ChatMessage> {
    new(ChatRole.User, "Escreva sobre orquestração de agentes com MAF.")
};

await using StreamingRun run =
    await InProcessExecution.RunStreamingAsync(workflow, mensagens);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    if (evt is AgentResponseUpdateEvent atualizacao)
    {
        Console.Write(atualizacao.Update.Text);
    }
    else if (evt is WorkflowOutputEvent saida)
    {
        var resultado = saida.As<List<ChatMessage>>()!;
        Console.WriteLine($"\nConcluído com {resultado.Count} mensagens.");
        break;
    }
}

Repare no fluxo de eventos: AgentResponseUpdateEvent traz os tokens conforme são gerados (com ExecutorId identificando qual agente falou), e WorkflowOutputEvent sinaliza o resultado final. Esse modelo de eventos é o que dá observabilidade real ao pipeline.

Humano no circuito, sem esforço extra

Um diferencial prático: se um agente usa uma ferramenta sensível envolvida em ApprovalRequiredAIFunction, o workflow pausa e emite um RequestInfoEvent com um ToolApprovalRequestContent. Um operador humano aprova ou rejeita, e a execução continua — tudo pelo mesmo fluxo de eventos, sem configuração adicional.

Grafos customizados: além da linha reta

Quando você precisa de topologias que não cabem num pipeline — ramificações condicionais, fan-out/fan-in, misturar agentes com código determinístico — usa-se a API de baixo nível WorkflowBuilder, conectando executores com AddEdge:

using Microsoft.Agents.AI.Workflows;

var processador = new DataProcessor();   // executor customizado
var validador   = new Validator();
var formatador  = new Formatter();

// O executor passado ao construtor é o ponto de partida
WorkflowBuilder builder = new(processador);
builder.AddEdge(processador, validador);
builder.AddEdge(validador, formatador);
var workflow = builder.Build();

// Execução não-streaming: aguarda a conclusão
Run resultado = await InProcessExecution.RunAsync(workflow, entrada);
foreach (WorkflowEvent evt in resultado.NewEvents)
{
    if (evt is WorkflowOutputEvent saida)
        Console.WriteLine($"Resultado final: {saida.Data}");
}

Ao construir o grafo, o framework valida compatibilidade de tipos entre executores conectados, conectividade (todos alcançáveis a partir do início) e arestas duplicadas. É a “type safety” que evita erros descobertos apenas em runtime.

Regra prática: comece pelo BuildSequential para pipelines lineares; migre para o WorkflowBuilder quando precisar de roteamento condicional, paralelismo real ou executores não-LLM no meio do caminho.

Quando usar cada abordagem

  • Sequencial (BuildSequential): revisão em etapas, tradução em cadeia, refinamento progressivo.
  • Grafo (WorkflowBuilder): topologias fixas com fan-out/fan-in, roteamento por condição e validação estrita de mensagens.
  • Checkpointing: para processos longos, o MAF salva o estado nas fronteiras dos supersteps, permitindo retomar de onde parou.

O resultado é um modelo em que você ganha a flexibilidade cognitiva dos agentes sem abrir mão do controle de um processo bem definido — a combinação que faltava para levar sistemas multiagente do protótipo à produção.

Fontes

O post Orquestrando múltiplos agentes com o Microsoft Agent Framework: workflows em grafo e o padrão sequencial em C# apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/orquestrando-multiplos-agentes-com-o-microsoft-agent-framework-workflows-em-grafo-e-o-padrao-sequencial-em-c/feed/ 0
Microsoft Agent Framework vs. LangGraph: o guia prático dos frameworks de agentes de IA https://rickytech.com.br/microsoft-agent-framework-vs-langgraph-o-guia-pratico-dos-frameworks-de-agentes-de-ia/ https://rickytech.com.br/microsoft-agent-framework-vs-langgraph-o-guia-pratico-dos-frameworks-de-agentes-de-ia/#respond Mon, 27 Jul 2026 06:51:16 +0000 https://rickytech.com.br/?p=200 Dois dos frameworks mais comentados para construir agentes de IA seguem filosofias diferentes. Entenda o que cada um é, como se comparam e quando escolher um ou outro.

O post Microsoft Agent Framework vs. LangGraph: o guia prático dos frameworks de agentes de IA apareceu primeiro em Central de Artigos.

]]>
Construir um chatbot que responde a uma pergunta é uma coisa. Construir um agente que planeja tarefas, chama ferramentas, mantém contexto entre passos e sabe pedir ajuda a um humano quando trava é outra bem diferente. É aí que entram os frameworks de agentes de IA — camadas de software que cuidam da orquestração para você não reinventar a roda a cada projeto. Dois nomes dominam a conversa hoje: o Microsoft Agent Framework e o LangGraph. Vamos entender cada um e, principalmente, quando usar qual.

O que é um framework de agentes

Um agente de IA é um sistema em que um modelo de linguagem (LLM) decide, em ciclos, quais ações tomar: consultar uma API, ler um documento, chamar outro agente ou responder ao usuário. O desafio não é o modelo em si, e sim tudo ao redor: manter o estado da conversa, permitir que o processo pause e retome, orquestrar vários agentes e garantir que dá para observar e depurar o que acontece.

Frameworks de agentes existem para resolver a parte chata e crítica: estado, persistência, orquestração e observabilidade — não a “mágica” do LLM.

Microsoft Agent Framework: a consolidação da Microsoft

Aqui é preciso cuidado com os nomes, porque a Microsoft tinha dois projetos separados. O Semantic Kernel trazia fundações voltadas a empresas, e o AutoGen, nascido na Microsoft Research, era mais experimental e focado em orquestração de múltiplos agentes. O Microsoft Agent Framework é a fusão dos dois em um único SDK. Ele foi anunciado em versão preview em outubro de 2025 e chegou à versão 1.0, considerada pronta para produção, em 3 de abril de 2026, com APIs estáveis e compromisso de suporte de longo prazo.

É um projeto open source (licença MIT) disponível para .NET (pacote NuGet Microsoft.Agents.AI) e Python (pacote PyPI agent-framework). Entre os recursos que a Microsoft destaca:

  • Um motor de workflows baseado em grafos para compor agentes e funções em processos determinísticos e repetíveis.
  • Padrões de orquestração multiagente: sequencial, concorrente, handoff (passagem de bastão), group chat e o padrão Magentic-One.
  • Suporte a vários provedores: Microsoft Foundry, Azure OpenAI, OpenAI, Anthropic Claude, Amazon Bedrock, Google Gemini e Ollama.
  • Padrões de interoperabilidade como MCP (Model Context Protocol) para descoberta de ferramentas e A2A (Agent-to-Agent) para colaboração entre agentes.
  • Observabilidade com integração ao OpenTelemetry, um depurador de navegador (DevUI) e fluxos de aprovação para manter humanos no circuito.

A Microsoft posiciona o Agent Framework como o sucessor de Semantic Kernel e AutoGen, recomenda-o como padrão para novos projetos e publicou guias oficiais de migração a partir dos dois antecessores.

LangGraph: controle de baixo nível e estado explícito

O LangGraph, do ecossistema LangChain, se descreve como um “framework e runtime de orquestração de baixo nível” para agentes com estado e execução longa. A palavra-chave aqui é estado. Em LangGraph você define primeiro um esquema de estado tipado; cada nó do grafo recebe esse estado e devolve uma atualização parcial. Os conceitos centrais são:

  • Estado, nós e arestas: os nós são passos de processamento e as arestas definem as transições — inclusive ciclos, o que permite raciocínio iterativo.
  • Persistência e checkpointing: o agente pode pausar e retomar depois de uma falha, sem perder o contexto.
  • Humano no circuito e streaming: interrupções para aprovação humana e transmissão de resultados em tempo real.

LangGraph roda em Python e JavaScript e integra-se ao restante do ecossistema: o LangChain oferece abstrações de mais alto nível, e o LangSmith cuida de observabilidade, avaliação e rastreamento. A LangChain cita empresas como Klarna, Uber e J.P. Morgan entre os usuários. É o framework preferido de quem quer controle fino e explícito sobre cada passo do agente.

Como se comparam

A verdade é que, em recursos, os dois convergiram bastante. Ambos oferecem hoje workflows baseados em grafos, nós e arestas tipados, checkpointing, interrupção para humano no circuito, orquestração multiagente e suporte a MCP. A tabela de funcionalidades é quase idêntica. A diferença real está na filosofia e no ecossistema.

LangGraph pede que você pense em termos de grafo e estado desde o primeiro momento; o Agent Framework parte do conceito de agentes e workflows e se encaixa naturalmente no mundo Microsoft.

Quando escolher LangGraph

  • Você quer orquestração “grafo primeiro”, com controle explícito e granular do fluxo.
  • Sua stack é Python ou JavaScript e você já usa (ou pretende usar) LangChain e LangSmith.
  • O caso de uso exige ciclos, memória persistente e precisão em cada etapa.

Quando escolher Microsoft Agent Framework

  • Seu time trabalha com .NET (ou Python) e já vive no ecossistema Azure/Microsoft Foundry.
  • Você quer um framework novo, com APIs estáveis 1.0 e suporte de longo prazo, construído em torno de agentes e workflows.
  • Você está migrando de Semantic Kernel ou AutoGen e quer o caminho oficial recomendado.

Conclusão

Não existe vencedor universal. Se o seu mundo é .NET e Azure, o Microsoft Agent Framework é a aposta mais natural e recém-amadurecida. Se você valoriza controle de baixo nível, ciclos explícitos e já investe no ecossistema LangChain, o LangGraph continua sendo uma escolha sólida. O melhor conselho é começar pequeno: modele um fluxo real, avalie a curva de aprendizado e a observabilidade, e deixe o caso de uso decidir.

Fontes

O post Microsoft Agent Framework vs. LangGraph: o guia prático dos frameworks de agentes de IA apareceu primeiro em Central de Artigos.

]]>
https://rickytech.com.br/microsoft-agent-framework-vs-langgraph-o-guia-pratico-dos-frameworks-de-agentes-de-ia/feed/ 0