<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-02-22T22:54:55+00:00</updated><id>/feed.xml</id><title type="html">Gabriel Arruda</title><subtitle>Anedotas sobre dados</subtitle><entry><title type="html">O cientista que não gostava de números</title><link href="/2026/02/21/metricas.html" rel="alternate" type="text/html" title="O cientista que não gostava de números" /><published>2026-02-21T00:00:00+00:00</published><updated>2026-02-21T00:00:00+00:00</updated><id>/2026/02/21/metricas</id><content type="html" xml:base="/2026/02/21/metricas.html"><![CDATA[<p>O mundo corporativo é cheio de jargões e frases prontas. Alguns exemplos no contexto de dados:</p>

<blockquote>
  <p>“If you cannot measure it, you cannot improve it.” (Lord Kelvin)</p>
</blockquote>

<blockquote>
  <p>“In God we trust; all others must bring data. (W. Edwards Deming)</p>
</blockquote>

<blockquote>
  <p>Errors using inadequate data are much less than those using no data at all (Charles Babbage)</p>
</blockquote>

<p>Em geral, as afirmações fazem sentido, mas frases rápidas não tratam de nuances. Ao aplicar a cultura “data driven”, vejo dois erros comuns que são pouco discutidos:</p>

<ul>
  <li>ignorar as limitações dos dados, confiar em métricas poucos representativas;</li>
  <li>interpretar os dados de forma simplória, caindo em erros por análises rasas.</li>
</ul>

<p>Quando percebo que esses problemas podem ocorrer – tenho um comportamento contra-intuitivo como cientista de dados – que é evitar trazer números. Pelo menos, tento trazê-los de forma que destaque essas limitações. Não tenho como controlar a interpretação dos resultados, mas tento destacar as limitações e pontos de atenção.</p>

<p>De forma alguma, quero questionar a cultura “data driven” que se criou na última década: essa é uma discussão de um problema menor, no contexto de uma mudança muito positiva. Antes errar tentando usar dados, que nem tentar.</p>

<h2 id="a-falsa-confiança-da-matemática">A falsa confiança da matemática</h2>

<p>Existe uma discussão filosófica sobre a matemática ter sido <a href="https://impa.br/notices/a-matematica-e-criada-ou-descoberta-pergunta-viana-na-folha/">criada ou descoberta</a>. Um argumento, a favor do segundo cenário, é sua relação com as ciências naturais<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>:</p>

<blockquote>
  <p>Se o π foi inventado para estudar o perímetro do círculo, então por que aparece na lei da distribuição normal de Gauss, um dos princípios fundamentais da estatística? O que perímetro do círculo tem a ver com estudo de populações e pesquisas de opinião?</p>
</blockquote>

<p>Independente da sua opinião sobre essa discussão, a efetividade da matemática nas ciências naturais é indiscutível. E o grande sucesso das ciências naturais nos últimos séculos, gerou o efeito de <a href="https://en.wikipedia.org/wiki/Physics_envy">“inveja da física”</a>, em que outras áreas do conhecimento tentam replicar o uso de métodos matemáticos das ciências naturais.</p>

<p>Por que <a href="https://www.nytimes.com/2017/03/17/upshot/what-if-sociologists-had-as-much-influence-as-economists.html">economistas são mais influentes</a> que sociologistas? Justamente por utilizar muitos modelos matemáticos, a Economia parece mais próxima das ciências naturais que outras áreas de humanas. O que não significa que <a href="https://aeon.co/essays/how-economists-rode-maths-to-become-our-era-s-astrologers">realmente esteja</a>, mas mesmo sem resultados consistentes, o uso de linguagem matemática inescrutável é o suficiente para trazer credibilidade.</p>

<p>Modelar e representar algo matematicamente não é garantia de nada, não é somente sobre a qualidade da modelagem ou disponibilidade de dados, mas principalmente do quão bem a matemática se aplica ao problema. A questão não é a falta de capacidade dos economistas em modelar, mas a natureza do problema que estão lidando.</p>

<h2 id="preciso-de-um-número-somente-um-número">Preciso de um número, somente um número</h2>

<p>Se você já respondeu a pergunta “Você recomendaria X para um amigo ou familiar?” em alguma pesquisa de satisfação, é porque a empresa adotou a metodologia <a href="https://pt.wikipedia.org/wiki/Net_Promoter_Score">Net Promoter Score (NPS)</a>. Ela foi popularizada por um artigo publicado na HBR em 2003, com o chamativo título <a href="https://www.nashc.net/wp-content/uploads/2014/10/the-one-number-you-need-to-know.pdf">“The One Number You Need to Grow”</a>. A premissa era ambiciosa:</p>

<blockquote>
  <p>The good news is: you don’t need expensive surveys and complex statistical models. You only have to ask your customers one question: “How likely is it that you would recommend our company to a friend or colleague?” The more “promoters” your company has, the bigger its growth.</p>
</blockquote>

<p>No próprio artigo, são discutidas limitações da métrica, como em cenários de monopólio ou que o consumidor não tem opções (e.g. saneamento básico, sistemas corporativos). A métrica teve uma grande adoção do mercado – surgiram estudos questionando a superioridade do NPS (<a href="https://www.researchgate.net/publication/228660597_A_Longitudinal_Examination_of_Net_Promoter_and_Firm_Revenue_Growth">1</a>, <a href="https://www.researchgate.net/publication/230557989_A_Holistic_Examination_of_Net_Promoter">2</a>) em relação às demais métricas de satisfação – mas não parece ter impactado em sua fama.</p>

<p>O foco não é criticar o NPS, mas ilustrar o apelo que métricas simples e ambiciosas têm. O mundo digital inundou as empresas e as nossas vidas com métricas e dados sobre tudo, mas é humanamente impossível organizar e extrair sentido sem ajuda. A “solução” é simplificar esses dados a qualquer custo e reduzir tudo a um valor mágico.</p>

<p>A <a href="https://shop.whoop.com/pt/pt/">WHOOP</a> calculou que <a href="https://www.instagram.com/whoop/p/DT24U9xjyiQ/?img_index=5">Cristiano Ronaldo tem 28 anos de “idade biológica”</a>, usando sensores  básicos e não invasivos das suas pulseiras, encontrados em qualquer relógio inteligente. São dados extremamente limitados que alimentam um modelo proprietário, mas que foi publicado em vários locais (<a href="https://www.menshealth.com/uk/fitness/a64860523/cristiano-ronaldo-biological-age/">1</a>, <a href="https://www.si.com/soccer/cristiano-ronaldo-reveals-stunning-biological-age">2</a>, <a href="https://www.marca.com/en/football/2025/05/21/682ddc75e2704e80108b457e.html">3</a>) como uma verdade absoluta, com o próprio atleta dizendo <a href="https://www.facebook.com/Cristiano/posts/the-data-doesnt-lie-whoop-/1494201485399720/">“The data doesn’t lie”</a>.</p>

<p>Quando questiono o NPS, não é porque eu tenho uma proposta melhor ou que deveríamos abandoná-lo, a ideia é apenas ter atenção às limitações e tomar cuidado ao transformar esse tipo de métrica em KPIs e OKRs. Prever crescimento de uma empresa e avaliar satisfação do cliente – são problemas abstratos, cheio de nuances e contextual – é muito difícil que um único indicador dê conta de representar bem esses problemas.</p>

<p>É uma posição impopular, tanto com as áreas de negócio quanto com os parceiros da área de dados, quando eu questiono se deveríamos focar tanto em determinados números. Eu entendo a premissa de que usar um número – impreciso e/ou pouco representativo ainda é melhor que nada – mas é muito difícil apresentar um número e depois dissuadir as pessoas de confiarem cegamente nele.</p>

<p>Ao trazer um número e colocar vários ressalvas para utilizá-lo, as pessoas interpretam como falta de confiança. A limitação pode ser inerente ao problema, mas é algo difícil de explicar, especialmente para uma audiência menos especializada. Sempre que você titubear, haverá alguém vendendo números mágicos e muito apetite para utilizá-los.</p>

<h2 id="não-é-tão-simples-quanto-parece">Não é tão simples quanto parece</h2>

<p>Um outro problema comum ao utilizar dados, é fazer leitura errada ou limitada das métricas. Uma questão sensível no Brasil, que ilustra muito bem esse problema, são discussões sobre renda.</p>

<p>As pessoas ficam surpresas, ao saberem que estão entre os 10% ou 5% mais ricos do país. É complicado compreender o que significa alguém ser bilionário, existir pessoas que estão 3 ordens de grandeza acima do limiar dos top 5%: esse tipo de coisa não existe em uma curva normal, nenhuma pessoa tem 200 metros de altura por exemplo. Distribuição de renda é um <a href="https://share.google/aimode/g58U1jer97zUD6EJG">problema complexo</a> de modelar, com números que <a href="https://www.npr.org/2024/01/03/1198909057/brain-struggles-big-numbers-neuroscience">fogem da nossa compreensão</a> intuitiva sobre números.</p>

<p>Programadores aprenderam a olhar <a href="/2023/04/16/grpc-rest.html">tempos de reposta</a> sem assumir distribuição normal, é uma prática acompanhar percentis altos (.e.g. P95 e P99) para ter uma visão mais concreta do cenário. Mas ainda é muito difícil explicar como funciona: o tempo médio de reposta da API é 50ms, mas que não é raro ficar em  20ms ou passar de 500ms. Esses valores não encaixam na intuição de média e desvio padrão que as pessoas aprendem, tendo como perspectiva uma gaussiana.</p>

<p>Para lidar com dados fora da distribuição normal, é importante avaliar o cenário com boxplot, histograma e testes de normalidade. Simplesmente olhar métricas como média e desvio padrão, pensando em uma gaussiana, pode levar a uma interpretação errada da situação. Mas quantas pessoas sabem o que é um boxplot?</p>

<p>É fundamental que a área de negócio faça a interpretação dos dados – algo que os especialistas em dados podem e devem ajudar – mas em última instância, não é algo que pode ser completamente delegado para o time de ciência de dados.</p>

<h2 id="quando-o-mundo-cometeu-esses-erros">Quando o mundo cometeu esses erros</h2>

<p>Um cenário que exemplifica ambos os desafios, foram os <a href="https://en.wikipedia.org/wiki/Compartmental_models_(epidemiology)">modelos epidemiológicos</a> utilizados durante a pandemia de COVID-19. Os modelos de compartimento são baseados em derivadas parciais, em que a curva de infecção é governada pela taxa \(R_{t}\): um indivíduo infecta quantas outras pessoas em um tempo \(t\) ?</p>

<p>Por esses modelos, lidar com a pandemia era uma questão de achatar a curva, reduzindo o \(R_{t}\) para os sistemas de saúde suportarem a demanda. Achatar a curva significava ter uma pandemia mais longa, mas com um pico de infecção menor.</p>

<figure style="text-align:center;">
  <img src="/assets/images/metricas/curva.png" />
</figure>

<p>Os gráficos gerados por esses modelos foram vistos como exemplos de “storytelling” e tomada de decisão com dados , mas na prática as coisas foram diferentes. Percebeu-se que esse modelo não respondia às perguntas que a população fazia e nem serviam como preditor.</p>

<p>Podia-se estimar um \(R_{t}\) alvo, mas como medir esse número e como chegar nesse alvo? Percebeu-se que \(R_{t}\) era uma média, mas <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC8328407/">pessoas e contextos <em>outliers</em></a> poderiam ser o grande problema. Além disso, a nossa capacidade de diagnosticar a doença tem limitações importantes, era muito difícil saber ao certo quantas pessoas estavam infectadas em um determinado momento.</p>

<p>Além da dificuldade em medir o \(R_{t}\) real, o modelo de compartimentos não explicava a realidade do sistema de saúde. O mesmo \(R_{t}\) – considerando um asilo ou um vestiário de futebol profissional – traziam impactos práticos muito distintos no sistema de saúde. Outros parâmetros, como tempo de recuperação e re-infecção mudavam a todo momento: mutações do virus, tratamentos, prevenções e vacinas.</p>

<p>Era um cenário muito complexo, para um modelo muito simples. Uma pandemia global não é como uma doença infecciosa em ambiente natural: animais não se previnem, não fazem viagens intercontinentais, não procuram tratamentos e nem desenvolvem vacinas.</p>

<p>Apesar de ser impossível parametrizar os modelos corretamente em grande escala, a dinâmica geral de infecção descrita pelos modelos, ainda fazia sentido: achatar a curva era um plano, mesmo que não fosse possível estimar com precisão os parâmetros do modelo. Em contextos limitados e para projeções, esses modelos e o  \(R_{t}\) eram úteis.</p>

<p>Ou seja, tínhamos métricas e um modelo insuficiente para lidar com a complexidade da situação. Além disso, interpretar os resultados e as implicações, era algo muito complexo para a população e tomadores de decisão.</p>

<h2 id="conclusão">Conclusão</h2>

<p>Após anos de incentivo à tomada de decisão baseada em dados, talvez seja um tabu questionar seu uso. Todo modelo é uma aproximação e não uma realidade, como diria o “ditado” da estatística: <a href="https://en.wikipedia.org/wiki/All_models_are_wrong">todos os modelos estão errados, mas alguns são úteis</a>.</p>

<p>Sou a favor de usar dados e modelos sempre que possível, mas não virar uma muleta para toda tomada de decisão. Infelizmente, nem sempre é possível trabalhar com dados, especialmente em produtos muito inovadores ou cenários em que pesquisa e testes são muito caros. É aceitar que é necessário tomar algumas decisões por convicção ou intuição, ao invés de forçar o uso de dados e modelos insuficientes.</p>

<p>Steve Jobs – talvez a maior referência dos “produteiros” – era famoso por não ser o maior entusiasta de pesquisas de mercado (e nem de consultores). Nenhum dashboard ou modelo, iria corroborar com a ideia de fazer um <a href="https://en.wikipedia.org/wiki/IPhone_(1st_generation)">smartphone sem teclado em 2007</a>. Muito pelo contrário, já que o produto na época era restrito ao mercado corporativo, com foco em comunicação via texto ao invés de consumo de conteúdo de hoje em dia. O valor da ideia era justamente ir contra o restante do mercado, não fazer algo incremental.</p>

<p>Os riscos existem: você provavelmente não tem a visão do Steve Jobs, nem os recursos da Apple e não trabalha em uma empresa com o mesmo apetite a risco. Mas forçar um número que justifique uma decisão revolucionária e arriscada, não vai mitigar seus riscos, mas pode mascará-los.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Se você já leu algum artigo com título no estilo <em>The Unreasonable Effectiveness of X in Y</em>, a origem foi um sobre essa discussão da matemática e ciências da natureza: <a href="https://en.wikipedia.org/wiki/The_Unreasonable_Effectiveness_of_Mathematics_in_the_Natural_Sciences">“The Unreasonable Effectiveness of Mathematics in the Natural Sciences”</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[O problema da estratégia data-driven]]></summary></entry><entry><title type="html">Programar não é a parte difícil</title><link href="/2025/11/23/vibe-coding.html" rel="alternate" type="text/html" title="Programar não é a parte difícil" /><published>2025-11-23T00:00:00+00:00</published><updated>2025-11-23T00:00:00+00:00</updated><id>/2025/11/23/vibe-coding</id><content type="html" xml:base="/2025/11/23/vibe-coding.html"><![CDATA[<p>Como alguém que trabalha com dados e gosta de refletir sobre programação (<a href="/2025/05/24/aprendendo-com-cientistas-dados.html">1</a>, <a href="/2024/ 01/20/low-code-dilema.html">2</a>, <a href="/2024/06/26/codigo-limpo-positivismo-fotografia.html">3</a> e <a href="/2020/09/23/linguagens-programacao.html">4</a>), um post sobre “vibe coding” era inevitável. Ao final, chego a conclusão não muito bombástica: é uma ferramenta muito útil e difícil de abandonar após se acostumar, mas não resolve o problema fundamental de fazer software.</p>

<h1 id="a-eterna-promessa-do-low-code-a-nova-do-vibe-code">A eterna promessa do low code, a nova do vibe code</h1>

<p>A promessa do vibe code é muito similar ao low code, tornar desenvolvimento de software acessível para quem não sabe programar. Mas, ao invés de inventar uma nova linguagem visual baseadas em workflows, a ideia é que os agentes consigam traduzir especificações em linguagem natural para código.</p>

<p>Eu já discuti sobre os motivos do <a href="/2024/01/20/low-code-dilema.html">low code</a> sempre ficar aquém das expectativas criada pelos vendedores – alguns desses problemas são mitigados com vibe coding, outros persistem – mas a limitação fundamental é a mesma: a grande dificuldade não é programar, mas lidar com a complexidade inerente de grandes sistemas.</p>

<p>Os problemas que um ERP precisa resolver são bem conhecidos (e.g. gestão de estoque, cálculos de imposto, contabilidade) e não exigem conhecimentos em tópicos avançados de computação. Só que cada empresa tem um ERP diferente, seguindo a  <a href="https://en.wikipedia.org/wiki/Conway%27s_law">“lei de Conway”</a>:</p>

<blockquote>
  <p>Organizations which design systems (in the broad sense used here) are constrained to produce designs which are copies of the communication structures of these organizations.</p>
</blockquote>

<p>Suponha uma nova regra para cálculo imposto – o desafio não é compreender e codificar a regra – é saber em que parte do sistema esse cálculo é feito, identificar os impactos da mudança e desenhar como será alterado. O padrão é que profissionais mais experientes e arquitetos façam o desenho da solução, profissionais menos experientes fazem a codificação.</p>

<p>No “mundo real”, especificar o problema é bem mais complexo do que implementar. Os agentes são muito efetivos na implementação de algo específico, mas ainda são bem limitados para tratar problemas vagos. Existem agentes mais ambiciosos como <a href="https://devin.ai">Devin</a>, que se propõe a trabalhar a partir de um chamado, mas os relatos são de <a href="https://www.answer.ai/posts/2025-01-08-devin.html">experiências frustrantes</a>:</p>

<blockquote>
  <p>But that’s the problem - it rarely worked. Out of 20 tasks we attempted, we saw 14 failures, 3 inconclusive results, and just 3 successes. More concerning was our inability to predict which tasks would succeed. Even tasks similar to our early wins would fail in complex, time-consuming ways. The autonomous nature that seemed promising became a liability - Devin would spend days pursuing impossible solutions rather than recognizing fundamental blockers.</p>
</blockquote>

<p>O vibe code compartilha da mesma limitação fundamental do low code, mas seria injusto dizer que ele não é um avanço em outros aspectos. O principal avanço é trabalhar com código em linguagens e frameworks populares, ao invés de DAGs especificados em uma tecnologia proprietária.</p>

<p>As grandes evoluções em linguagens, arquiteturas e metodologias – orientação a objetos, microsserviços, data mesh, devops – surgiram para (tentar) domar a entropia dos sistemas. As ferramentas de low code são ótimas para demonstrações simples, mas oferecem muito pouco para controlar a complexidade.</p>

<p>Mesmo com pessoas capacitadas e experientes, acabamos com sistemas insustentáveis que precisam ser substituídos. Pessoas leigas gerando milhares de linhas de código – como podemos ver com sistemas feito sem boas práticas, planilhas e ferramentas low code – é apenas acelerar esse processo.</p>

<p>Por melhor que seja o código, qualquer linha a mais significa débito técnico: mais possibilidades de falhas de segurança, erros de lógica e tempo para entender o sistema. Gerar muito código com vibe code é fácil, mas pode ser uma armadilha disfarçada de produtividade.</p>

<h1 id="reduzindo-expectativas">Reduzindo expectativas</h1>

<p>A frustração é proporcional às expectativas criada, até os mais entusiastas concordam que “IA” ficou aquém: o Chat GPT foi lançado em Novembro de 2022, três anos depois e não temos um mundo tão diferente assim. Deixando as expectativas de lado, é algo que tenho usado regularmente para programação e certamente sentiria falta se perdesse acesso.</p>

<p>Nesse cenário de hype infinito, talvez a opinião de alguém que tem preguiça disso tudo e até 
um pouco de “ranço”, seja interessante: uso apenas o chat como um Stack Overflow melhorado. Não fico explorando diferentes modelos, nunca usei IDEs otimizadas para agentes e nem perco muito tempo com “prompt engineering”.</p>

<p>A minha primeira experiência com IA foi usando o auto completar, algo que eu logo desisti. Algumas vezes o auto completar parecia estar lendo minha mente, mas na maioria dos casos era uma distração perigosa:</p>

<ul>
  <li>
    <p>código verboso demais, com tratamentos de erro desnecessários e padrões diferentes do restante do código;</p>
  </li>
  <li>
    <p>geração de código para problemas que eu sei resolver, perdia mais tempo lendo a sugestão que implementando sem auxílio;</p>
  </li>
  <li>
    <p>erros bobos, como um nome de variável ou definição de constante, que podem ser identificados muito tarde no processo de desenvolvimento.</p>
  </li>
</ul>

<p>Por isso, eu prefiro ir proativamente ao chat quando faz sentido. Eu testei o modo agente do VS Code, mas não gostei muito de precisar revisar as mudanças, prefiro ler a resposta e editar manualmente os arquivos. Era muito trabalhoso, quando o agente fazia mudanças parcialmente corretas, para aceitar apenas o que eu desejava.</p>

<p>Pedir via chat é o jeito mais básico de aplicar IA, nada que não conseguiria resolver olhando a documentação e Stack Overflow. A grande vantagem é o contexto: não preciso ir para um navegador tirar dúvidas e o agente responde melhor com acesso ao código. Não perco o <a href="https://en.wikipedia.org/wiki/Flow_(psychology)">flow</a>, porque não preciso sair da IDE para pesquisar e as soluções vêm mais prontas.</p>

<p>A estrutura principal do código eu prefiro fazer manualmente, o que eu peço são trechos de código para resolver problemas pontuais (e.g. como sortear uma posição do array, conectar ao banco de dados e fazer uma requisição HTTP). Quando preciso trabalhar em outras linguagens ou com novas bibliotecas, uso mais regularmente e isso deixa o aprendizado mais fluído.</p>

<p>Para códigos descartáveis, eu não vejo problemas em delegar completamente para IA. Códigos para gerar gráficos ou scripts que serão utilizados uma vez, não faço questão de cuidar da estrutura. Além de códigos menos críticos, problemas repetitivos em projetos como scaffold de modelos e criação de mocks para testes, sou menos criterioso com o código gerado.</p>

<p>Se eu não pudesse mais utilizar AI para programar, eu perderia produtividade? Imagino que de forma marginal, mas certamente teria uma percepção de menos produtividade. As tarefas que delego são as coisas chatas e repetitivas, que talvez não tomem muito tempo, mas é justamente o que não quero fazer.</p>

<p>É muito comum programadores discutirem avidamente, qual linguagem ou IDE é mais produtiva, mas existe zero rigor ou mesmo dados para essas afirmações. E tudo bem, é importante que você se sinta bem com as suas ferramentas, não precisa de um resultado científico que comprove isso. Esse anseio moderno de metrificar tudo, gera algo pior que não ter métricas: usar números pouco representativos como verdade absoluta.</p>

<p>Como eu disse, tenho um pouco de preguiça de explorar mais e faço um uso básico. Já li relatos de pessoas que viraram quase orquestradoras de agentes, mas não me convenci de que se aplica bem ao meu dia-a-dia. Se existir um jeito maravilhoso de usar IA para programação, tenho certeza que chegará em mim de alguma forma.</p>

<h1 id="vamos-chegar-lá">Vamos chegar lá?</h1>

<p>Pessoalmente, não tenho expectativas que esse cenário vá mudar no futuro próximo, ainda estaremos presos nessa situação: IA ajudará cada vez mais a programar, mas ainda não consegue descobrir o que deve ser programado. Posso estar errado, mas novamente: eu vou saber, se existir um jeito incrível de utilizar IA para programação.</p>

<p>Minha preocupação atual, é mais com os iniciantes. Já era um problema pessoas que copiavam código do Stack Overflow sem entender, caindo em um <a href="&quot;https://pt.wikipedia.org/wiki/Pontos_extremos_de_uma_função&quot;">máximo local</a>: consegue fazer algumas tarefas básicas, mas não evolui por estar copiando sem entender. O vibe code te leva muito mais longe sem entender as coisas, mas é ruim pensando em evolução profissional trabalhar dessa forma.</p>

<p>A minha maior dúvida é como o mercado ficaria, se uma <a href="https://www.npr.org/2025/11/23/nx-s1-5615410/ai-bubble-nvidia-openai-revenue-bust-data-centers">possível bolha de IA</a> estourasse. Eu não sei quanto custa para manter os maiores modelos rodando, quantos usuários seriam necessários e qual o valor a ser cobrado. Quais modelos seriam financeiramente viáveis? Utilizaríamos modelos locais menores?</p>

<p>Talvez o cenário mude muito – mas ao final de 2025 – ainda utilizo IA como mais um recurso legal da IDE.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Mais uma opinião sobre vibe coding]]></summary></entry><entry><title type="html">Navegando por mundos pequenos</title><link href="/2025/09/14/hnsw.html" rel="alternate" type="text/html" title="Navegando por mundos pequenos" /><published>2025-09-14T00:00:00+00:00</published><updated>2025-09-14T00:00:00+00:00</updated><id>/2025/09/14/hnsw</id><content type="html" xml:base="/2025/09/14/hnsw.html"><![CDATA[<p>Quando comecei a estudar sobre banco de dados vetorial, fiquei surpreso ao descobrir que não existe garantia de resultado ótimo. Por exemplo, ao buscar pelos \(k\) vetores mais próximos de um vetor \(v\), não é garantido que os vetores retornados serão os mais próximos do conjunto.</p>

<p>Seguindo meus <a href="/2025/05/24/aprendendo-com-cientistas-dados.html">próprios princípios</a>, procurei saber os conceitos por trás dessas soluções e descobri sobre o <a href="https://arxiv.org/pdf/1603.09320">HNSW (Hierarchical Navigable Small World)</a>. É um algoritmo muito interessante – que junta os conceitos de skip-list e uma estrutura de grafos – para oferecer complexidade \(log (N)\) na busca de <em>embeddings</em>.</p>

<p>Como esse algoritmo funciona e por que ele nem sempre retorna o resultado ótimo?</p>

<h2 id="o-problema">O problema</h2>

<p>Uma das aplicações mais comuns para o uso de <em>embeddings</em> são soluções baseadas em <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG</a>, no qual a etapa de <em>retrieval</em> é uma busca vetorial para alimentar a geração de resposta. É importante notar que a “busca vetorial” é, na verdade, um problema de ranqueamento: a proposta é ordernar o conteúdo por relevência e não filtrar o conjunto de dados por alguma chave de busca.</p>

<p>É um problema trivial para poucos vetores – basta calcular o produto interno entre o vetor da consulta e os vetores do conjunto de dados – mas não escala bem. Esse cálculo demanda uma matriz contendo todos os <em>embeddings</em> do conjunto de dados em memória, o que torna essa solução inviável para grandes conjuntos de dados.</p>

<p>A proposta do HNSW é análoga a um índice de banco de dados, criar uma estrutura de dados adicional para otimizar a recuperação de informação. A estrutura é uma hierarquia de grafos, em que cada nó representa um documento no conjunto de dados e as arestas são geradas por heurísticas com base na proximidade dos vetores.</p>

<figure>
  <img src="/assets/images/hnsw/hnsw.png" style="width:50%;margin-left: auto;margin-right: auto;" />
  <figcaption>Figura 1 – Exemplo de HNSW</figcaption>
</figure>

<p>Diferente de outras estruturas de índice, ela não garante o resultado ótimo: a depender dos parâmetros de busca e criação, é possível obter respostas diferentes para uma mesma consulta e mesmo conjunto de vetores. A construção do grafo também tem processos estocásticos, construir um HNSW com os mesmos dados e parâmetros, não garante uma mesma estrutura final.</p>

<p>O HNSW é uma estrutura de dados com aspectos de modelo de machine learning: a <em>performance</em> precisa ser avaliada tanto da perspectiva de computação como pelo prisma de qualidade resultados. Erros de configuração não atrapalham apenas o tempo de resposta, mas a qualidade dela também.</p>

<h2 id="criando-os-pequenos-mundos">Criando os pequenos mundos</h2>

<p>Os “mundos pequenos” do HNSW são as camadas da hierarquia de grafos da estrutura. Ao inserir um ítem, a hierarquia do grafo é escolhida de forma estocástica, usando a seguinde função:</p>

<p>\[ \lfloor -ln(U(0,1)) \cdot m_{l} \rfloor \]</p>

<p>sendo \(U(0,1)\) uma distribuição uniforme no intervalo \([0,1]\) e \(m_{l}\) um hiperparâmetro no intervalo \([0,1]\). Abaixo, a implementação em Python dessa etapa:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HNSW</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">m_max</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">m_max0</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">ef_construction</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">m_l</span><span class="p">:</span> <span class="nb">float</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Layer</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_max</span> <span class="o">=</span> <span class="n">m_max</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_max0</span> <span class="o">=</span> <span class="n">m_max0</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_l</span> <span class="o">=</span> <span class="n">m_l</span>
        <span class="n">self</span><span class="p">.</span><span class="n">ef_construction</span> <span class="o">=</span> <span class="n">ef_construction</span>
        <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">_get_level</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">floor</span><span class="p">(</span><span class="o">-</span><span class="n">np</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">uniform</span><span class="p">())</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">m_l</span><span class="p">))</span>
    
    <span class="k">def</span> <span class="nf">insert</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]):</span>
        
        <span class="n">level</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_level</span><span class="p">()</span>

        <span class="bp">...</span>
    <span class="bp">...</span>
</code></pre></div></div>

<p>A estrutura do HNSW começa com zero camadas, elas são criadas à medida em que se faz necessário. Por exemplo, se o nó foi sorteado para a camada 3 e existe apenas uma camada, as duas camadas adicionais serão criadas com esse nó:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HNSW</span><span class="p">:</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">m_max</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">m_max0</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">ef_construction</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">m_l</span><span class="p">:</span> <span class="nb">float</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Layer</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_max</span> <span class="o">=</span> <span class="n">m_max</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_max0</span> <span class="o">=</span> <span class="n">m_max0</span>
        <span class="n">self</span><span class="p">.</span><span class="n">m_l</span> <span class="o">=</span> <span class="n">m_l</span>
        <span class="n">self</span><span class="p">.</span><span class="n">ef_construction</span> <span class="o">=</span> <span class="n">ef_construction</span>
        <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">_get_level</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">floor</span><span class="p">(</span><span class="o">-</span><span class="n">np</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">uniform</span><span class="p">())</span> <span class="o">*</span> <span class="n">self</span><span class="p">.</span><span class="n">m_l</span><span class="p">))</span>

    <span class="k">def</span> <span class="nf">_create_layer</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="nc">Layer</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">insert</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]):</span>

        <span class="n">max_level</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
        <span class="n">level</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_level</span><span class="p">()</span>

        <span class="k">while</span> <span class="n">max_level</span> <span class="o">&lt;</span> <span class="n">level</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="nf">_create_layer</span><span class="p">()</span>
            <span class="n">max_level</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
        <span class="bp">...</span>
</code></pre></div></div>

<p>Caso o nó a ser incluído seja sorteado para uma camada inferior, é necessário iniciar da camada superior e  ir descendo até encontrar um nó que esteja presente na camada sorteada:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">insert</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]):</span>
    <span class="bp">...</span>
    <span class="n">entrypoint</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span>

    <span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="nf">reversed</span><span class="p">(</span><span class="nf">range</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">max_level</span><span class="p">)):</span>
        <span class="n">entrypoint</span><span class="p">,</span> <span class="o">*</span><span class="n">_</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">[</span><span class="n">level</span><span class="p">].</span><span class="nf">search</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">)</span>
    <span class="bp">...</span>

</code></pre></div></div>

<p>Ao chegar na camada sorteada, ela deva ser atualizada e todas as que estão abaixo da mesma. O processo consiste em selecionar os vizinhos aos quais esse nó será conectado, restrito a um hiperparâmetro \(m_{max}\). É criada uma conexão com esses vizinhos, caso o vizinho acabe com mais conexões que \(m_{max}\), eles são re-selecionados utilizando o mesmo método.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">insert</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]):</span>

    <span class="n">max_level</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="n">level</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_level</span><span class="p">()</span>

    <span class="k">while</span> <span class="n">max_level</span> <span class="o">&lt;</span> <span class="n">level</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">_create_layer</span><span class="p">()</span>
        <span class="n">max_level</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>

    <span class="n">entrypoint</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span>

    <span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="nf">reversed</span><span class="p">(</span><span class="nf">range</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">max_level</span><span class="p">)):</span>
        <span class="n">entrypoint</span><span class="p">,</span> <span class="o">*</span><span class="n">_</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">[</span><span class="n">level</span><span class="p">].</span><span class="nf">search</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">level</span> <span class="ow">in</span> <span class="nf">reversed</span><span class="p">(</span><span class="nf">range</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">max_level</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)):</span>

        <span class="n">layer</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">[</span><span class="n">level</span><span class="p">]</span>
        <span class="n">layer</span><span class="p">.</span><span class="nf">add_node</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>

        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">layer</span><span class="p">.</span><span class="n">nodes</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span> <span class="o">=</span> <span class="n">key</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">neighbors</span> <span class="o">=</span> <span class="n">layer</span><span class="p">.</span><span class="nf">select_neighbors_heuristic</span><span class="p">(</span>
                <span class="n">key</span><span class="p">,</span>
                <span class="n">layer</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">ef_construction</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">),</span>
                <span class="n">self</span><span class="p">.</span><span class="n">m_max</span><span class="p">,</span>
            <span class="p">)</span>

            <span class="k">for</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">neighbors</span><span class="p">:</span>

                <span class="n">layer</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">neighbor</span><span class="p">)</span>
                <span class="n">neighbor_neighbors</span> <span class="o">=</span> <span class="n">layer</span><span class="p">.</span><span class="nf">get_neighbors</span><span class="p">(</span><span class="n">neighbor</span><span class="p">)</span>

                <span class="n">m_max</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">m_max0</span> <span class="k">if</span> <span class="n">level</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="n">self</span><span class="p">.</span><span class="n">m_max</span>

                <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">neighbor_neighbors</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">m_max</span><span class="p">:</span>
                    <span class="n">layer</span><span class="p">.</span><span class="nf">set_neighbors</span><span class="p">(</span>
                        <span class="n">neighbor</span><span class="p">,</span>
                        <span class="n">layer</span><span class="p">.</span><span class="nf">select_neighbors_heuristic</span><span class="p">(</span>
                            <span class="n">neighbor</span><span class="p">,</span>
                            <span class="n">neighbor_neighbors</span><span class="p">,</span>
                            <span class="n">m_max</span><span class="p">,</span>
                        <span class="p">),</span>
                    <span class="p">)</span>
</code></pre></div></div>

<p>Nesse processo de inclusão, existem vários hiperparâmetros que impactam no resultado da estrutura:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">m_l</code>: constante que altera a probabilidade de gerar uma nova camada. As camadas são necessárias para manter a complexidade \(log(N)\), mas elas ficam ineficientes se existir muita sobreposição de nós entre as camadas.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">m_max</code> e <code class="language-plaintext highlighter-rouge">m_max0</code>: indicam a quantidade de conexões que um nó pode ter, o <code class="language-plaintext highlighter-rouge">m_max0</code> é um valor que se aplica apenas a camada mais inferior. Quanto mais conexões existir, maior a probabilidade de obter o resultado ótimo nas consultas. Por outro lado, um grafo com muitas conexões é mais demorado para navegar e ocupa mais espaço em memória.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ef_construction</code>: é um parâmetro utilizado no método <code class="language-plaintext highlighter-rouge">Layer.search</code>, que indica a quantidade de vizinhos a ser retornado pela busca. Quanto mais elementos, mais chances de obter melhores resultados na etapa de conectar os vizinhos, por outro lado demanda mais processamento.</p>
  </li>
</ul>

<p>A definição desses parâmetros dependerão da características do conjunto de dados, se tornando um problema muito similar com a escolha de hiperparâmetros para modelos de machine learning.</p>

<h2 id="escolhendo-a-vizinhança">Escolhendo a vizinhança</h2>

<p>O método mais trivial para escolher os vizinhos, é fazer uma simples busca pelos \(m\) mais próximos a partir de uma lista de candidatos:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Layer</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">y</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">sqrt</span><span class="p">(</span><span class="nf">sum</span><span class="p">([(</span><span class="n">x</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)]))</span>
    <span class="bp">...</span>
    <span class="k">def</span> <span class="nf">select_neighbors</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">candidates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">m</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>

        <span class="n">value</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>

        <span class="n">best_candidates</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span>
            <span class="p">[</span>
                <span class="p">(</span><span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">c</span><span class="p">]),</span> <span class="n">c</span><span class="p">)</span>
                <span class="k">for</span> <span class="n">v</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nf">product</span><span class="p">([</span><span class="n">value</span><span class="p">],</span> <span class="n">candidates</span><span class="p">)</span>
                <span class="k">if</span> <span class="n">c</span> <span class="o">!=</span> <span class="n">key</span>
            <span class="p">],</span>
            <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
        <span class="p">)</span>

        <span class="k">return</span> <span class="p">[</span><span class="n">c</span> <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">best_candidates</span><span class="p">][:</span><span class="n">m</span><span class="p">]</span>
</code></pre></div></div>

<p>Apesar de funcional, essa implementação ingênua pode criar vizinhanças isoladas, inacessíveis a depender do ponto de entrada da busca. Para mitigar esse cenário, os criadores do algoritmo propuseram uma seleção heurística:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Layer</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">y</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">sqrt</span><span class="p">(</span><span class="nf">sum</span><span class="p">([(</span><span class="n">x</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)]))</span>
    <span class="bp">...</span>

    <span class="k">def</span> <span class="nf">select_neighbors_heuristic</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span>
        <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
        <span class="n">candidates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
        <span class="n">m</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
        <span class="n">extend_candidates</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
        <span class="n">keep_pruned_connections</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">True</span><span class="p">,</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>

        <span class="n">value</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
        <span class="n">neighbors</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([])</span>
        <span class="n">working_candidates</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([</span><span class="n">c</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">candidates</span> <span class="k">if</span> <span class="n">c</span> <span class="o">!=</span> <span class="n">key</span><span class="p">])</span>

        <span class="k">if</span> <span class="n">extend_candidates</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">candidate</span> <span class="ow">in</span> <span class="n">candidates</span><span class="p">:</span>
                <span class="k">for</span> <span class="n">candidate_neighbor</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">edges</span><span class="p">[</span><span class="n">candidate</span><span class="p">]:</span>
                    <span class="nf">if </span><span class="p">(</span>
                        <span class="n">candidate_neighbor</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">working_candidates</span>
                        <span class="ow">and</span> <span class="n">candidate_neighbor</span> <span class="o">!=</span> <span class="n">key</span>
                    <span class="p">):</span>
                        <span class="n">working_candidates</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">candidate_neighbor</span><span class="p">)</span>

        <span class="n">discarded_candidates</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([])</span>

        <span class="k">while</span> <span class="nf">len</span><span class="p">(</span><span class="n">working_candidates</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="nf">len</span><span class="p">(</span><span class="n">neighbors</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">m</span><span class="p">:</span>

            <span class="n">nearest_wc</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_nearest</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">working_candidates</span><span class="p">)</span>
            <span class="n">working_candidates</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">nearest_wc</span><span class="p">)</span>

            <span class="n">nearest_wc_distance</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">nearest_wc</span><span class="p">],</span> <span class="n">value</span><span class="p">)</span>

            <span class="n">add_candidate</span> <span class="o">=</span> <span class="nf">bool</span><span class="p">(</span>
                <span class="nf">sum</span><span class="p">(</span>
                    <span class="p">[</span>
                        <span class="n">nearest_wc_distance</span> <span class="o">&lt;</span> <span class="n">self</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">neighbor</span><span class="p">])</span>
                        <span class="k">for</span> <span class="n">neighbor</span> <span class="ow">in</span> <span class="n">neighbors</span>
                    <span class="p">]</span>
                <span class="p">)</span>
            <span class="p">)</span>

            <span class="k">if</span> <span class="n">add_candidate</span> <span class="ow">or</span> <span class="nf">len</span><span class="p">(</span><span class="n">neighbors</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
                <span class="n">neighbors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">nearest_wc</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">discarded_candidates</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">nearest_wc</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">keep_pruned_connections</span><span class="p">:</span>
            <span class="k">while</span> <span class="nf">len</span><span class="p">(</span><span class="n">discarded_candidates</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="nf">len</span><span class="p">(</span><span class="n">neighbors</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">m</span><span class="p">:</span>
                <span class="n">nearest_candidate</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_nearest</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">discarded_candidates</span><span class="p">)</span>
                <span class="n">discarded_candidates</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">nearest_candidate</span><span class="p">)</span>
                <span class="n">neighbors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">nearest_candidate</span><span class="p">)</span>

        <span class="k">return</span> <span class="nf">list</span><span class="p">(</span><span class="n">neighbors</span><span class="p">)</span>
</code></pre></div></div>

<p>Se o parâmetro <code class="language-plaintext highlighter-rouge">extend_candidates</code> for verdadeiro, a seleção inclui os “vizinhos dos vizinhos” na busca. Com esses candidatos extras, a seleção pode criar conexões entre grupos de vetores que antes ficariam inacessíveis. Se o parâmetro <code class="language-plaintext highlighter-rouge">extend_candidates</code> for falso, <code class="language-plaintext highlighter-rouge">select_neighbors</code> e <code class="language-plaintext highlighter-rouge">select_neighbors_heuristic</code> são o mesmo algoritmo.</p>

<figure>
  <img src="/assets/images/hnsw/links.png" style="width:50%;margin-left: auto;margin-right: auto;" />
  <figcaption>Figura 2 – Conexão extra criada pela heurística</figcaption>
</figure>

<p>O parâmetro <code class="language-plaintext highlighter-rouge">keep_pruned_connections</code> é uma forma de não desprezar conexões válidas, já que a estratégia gulosa de seleção pode acabar descartando vizinhos eleígiveis no meio do caminho.</p>

<h2 id="a-busca">A busca</h2>

<p>A busca é feita a partir da camada superior com menos nós, até chegar a camada inferior que contém todos os nós do conjunto.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HNSW</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="k">def</span> <span class="nf">search</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">k</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">ef_search</span><span class="p">:</span> <span class="nb">int</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]]]:</span>

        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="p">[]</span>

        <span class="n">entrypoint</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">entrypoint</span>

        <span class="k">for</span> <span class="n">layer</span> <span class="ow">in</span> <span class="nf">reversed</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">layers</span><span class="p">):</span>
            <span class="n">keys</span> <span class="o">=</span> <span class="n">layer</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">ef_search</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">)</span>
            <span class="n">entrypoint</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span>
                <span class="p">[(</span><span class="n">key</span><span class="p">,</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">layer</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">key</span><span class="p">]))</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">keys</span><span class="p">],</span>
                <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
            <span class="p">)</span>

        <span class="n">layer</span><span class="p">,</span> <span class="o">*</span><span class="n">_</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">layers</span>
        <span class="n">keys</span> <span class="o">=</span> <span class="n">layer</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">ef_search</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">)</span>

        <span class="n">k_keys</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span>
            <span class="p">[(</span><span class="n">key</span><span class="p">,</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">layer</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">key</span><span class="p">]))</span> <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">keys</span><span class="p">],</span>
            <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="p">)[:</span><span class="n">k</span><span class="p">]</span>

        <span class="k">return</span> <span class="p">[(</span><span class="n">key</span><span class="p">,</span> <span class="n">layer</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">key</span><span class="p">])</span> <span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">k_keys</span><span class="p">]</span>
    <span class="bp">...</span>
</code></pre></div></div>

<p>O parâmetro <code class="language-plaintext highlighter-rouge">ef_search</code> indica quantos ítens devem ser considerados: quanto maior esse valor, maior a probabilidade de chegar na solução ótima. Entretanto, o processo de busca é mais caro computacionalmente. O parâmetro <code class="language-plaintext highlighter-rouge">k</code> é a quantidade de ítens que devem ser retornados.</p>

<p>A busca em uma camada é um processo guloso, que procura os vizinhos mais próximos a partir de um ponto de entrada. Para evitar ciclos, há uma lista de nós visitados:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Layer</span><span class="p">:</span>
    <span class="bp">...</span>
    <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">y</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">sqrt</span><span class="p">(</span><span class="nf">sum</span><span class="p">([(</span><span class="n">x</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)]))</span>

    <span class="k">def</span> <span class="nf">_get_nearest</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">nodes</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>

        <span class="n">nearest</span> <span class="o">=</span> <span class="p">(</span><span class="sh">""</span><span class="p">,</span> <span class="n">sys</span><span class="p">.</span><span class="n">float_info</span><span class="p">.</span><span class="nb">max</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">nodes</span><span class="p">:</span>

            <span class="n">node_distance</span> <span class="o">=</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">node</span><span class="p">])</span>
            <span class="n">_</span><span class="p">,</span> <span class="n">smaller_distance</span> <span class="o">=</span> <span class="n">nearest</span>

            <span class="k">if</span> <span class="n">node_distance</span> <span class="o">&lt;</span> <span class="n">smaller_distance</span><span class="p">:</span>
                <span class="n">nearest</span> <span class="o">=</span> <span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">node_distance</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">nearest</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>

    <span class="k">def</span> <span class="nf">_get_furthest</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">nodes</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>

        <span class="n">furthest</span> <span class="o">=</span> <span class="p">(</span><span class="sh">""</span><span class="p">,</span> <span class="o">-</span><span class="n">sys</span><span class="p">.</span><span class="n">float_info</span><span class="p">.</span><span class="nb">max</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">nodes</span><span class="p">:</span>

            <span class="n">node_distance</span> <span class="o">=</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">node</span><span class="p">])</span>
            <span class="n">_</span><span class="p">,</span> <span class="n">bigger_distance</span> <span class="o">=</span> <span class="n">furthest</span>

            <span class="k">if</span> <span class="n">node_distance</span> <span class="o">&gt;</span> <span class="n">bigger_distance</span><span class="p">:</span>
                <span class="n">furthest</span> <span class="o">=</span> <span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">node_distance</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">furthest</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="bp">...</span>
    <span class="k">def</span> <span class="nf">search</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">elements_to_return</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">entrypoint</span><span class="p">:</span> <span class="nb">str</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>

        <span class="n">visted</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([</span><span class="n">entrypoint</span><span class="p">])</span>
        <span class="n">candidates</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([</span><span class="n">entrypoint</span><span class="p">])</span>
        <span class="n">nearest_neighbors</span> <span class="o">=</span> <span class="nf">set</span><span class="p">([</span><span class="n">entrypoint</span><span class="p">])</span>

        <span class="k">while</span> <span class="nf">len</span><span class="p">(</span><span class="n">candidates</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>

            <span class="n">nearest_node</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_nearest</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">candidates</span><span class="p">)</span>
            <span class="n">candidates</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">nearest_node</span><span class="p">)</span>

            <span class="n">furthest_node</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_furthest</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">nearest_neighbors</span><span class="p">)</span>

            <span class="k">if</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">nearest_node</span><span class="p">])</span> <span class="o">&gt;</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span>
                <span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">furthest_node</span><span class="p">]</span>
            <span class="p">):</span>
                <span class="k">break</span>

            <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">edges</span><span class="p">[</span><span class="n">nearest_node</span><span class="p">]:</span>

                <span class="k">if</span> <span class="n">node</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">visted</span><span class="p">:</span>

                    <span class="n">visted</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
                    <span class="n">furthest_element</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_furthest</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">nearest_neighbors</span><span class="p">)</span>

                    <span class="nf">if </span><span class="p">(</span>
                        <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">node</span><span class="p">])</span>
                        <span class="o">&lt;</span> <span class="n">Layer</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">furthest_element</span><span class="p">])</span>
                        <span class="ow">or</span> <span class="nf">len</span><span class="p">(</span><span class="n">nearest_neighbors</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">elements_to_return</span>
                    <span class="p">):</span>

                        <span class="n">candidates</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
                        <span class="n">nearest_neighbors</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>

                        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">nearest_neighbors</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">elements_to_return</span><span class="p">:</span>
                            <span class="n">nearest_neighbors</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">furthest_element</span><span class="p">)</span>

        <span class="k">return</span> <span class="nf">list</span><span class="p">(</span><span class="n">nearest_neighbors</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="e-como-escolher-os-parâmetros">E como escolher os parâmetros?</h2>

<p>Algo que eu sempre digo: se existisse uma forma trivial de escolher os hiperparâmetros, eles seriam constantes e não parâmetros ou escolhidos automaticamente. Eu pensei em explorar esse aspecto nesse post, mas a conclusão é que não devo utilizar essa estrutura para o problema que estou tratando hoje: fazer busca vetorial em pequenos subconjuntos. É mais eficiente filtrar o conjunto com um índice tradicional, para depois ordenar sem uma estrutura adicional como o HNSW.</p>

<p>Eu só pude chegar a essa conclusão, porque entendi como funciona uma busca vetorial. Caso eu precise trabalhar com <em>tunning</em> de um RAG em milhões de registros, já sei quais parâmetros eu devo olhar a depender do problema que estou enfrentando (<em>e.g.</em> tempo de construção, tempo de busca, consumo de memória) e das características dos meus dados. Assim como ajustar os parâmetros de uma <em>random forest</em> por exemplo, é importante realmente entender a dinâmica dos parâmetros do HNSW, para não perder tempo com uma tentiva e erro sem direção.</p>

<p>Eu <a href="https://github.com/gdarruda/hnsw-python">implementei o algoritmo</a> apenas para fins didáticos, nem tenho certeza de sua corretude, mas é um jeito que me ajuda a realmente entender os problemas. Talvez, eu retome esse assunto de como escolher os parâmetros, mas é um assunto que merece outro post.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Explicação do algoritmo HNSW utilizado para busca vetorial]]></summary></entry><entry><title type="html">Aprendendo com cientistas de dados</title><link href="/2025/05/24/aprendendo-com-cientistas-dados.html" rel="alternate" type="text/html" title="Aprendendo com cientistas de dados" /><published>2025-05-24T00:00:00+00:00</published><updated>2025-05-24T00:00:00+00:00</updated><id>/2025/05/24/aprendendo-com-cientistas-dados</id><content type="html" xml:base="/2025/05/24/aprendendo-com-cientistas-dados.html"><![CDATA[<p>Eu tenho o cargo de cientista de dados há vários anos, mas durante esse período, trabalhei muito pouco com modelos e análises descritivas. Gostos dessas atividades, mas sou medíocre, meus colegas conseguem fazer esse tipo de trabalho tão bem ou melhor que eu. Por outro lado, tenho bastante experiência como desenvolvedor, então foco meus esforços em colocar os modelos desenvolvidos por outros cientistas para rodar.</p>

<p>Nessa posição, trabalho muito em conjunto com os cientistas de dados, mas atuando como programador. Qualquer desenvolvedor, que precisou colocar em produção – um modelo implementado em um único arquivo chamado <em>Untitled (7) Copy.ipynb</em> – sabe que alguns cientistas precisam aprender muito sobre as práticas de programação. Por outro lado, trabalhar com cientistas me ensinou a ver os desafios de programação com outros olhos.</p>

<h1 id="saber-somente-a-api-não-é-saber">Saber (somente) a API, não é saber</h1>

<p>Um dos meus pontos fracos como cientista de dados, é não ter muita profundidade teórica. Eu tenho a intuição de como um <a href="https://en.wikipedia.org/wiki/Support_vector_machine">SVM</a> funciona, mas não saberia implementar a otimização e nem entendo as partes mais sofisticadas como o <a href="https://en.wikipedia.org/wiki/Kernel_method#Mathematics:_the_kernel_trick">kernel trick</a>. Apesar de ser um modelo complexo de entender, é muito simples utilizá-lo no <a href="https://scikit-learn.org/stable/modules/svm.html">scikit-learn</a>, mais trivial impossível:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sklearn</span> <span class="kn">import</span> <span class="n">svm</span>
<span class="n">X</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">]]</span>
<span class="n">y</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span>
<span class="n">clf</span> <span class="o">=</span> <span class="n">svm</span><span class="p">.</span><span class="nc">SVC</span><span class="p">()</span>
<span class="n">clf</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</code></pre></div></div>

<p>Não posso dizer que sei usar o SVM por conseguir treiná-lo usando uma biblioteca. Já como programador, é normal usar bibliotecas e abstrair completamente a implementação, conquanto que tenha o comportamento esperado. Essa se tornou a abordagem padrão, tanto que o <a href="https://www.wisdomandwonder.com/link/2110/why-mit-switched-from-scheme-to-python">MIT trocou Scheme por Python</a> em seu curso introdutório, para refletir esse cenário:</p>

<blockquote>
  <p>In 1980, good programmers spent a lot of time thinking, and then produced spare code that they thought should work. Code ran close to the metal, even Scheme — it was understandable all the way down. […] But programming now isn’t so much like that, said Sussman. Nowadays you muck around with incomprehensible or nonexistent man pages for software you don’t know who wrote. You have to do basic science on your libraries to see how they work, trying out different inputs and seeing how the code reacts. This is a fundamentally different job, and it needed a different course.</p>
</blockquote>

<p>Essa estratégia tende a funcionar bem, mas algumas vezes faz-se necessário entender a implementação, para fazer um <em>troubleshoot</em> ou resolver problemas de desempenho. Eu discuti nesse <a href="/2023/03/04/engenharia-dados.html">post</a>, como esse problema é recorrente na engenharia de dados: os tutoriais são super amigáveis e tudo funciona bem no começo, mas as coisas podem se complicar muito rapidamente.</p>

<p>Eu brinco que a curva de aprendizado de Apache Spark é suave, especialmente se o uso se limitar a queries SQL, mas de repente vira algo super avançado quando é necessário lidar com problemas de serialização e erros internos em Scala.</p>

<figure>
  <img src="/assets/images/cientista-programadores/grafico-spark.svg" style="display: block;margin-left:auto;margin-right: auto;" />
</figure>

<p>Não estou sugerindo que se tenha conhecimento profundo sobre todas as ferramentas e frameworks, é simplesmente impossível e os ganhos práticos seguem a <a href="https://pt.wikipedia.org/wiki/Lei_dos_rendimentos_decrescentes">lei dos rendimentos decrescentes</a>. Minha sugestão é seguir o conselho do Martin Kleppman, autor do livro “Design Data Intensive Applications”, de se preocupar em ter uma noção de como as coisas estão funcionando por trás das abstrações:</p>

<blockquote>
  <p>Learn just enough abot the internals of the tools you are using, so that you have a reasonable mental model of what’s going on there. You don’t need to be able to, like, modify of the Kafka yourself, but I think having just enough of an idea of the internals that […] if the the performance goes bad, you have a way of visualizing in your head what’s going on. […]. It’s incredibly valuable to just have a bit of a mental model and not just treat it as a black box.</p>
</blockquote>

<p>Para isso, eu gosto de assistir apresentações <a href="https://www.youtube.com/watch?v=dmL0N3qfSc8">como essa</a>, normalmente descritas como <em>deep dive</em> ou <em>internals</em>. Infelizmente, é um pouco difícil achar conteúdo que esteja nesse meio do caminho entre teoria pura e apenas a aplicação prática.</p>

<p>Esse tipo de conhecimento, além de ser mais perene, facilita transitar entre diferentes implementações do mesmo conceito. Por exemplo, basta ler a documentação do <a href="https://duckdb.org/why_duckdb">DuckDB</a> para ter uma noção de como ele pode te ajudar a substituir rotinas Spark, mas dificilmente será útil em processos transacionais.</p>

<p>Esse último ponto emenda com outro tópico que eu queria discutir, repensar o valor que damos ao conhecimento especializado em ferramentas. É relevante para ter velocidade no desenvolvimento e maior confiança, mas vejo que as vezes ficamos muito presos ao que já conhecemos.</p>

<h1 id="ferramentas-importam-mas-não-tanto">Ferramentas importam, mas não tanto</h1>

<p>Quando se aprende a programar, é normal o discurso de que o importante é aprender lógica, a linguagem é uma questão secundária. Mas isso é verdade até os primeiros meses, depois escolher a “sua” linguagem vira algo extremamente importante na carreira do programador.</p>

<p>O mercado é avesso a migração de linguagens, é muito difícil trocar de “stack” tecnológica. Mesmo com 15 anos de experiência, seria difícil eu conseguir uma vaga de senior para uma linguagem que eu não tenha experiência prévia, especialmente se tiver uma vasta oferta de profissionais experientes como C# ou PHP por exemplo.</p>

<p>Os cientistas de dados têm especialidades também – pessoas que trabalham mais com dados não-estruturados, outras focadas em métodos estatísticos, ou especialistas em otimização – mas sinto que as barreiras são menores para migrar entre especialidades. É mais importante que você tenha mostrado capacidade de se aprofundadar em algo, do que ter experiência prévia em um domínio específico.</p>

<p>Meu mestrado é completamente irrelevante como pesquisa hoje em dia, mas ainda é importante como experiência. Não importa tanto o assunto da sua pesquisa, mas a premissa de que você conseguiu se aprofundar em um tópico relacionado, então pode fazer o mesmo em outro domínio próximo. Afinal, é uma área em que as inovações acadêmicas chegam muito rapidamente ao mercado, acompanhar essa evolução é primordial.</p>

<p>Apesar do mercado não incentivar, tento aproveitar as oportunidades que tenho para trabalhar com outras linguagens e tecnologias. É normal não ter a mesma velocidade no começo, mas concordo com essa <a href="https://www.norvig.com/21-days.html">sugestão do Norvig</a>, que é importante aprender várias linguagens com propostas diferentes:</p>

<blockquote>
  <p>Learn at least a half dozen programming languages. Include one language that emphasizes class abstractions (like Java or C++), one that emphasizes functional abstraction (like Lisp or ML or Haskell), one that supports syntactic abstraction (like Lisp), one that supports declarative specifications (like Prolog or C++ templates), and one that emphasizes parallelism (like Clojure or Go).</p>
</blockquote>

<p>Recentemente, eu precisei desenvolver uma aplicação C#, mas nunca tinha mexido em nada do ecossistema .NET. Após algumas semanas, eu já estava conseguindo ser produtivo, existem mais similaridades que diferenças entre linguagens como Java e C#. A infinidade de recursos que existe para aprendizado hoje em dia – IDEs/LSPs, IAs, documentação, Stack Overflow – facilitam muito essa migração para alguém com experiência prévia em outra linguagem.</p>

<p>Eu tenho minhas preferências, mas se o projeto é uma aplicação web tradicional, a linguagem dificilmente é uma questão. Normalmente, são outras decisões de arquitetura que viram um problema, como um banco de dados inadequado ou separação incorreta de serviços. Um argumento plausível – mas incompreendido e mal utilizado – em que a linguagem escolhida importa, são problemas de escalabilidade.</p>

<h1 id="seja-criterioso-com-métricas">Seja criterioso com métricas</h1>

<p>Um conhecimento muito cobrado dos cientistas de dados, é o domínio sobre métricas de avaliação. Saber escolher a mais adequada para o problema, como interpretá-las e suas limitações. O mesmo deveria ser cobrado de programadores e arquitetos, quando usam a <em>performance</em> como argumento de suas escolhas.</p>

<p>O erro mais comum que eu vejo ser cometido, é não entender a relevância da métrica. Por exemplo, se formos considerar uma taxa de falso positivo. Em um julgamento, condenar alguém inocente a pena de morte é um erro irreversível. Uma compra classificada erroneamente como fraude, é um pequeno transtorno em comparação. O problema e o contexto que dão a relevância de uma métrica.</p>

<p>O relatório das <a href="https://greenlab.di.uminho.pt/wp-content/uploads/2017/10/sleFinal.pdf">linguagens mais sustentáveis</a> apareceu várias vezes no meu LinkedIn, destacando a ineficiência do Python: é tão lenta, que consome 70x mais energia para fazer o mesmo trabalho que C. É um resultado válido pelos experimentos feitos, mas que não é relevante para os cenário de uso mais comuns da linguagem.</p>

<p>Os testes foram feitos com tarefas chamadas <em>CPU bound</em>, como cálculo de autovalor e manipulação de árvores binárias. Para cenários de computação pesada, a “regra” é usar bibliotecas <a href="/2021/01/12/para-se-preocupar-ame-numpy.html">como o numpy</a>, implementadas em outras linguagens mais rápidas. Para desenvolvimento de aplicações web, normalmente estamos falando de um cenário <em>IO Bound</em>, com muito mais tempo gasto com rede e armazenamento.</p>

<p>Mesmo quando uma métrica é relevante, ela pode ser muito limitada para representar todas as nuances do problema. Por exemplo, eu fiz essa <a href="/2023/04/16/grpc-rest.html">comparação de performance entre gRPC e REST</a>, utilizando diferentes linguagens de programação. Pelo primeiro experimento, considerando o tempo médio de resposta, poderia dizer que Python com gRPC é a melhor alternativa.</p>

<figure>
  <img src="/assets/images/grpc-rest/boxplot_outliers.svg" />
</figure>

<p>A média é uma métrica muito sensível a outliers, o que a torna pouco adequada para comparar resultados que envolvem comunicação por rede. É normal terem muitos outiliers em um experimento como esse, porque os protocolos de rede são otimizados para aumentar <em>throughput</em> em detrimento à constância.</p>

<p>Mudando o cenário do experimento, para requisições concorrentes, os resultados de Go com gRPC são melhores que Pyhton com gRPC:</p>

<figure>
  <img src="/assets/images/grpc-rest/histogram_batch.svg" />
</figure>

<p>Esses experimentos e resultados são mais relevantes para avaliar aplicações web, Python é uma alternativa competitiva e não 70x mais lento. Entretanto, pela natureza do problema, não é recomendado extrapolar esses resultados para outros cenários:</p>

<ul>
  <li>
    <p>os testes foram feitos em uma rede Wi-Fi, completamente diferente de uma comunicação entre servidores em um datacenter por exemplo;</p>
  </li>
  <li>
    <p>se eu tivesse resumido os resultados na média – eu poderia concluir que Python com gRPC é a solução mais rápida – o que seria uma conclusão válida e rasa ao mesmo tempo;</p>
  </li>
  <li>
    <p>o tempo é dominado por I/O, ter repostas em tempo parecido não significa que teremos a mesma escalabilidade, precisaríamos focar no consumo de recursos para comparar as linguagens nesse aspecto.</p>
  </li>
</ul>

<p>Eu deliberadamente não calculei as métricas, porque é natural que as pessoas simplesmente comparem os números e ignore as nuances. Números sempre trazem uma maior credibilidade e sensação de confiança, mesmo que não seja o caso .</p>

<p>Pior que lidar com as métricas de performance de software, é lidar com as métricas de produtividade e qualidade no desenvolvimento: quantidade de deploys, contagem de bugs, cobertura de testes, linhas de código, commits, “tamanho” de histórias, etc…</p>

<p>Eu compadeço da dor que os gestores sentem, fazer software é caro e caótico. Essas são as métricas possíveis de medir, mesmo que sejam pouco relevantes e pouco representativas. Isso é um reflexo de como a engenharia de software sofre de <a href="https://en.wikipedia.org/wiki/Physics_envy">inveja da física</a>, aceitar nossas limitações pode ajudar a reduzir as frustrações no processo de desenvolvimento de software.</p>

<h1 id="navalha-de-ockham">Navalha de Ockham</h1>

<p>Eu <a href="/2024/06/26/codigo-limpo-positivismo-fotografia.html">escrevi sobre</a> o livro Código Limpo, discutindo como a visão positivista sobre o tema, fez dele um sucesso no lançamento e alvo de críticas recentes. As ideias propostas são muito boas, mas o tom prescritivo e científico do texto, não está realmente embasado com dados e rigor metodológico.</p>

<p>Como enxergo a engenharia de software mais próximo às ciências humanas que exatas, optei por tratar a engenharia de software com outra ferramenta do pensamento científico, a <a href="https://pt.wikipedia.org/wiki/Navalha_de_Ockham">navalha de Ockham</a>: em igualdade de condições, a explicação mais simples é geralmente a mais provável.</p>

<p>A ideia da navalha de Ockam é para comparar hipóteses científicas, a mais simples é melhor que a mais complexa. Os cientistas de dados tratam modelos por essa mesma heurística: modelos complexos são mais suscetíveis a problemas e via de regra são menos interpretáveis, o mais simples possível é a melhor opção.</p>

<p>No contexto de desenvolvimento, eu uso essa abordagem para definir arquitetura e aplicar metodologias. Infelizmente, é muito díficil definir o que é “funcionar” nesses contextos, mas procuro pensar sempre na alternativa mais minimalista e expandir quando sentir necessidade.</p>

<p>É normal arquitetura seguir pelo caminho inverso: ser desenhada para ser o mais “future proof” possível e acabar com várias soluções para problemas que nunca existirão. Um exemplo clássico desse cenário, são startups iniciando com arquitetura de microsserviços, perdendo velocidade para resolver um problema que só existe em grandes empresas.</p>

<p>Não existe uma regra para definir o quanto de “future proof” devemos ser, mas é possível iniciar a partir do mínimo para funcionar e adicionar complexidade após uma análise de custo/beneício: talvez eu não precise começar o projeto com Kubernetes, mas criar imagens Docker é algo simples que pode facilitar no futuro e traz benefícios imediatos.</p>

<p>As metodologias de trabalho seguem uma tendência parecida, em que as pessoas tentam emular os rituais de empresas bem sucedidas, na mesma lógica de seguir rotinas matinais de personalidades. É muito tentador creditar uma série de processos e regras como a razão do sucesso, porque basta aplicar a fórmula do sucesso para avançar.</p>

<p>Seguindo as ideias do <a href="https://agilemanifesto.org">manifesto ágil</a>, sou a favor de implementar o mínimo de cerimônias e processos. Falando de scrum, começo com três cerimônias: refinamento, planning (sem poker) e daily. Para times menores e mais experientes, normalmente funciona muito bem. Outras cerimônias como demo e retrospectiva, aplico quando o time sente que faz sentido.</p>

<p>Seguir essa premissa “minimalista” não é a prova de falhas, não sei nem dizer se realmente é melhor. Mas entre pecar pela falta ou pelo excesso, minha impressão é que o excesso é o mais comum, porque existe toda uma dinâmicas de incentivos para vender e consumir soluções e metodologias complexas. Afinal, <a href="https://www.forbes.com/sites/duenablomstrom1/2018/11/30/nobody-gets-fired-for-buying-ibm-but-they-should/">ninguém é demitido por comprar IBM</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Discussão sobre como tratar problema]]></summary></entry><entry><title type="html">Criando um índice em Rust</title><link href="/2025/03/05/indice-rust.html" rel="alternate" type="text/html" title="Criando um índice em Rust" /><published>2025-03-05T00:00:00+00:00</published><updated>2025-03-05T00:00:00+00:00</updated><id>/2025/03/05/indice-rust</id><content type="html" xml:base="/2025/03/05/indice-rust.html"><![CDATA[<p>O meu livro técnico favorito é o <a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/">Designing Data-Intensive Applications</a> do Kleppmann. Apesar de ser relativamente antigo, como ele trata mais do aspecto teórico que prático, continua sendo uma ótima leitura para engenheiros de dados e profissionais que precisam lidar com sistemas de larga escala.</p>

<p>Em uma entrevista recente com o autor – quando perguntado sobre dicas para um aspirante a engenheiros de dados – concordei plenamente com <a href="https://youtu.be/P-9FwZxO1zE?si=42wwf1Pan7BG5bM2&amp;t=1529">esse conselho</a>:</p>

<blockquote>
  <p>Learn just enough abot the internals of the tools you are using, so that you have a reasonable mental model of what’s going on there. You don’t need to be able to, like, modify of the Kafka yourself, but I think having just enough of an idea of the internals that […] if the the performance goes bad, you have a way of visualizing in your head what’s going on. […]. It’s incredibly valuable to just have a bit of a mental model and not just treat it as a black box.</p>
</blockquote>

<p>Uma das primeiras vezes, que entendi o valor de construir esses modelo mentais, foi quando aprendi sobre índices de banco de dados. Passei a entender melhor o <a href="https://en.wikipedia.org/wiki/Query_plan">plano de execução</a>, quando a criação de um índice fazia sentido e otimizei consultas que outras pessoas não estavam conseguindo.</p>

<p>A solução mais comum para índices são as <a href="https://www.youtube.com/shorts/Ah_LMYqd2CE">árvores B</a>, esse tipo de estrutura é utilizada por diversos tipos de banco de dados: desde o SQLite, passando pelo Oracle Database e até mesmo em soluções distribuídas como o Dynamo DB.</p>

<p>Sendo uma estrutura tão prevalente em soluções de dados, acho importante que todo engenheiro de dados tenha uma noção de como ela funciona, construir esse modelo mental que Kleppmann comentou. Como eu queria brincar um pouco com a linguagem Rust, achei que era um bom exercício implementar uma árvore B na linguagem e escrever um pouco sobre.</p>

<h2 id="o-que-é-uma-árvore-b">O que é uma árvore B?</h2>

<p>As árvores B são uma generalização das árvores binárias: ao invés de ter uma chave por nó, são \(k\) chaves por nós. Ela foi criada na década de 70 para lidar com dados persistentes, em um época em que a memória era escassa e a forma mais comum de armazenamento durável era o disco rígido. Atualmente, temos fartura de memória e armazenamento em SSD, novas estruturas como as <a href="https://en.wikipedia.org/wiki/Log-structured_merge-tree">LSM Trees</a> foram desenvolvidas pensando nessa nova realidade.</p>

<p>Em soluções focadas em performance e escalabilidade, como o <a href="https://cassandra.apache.org/_/case-studies.html">Cassandra</a> por exemplo, faz muito sentido usar estruturas como as LSM Tree. Entretanto, algo que eu aprendi após anos trabalhando com “Big Data”: soluções com uso intensivo de memória têm performance excepcional, mas podem ficar inutilizáveis em cenários de escassez da mesma.</p>

<p>Apesar de ser uma estrutura com mais de 50 anos, desenvolvida em um cenário diferente do atual, segue sendo popular em novas soluções de dados por vários motivos:</p>

<ul>
  <li>as ávores B não geram pressão em memória;</li>
  <li>são flexíveis como uma árvore binária (<em>e.g.</em> possibilidade de chaves parcias; suporte a múltiplos operadores  de busca(\(&gt;\), \(&lt;\) e \(=\));</li>
  <li>dado é armazenado de forma ordenada;</li>
  <li>desempenho suficiente para muitos casos de uso.</li>
</ul>

<p>Não faz muito sentido eu fazer (mais) uma explicação detalhada de como elas funcionam, porque existem infinitos materias sobre o assunto nos mais diversos formatos: <a href="https://www.youtube.com/watch?v=5mC6TmviBPE">aulas online</a>, <a href="https://www.youtube.com/watch?v=9GdesxWtOgs&amp;t=1218s">vídeos do Akita</a>, <a href="https://planetscale.com/blog/btrees-and-database-indexes">blog posts</a> e <a href="https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/">livros de algoritmos</a>. É mais importante que o leitor procure esses materiais para entender sobre a estrutura, do que se preocupar em ler o restante desse post: eu fiz para meu próprio entretenimento, não como algo útil ou didático necessariamente.</p>

<p>Para fazer a implementação em Rust, revi o assunto no famoso <a href="https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/">“Cormen”</a> e fiz praticamente uma cópia do proposto no livro. Implementei apenas a parte de inserção e busca, usando a estrutura para indexar arquivos no formato csv.</p>

<h2 id="a-estrutura-da-estrutura">A estrutura da estrutura</h2>

<p>Ao implementar uma estrutura de dados, normalmente começo imaginando as sub-estruturas que vou precisar e só depois penso na manipulação. Para essa implementação da árvore B, utilizei três sub-estruturas: <code class="language-plaintext highlighter-rouge">BTree</code>, <code class="language-plaintext highlighter-rouge">Node</code> e <code class="language-plaintext highlighter-rouge">Key</code>.</p>

<h3 id="btree">BTree</h3>

<p>A <code class="language-plaintext highlighter-rouge">BTree</code> é a estrutura pública, que será utilizada pelas outras aplicações, contendo três campos:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">root</code> é o nó raíz por onde serão iniciadas as buscas;</li>
  <li><code class="language-plaintext highlighter-rouge">order</code> indica a quantidade de nós máximos que uma árvore pode conter;</li>
  <li><code class="language-plaintext highlighter-rouge">path</code> é o diretório em que os arquivos da árvore serão armazenados.</li>
</ul>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">BTree</span> <span class="p">{</span>
    <span class="n">root</span><span class="p">:</span> <span class="n">Node</span><span class="p">,</span>
    <span class="n">order</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span>
    <span class="n">path</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="node">Node</h3>

<p>O <code class="language-plaintext highlighter-rouge">Node</code> é a estrutura mais importante, que armazena e organiza as chaves de forma que as buscas possam ser feita em \(log(N)\). É composta pelos seguintes campos:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">key</code> é um vetor contendo as chaves ordenadas;</li>
  <li><code class="language-plaintext highlighter-rouge">children</code> é um vetor contendo os filhos de cada chave;</li>
  <li><code class="language-plaintext highlighter-rouge">leaf</code> indica se o nó é uma folha, informação importante para tratar casos especiais de inclusão;</li>
  <li><code class="language-plaintext highlighter-rouge">filename</code> é o nome do arquivo em que esse nó é armazenado.</li>
</ul>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">Node</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">keys</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Key</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">children</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">leaf</span><span class="p">:</span> <span class="nb">bool</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">filename</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="key">Key</h3>

<p>A estrutura <code class="language-plaintext highlighter-rouge">Key</code> é a mais simples, contendo apenas dois campos:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">value</code> é o valor da chave propriamente dito, optei por deixar como <code class="language-plaintext highlighter-rouge">String</code> por simplicidade de implementação. Em uma solução produtiva, seria interessante ter algo mais flexível como um vetor de bytes ou um campo com tipo genérico.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">position</code> é uma tupla com a posição da chave dentro do arquivo csv, o primeiro campo é o <em>offset</em> no arquivo e a segunda é a quantidade de bytes para ser lido a partir do <em>offset</em>.</p>
  </li>
</ul>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">Key</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">value</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">position</span><span class="p">:</span> <span class="p">(</span><span class="nb">u64</span><span class="p">,</span> <span class="nb">u64</span><span class="p">),</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="criando-a-árvore">Criando a árvore</h2>

<p>A busca em árvore costuma ser algo simples, a complexidade é maior para criação e manutenção da mesma. Optei por fazer apenas a inserção, que é o mínimo necessário para criar uma árvore funcional.</p>

<p>A inclusão na árvore é feito pela função <code class="language-plaintext highlighter-rouge">insert</code>, associada ao objeto <code class="language-plaintext highlighter-rouge">Btree</code>. A lógica de inclusão é feita pelo objeto <code class="language-plaintext highlighter-rouge">Node</code>, mas antes existe um desvio para o cenário em que o nó raiz está cheio.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">is_full</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">order</span><span class="p">:</span> <span class="nb">usize</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">bool</span> <span class="p">{</span>
    <span class="k">self</span><span class="py">.keys</span><span class="nf">.len</span><span class="p">()</span> <span class="o">==</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">order</span> <span class="o">-</span> <span class="mi">1</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">insert</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="n">Key</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="k">self</span><span class="py">.root</span><span class="nf">.is_full</span><span class="p">(</span><span class="k">self</span><span class="py">.order</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">new_root</span> <span class="o">=</span> <span class="nn">Node</span><span class="p">::</span><span class="nf">empty</span><span class="p">(</span><span class="k">self</span><span class="py">.order</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.path</span><span class="p">);</span>
        <span class="n">new_root</span><span class="py">.children</span><span class="nf">.push</span><span class="p">(</span><span class="k">self</span><span class="py">.root</span><span class="nf">.clone</span><span class="p">()</span><span class="py">.filename</span><span class="p">);</span>
        <span class="n">new_root</span><span class="nf">.split</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="k">self</span><span class="py">.order</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.path</span><span class="p">);</span>
        <span class="k">self</span><span class="py">.root</span> <span class="o">=</span> <span class="n">new_root</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">self</span><span class="py">.root</span><span class="nf">.insert</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="k">self</span><span class="py">.order</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">self</span><span class="py">.path</span><span class="p">);</span>
    <span class="k">self</span><span class="nf">.save</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Se o nó da raiz estiver cheio, um nó vazio é criado e a raiz vira filho desse novo nó. Após tratar esse caso específico, a inserção do registro é feito pela função <code class="language-plaintext highlighter-rouge">insert</code> na estrutura <code class="language-plaintext highlighter-rouge">Node</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">find_position</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Key</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">usize</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">idx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">iter_key</span><span class="p">)</span> <span class="k">in</span> <span class="k">self</span><span class="py">.keys</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.enumerate</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">idx</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
        <span class="k">if</span> <span class="n">iter_key</span><span class="py">.value</span> <span class="o">&gt;</span> <span class="n">key</span><span class="py">.value</span> <span class="p">{</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="k">self</span><span class="py">.keys</span><span class="nf">.len</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">key</span><span class="py">.value</span> <span class="o">&gt;</span> <span class="k">self</span><span class="py">.keys</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span><span class="py">.value</span> <span class="p">{</span>
            <span class="n">idx</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">idx</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">add_key</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">idx</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="n">Key</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="k">self</span><span class="py">.keys</span><span class="nf">.len</span><span class="p">()</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.keys</span><span class="nf">.push</span><span class="p">(</span><span class="n">key</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">self</span><span class="py">.keys</span><span class="nf">.insert</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span> <span class="n">key</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">self</span><span class="nf">.save</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">insert</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="n">Key</span><span class="p">,</span> <span class="n">order</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="k">self</span><span class="py">.leaf</span> <span class="p">{</span>
        <span class="k">self</span><span class="nf">.add_key</span><span class="p">(</span><span class="k">self</span><span class="nf">.find_position</span><span class="p">(</span><span class="o">&amp;</span><span class="n">key</span><span class="p">),</span> <span class="n">key</span><span class="nf">.clone</span><span class="p">());</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">idx</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.find_position</span><span class="p">(</span><span class="o">&amp;</span><span class="n">key</span><span class="p">);</span>

        <span class="k">if</span> <span class="nn">Node</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.children</span><span class="p">[</span><span class="n">idx</span><span class="p">])</span><span class="nf">.is_full</span><span class="p">(</span><span class="n">order</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="nf">.split</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">path</span><span class="p">);</span>
            <span class="n">idx</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.find_position</span><span class="p">(</span><span class="o">&amp;</span><span class="n">key</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="nn">Node</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.children</span><span class="p">[</span><span class="n">idx</span><span class="p">])</span><span class="nf">.insert</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">path</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A função <code class="language-plaintext highlighter-rouge">add_key</code> assume que o nó tem espaço e não precisa ser separado em múltiplos. Se for um nó folha, essa função é chamada diretamente na posição definida por <code class="language-plaintext highlighter-rouge">find_position</code>. Caso contrário, o nó filho em que a chave será inserida é procurada pela mesma função <code class="language-plaintext highlighter-rouge">find_position</code>, esse nó filho é separado em dois caso esteja cheio.</p>

<p>Para separar um nó, a chave central do nó é usada como referência. No caso abaixo, a chave <code class="language-plaintext highlighter-rouge">S</code> vai para o nó superior, os filhos à esquerda permanecem no nó original e os filhos à direita são movidos para um novo nó.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 497 186" width="700" height="200"><!-- svg-source:excalidraw --><metadata></metadata><defs><style class="style-fonts">
      @font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAlQAA4AAAAAD2gAAAj8AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgiocNAZgAEwRCAqTTI5SCxoAATYCJAMwBCAFgxgHIBvhC1FUj0JkPw7KjWfaSJIzipsXKGMbS3LSz/Pb/HNfEGWCOaUFAyOCnOLXJ84cGDNzGbqozA8PH6e+/J/Uui2AHULLe8W5hXte1Bp00C3jfjzWCAcVle1F19an/k8LrLAmBDlxKm7vgfj/3Kual63TxxYbEyRAEXk/+/6xLBtzYmMWx8JyC1QtFksbpNoNGAGtcC1iV8Re9MQnUwAB4BAomJJKwLGhrwJgCZCAmKYZssH11NlUA663prJqcH2bW+rAFQEA3h5Lr7KmOmAs4gFAqWuBBsMAI5ZKwVdHQws6GrA+BrC+BarEOhcm1pvWjXzG28vW3Xsv4RuAxhPGPtFbb7cgiYDqe6MARuShPbQ594N4WPUtDYBcQ0tJuKChzRQ4S4EgQbk8CJpnqpHPRDaaCKQdjC4sF2R9fBu0hSC/UTnyYMpSnQ4HQciUbf93AWktoTYhwIJB0mcZTXgFgPtwVCUUewgRYPUQtiykks60L43cYryD9ZH1qnWbdet2BiBgHOBOkZ1TA5/4yk+8QIACIywN/X6vMFIUHlyfmZEd0/gFamlTuTqDV8YfXKhLoTuyOGmq6W5mYJaJbdzQWIEVUGaY0SWZTnNhKqykOQgx/R0gnp6zIzI3sFHQCs3BlaJzoBNQw92THGy2FJvsZW8deZFKVp0scJyUwllTqeOYMGWEVpgqo2XNu5AtPm2kcokdUiFFXeqA1ZFD3rA7dCslgDiuku2khDjgdD4QzSxgQYCZgezlchntR01mmQCSBIQn5O53puA+x/un2vH48rOAs7rk9vEzWo6q06L9v4lV73w7kC1ZSIXZDld0dw2GmbpLwPduPCdLgiDYrP92WvPbg1Z6BRnz8c5UpNuPmwriOoBwMDG1gseGtSY6frsctf3K8TTPx2zwbNB6ONjRvLLd9JwD7mU2TVFXukrREKYqnFO0q+anRI43zVYCjLc1/6VxCd7JLDhI5ZCi4RpMm+kskkNQ3OVAWsUvhY0QQktSrkroKQeQHzynjI5j3hp80Epbjz5YofC6UL0IRTn7y21evT9uHcjmzEnwgtmLv1+fIqe5jrRWbpbj9tIiKHLApdKsXSEMqa2KJB94NBe7bIGMCBsFQ7VRFDIxFRwOPLvZmlALEzsIsFUqNVtimoo7JXUJJASUzQZzNx7vcmwgcYdPQmnatUppqoLQGxbMoSjufCotNENg5vuYBZCuKWQzeVXudRYl1s4aS7LlxA4uXGeFGdhHWN/1VSMd86IPWlUWlnO6ovMOcg4euMwmC8V91a7yoqp0FB/w1lFa22ikZLrwbP1bANkOFibhE+BgD7oKkabuBRNzuaInpamOZLLYhuwYdktPGVXpmO0Rm3OgkXW+mSZKJVwIKcasW7j5mo992eJDKsFcgFls5rbRvVNdmO4PRpZmMKwx2ZLRhA6THZvK6gR3JHz1oOK0kCYbldvQcp1q2GBiZQshRatRTxu9VizQo86S5/IpGezi4iNgGLsNfNcV75yHdWAM/ymjfI56JuuqtcYfw5BaF8Jv4Uww8x8b+G3yATNbesuTTuY5aA/Y58bxxzO63zyKHY/x1TjZk9EmGTB2V+yc4smP5amXdr8YLz0vbs4ciXVdEezJY8pJnWQLlPcL/VASK0/nNt7wHMtS7OqMiAh15W6j5lK0ubRaB4t0gk9aEX+LHkzzn09OOZfWJ0nHBTsbop1A2KTF6ydhsGm0fbPdk4RvR9qcRgBb2iJmg510sv8SIvFRqOQhcGoLxnKDt2Pdl6uCkg8deAp4Kfx6ieIpZSs2nrBJfKeq8KOJyisY2vDKzaWyXgKXyO3ZQcMVlEDTL7iyN3u5RZaY9RN38M08+sb+i/HSZ025XaNZigume23wt0/7PtkSWnEmef42yQlFUXTRy1M8D6ANgkauTHBZRVWayiBllRUO/QvGtdC7zysoY7Mh7beD6Nj/M0SXI/jCi/9zF8lW+w/wvusbkmjv8Ckql8OsI5ZhpD5UKHTgf8HXmFmtjA2Yeg9KETvaNFOJHwilOXhbuZ40SozkEEmeH6UOveVus3bLr9ejg9i3k8ghtNyRcUZ1bMose4SE5YNJW2VqGbhEM5YuXrE6X2KreiSfEYhWf5UnIB3z9cQZWamBVK1XvxibG/M2NhoDUD9858PuwWpo+v5OOjTNMjKBTU4ciRUgo0NFwPr+Idt6urNoh+boPZsnhsanjyj6lhhTWwabXJKDy1A66UCcUvEKAowpthloYKOHdaiEbxPT47dFOSPLqYCeQJaNwBc22JINLA16pCN8aGkiTwaaZduwqIGzjONAJjxJIF/TTXCRwWLOXow7DxLvsT0Gqob74XRn9wMOeb1pXbW4rEGOK8OCuz4c/N4WyE/6eY9JZxk9dSNlda/OKMUvp3DC6jrKy+8rJyyRDNA5YRneDnbYQXymkmgco3ThUarevooJreO+9RnDS97c5rQN3OvSUdu0Hd1BnZM2S0d1uL3JDA/4MeRn7617044h8WpxWZVHzvlTFR3mgx49BTot9ym2bdDnz7sxyjdUv3pPwc61uBFTBP+D6WxZXLfWoUd4JVEmKKz08PluSUkJ+fWdVxPqYgxJE/ZerZUND0jL2mEat2CSs333mf0Suz6GgDbWx5fx6p8vjkozZnIxFIxo8PifE5/bEspQTCo/ON7LReOWFqGR6MOZTy5+TzZZMlP6jZab04lRARE6w7j3Z84AAAAg/wsD/2uxYpuYbww2/hIA4FGndwoAwOPFb/pZh//fj3hNRAIAAzah8d3XK/se97+/VwBCn89JC6QWEwFQD5KiA9xiNyQmgCQegi7qgJm9AMVtUIURBGkPMX+BwxtAUxX4+9MSljtIlweHSmYeqHE5hjjaH8PwbIzhvI2MEaQsMVIcKWjKQYBUHUqYhfeoVK5enRZ+/lOmj1Y1zJrkKNOkWWXfslAglQDWjabun3ZqUCFwKwUjKIZCpaDrwykXzyI8vNhsiixq6cLxkw4rx/ciOe426OxBJSRVbAQFAGXcDRLgI8S+adFJOBWlMAvddioLiTVWLwqnS80llMVUhjZlqZQKjqpZ/0cCAAA=); }</style></defs><rect x="0" y="0" width="497" height="186" fill="#ffffff"></rect><g stroke-linecap="round" transform="translate(32 10) rotate(0 59 28)"><path d="M14 0 C37.71 2.5, 65.62 -1, 104 0 M14 0 C36.28 0.01, 57.91 -1.12, 104 0 M104 0 C111.88 -0.93, 116.77 5.42, 118 14 M104 0 C111.51 1.63, 119.43 2.75, 118 14 M118 14 C119.83 21.63, 118.69 27.26, 118 42 M118 14 C117.52 20.85, 118.26 27.52, 118 42 M118 42 C119.97 50.14, 112.35 56.53, 104 56 M118 42 C115.94 49.43, 115.26 56.06, 104 56 M104 56 C78.39 56.98, 50.96 57.12, 14 56 M104 56 C69.99 56.33, 37.38 56.07, 14 56 M14 56 C5.97 56.06, 0.57 50.2, 0 42 M14 56 C5.24 56.83, 0.37 52.71, 0 42 M0 42 C1.26 34.95, -0.69 25.88, 0 14 M0 42 C0.71 35.7, 0.26 27.23, 0 14 M0 14 C-1.94 6.28, 6.54 -0.5, 14 0 M0 14 C-1.27 3.51, 4.94 0.75, 14 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(48.3799934387207 25.5) rotate(0 42.6200065612793 12.5)"><text x="42.6200065612793" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">... N W ...</text></g><g stroke-linecap="round" transform="translate(315 10) rotate(0 70.5 28)"><path d="M14 0 C44.89 -1.08, 73.51 -1.61, 127 0 M14 0 C51.76 0.79, 89.89 1.46, 127 0 M127 0 C137.95 0.48, 139.27 5.91, 141 14 M127 0 C134.13 1.15, 141.22 3.96, 141 14 M141 14 C139.83 21.63, 141.82 28.4, 141 42 M141 14 C141.12 21.88, 141.51 29.27, 141 42 M141 42 C140.74 50.7, 134.61 57.33, 127 56 M141 42 C140.62 51.42, 135.05 55.14, 127 56 M127 56 C98.01 55.54, 66.69 56.81, 14 56 M127 56 C84.63 56.67, 43 55.48, 14 56 M14 56 C4.76 57.72, -1.15 50.12, 0 42 M14 56 C6.9 56.7, -0.81 53.26, 0 42 M0 42 C2.07 34.48, 2.08 30.33, 0 14 M0 42 C0.62 35.84, -0.93 29.15, 0 14 M0 14 C1.82 6.4, 3.23 1.7, 14 0 M0 14 C1.48 4.86, 6.93 1.58, 14 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(332.6599922180176 25.5) rotate(0 52.84000778198242 12.5)"><text x="52.84000778198242" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">... N S W ...</text></g><g stroke-linecap="round" transform="translate(10 115) rotate(0 88 30.5)"><path d="M15.25 0 C63.61 0.87, 108.9 -0.12, 160.75 0 M15.25 0 C67.31 1.03, 118.04 1.01, 160.75 0 M160.75 0 C171.72 -1.97, 175.47 6.26, 176 15.25 M160.75 0 C173.07 0.79, 173.92 3.6, 176 15.25 M176 15.25 C175.9 21.92, 177.4 31.4, 176 45.75 M176 15.25 C176.23 21.26, 176.13 27.3, 176 45.75 M176 45.75 C177.03 56.13, 172.77 62.68, 160.75 61 M176 45.75 C174.81 55.51, 169.78 62.86, 160.75 61 M160.75 61 C124.76 63.89, 89.63 60.86, 15.25 61 M160.75 61 C111.63 60.03, 62.24 60.23, 15.25 61 M15.25 61 C5.79 62.36, 1.82 56.24, 0 45.75 M15.25 61 C5.57 62.4, -1.44 55.65, 0 45.75 M0 45.75 C-0.48 40.02, 0.99 30.68, 0 15.25 M0 45.75 C-0.4 36.35, 0.86 26.65, 0 15.25 M0 15.25 C-1.24 6.65, 6.42 1.48, 15.25 0 M0 15.25 C1.89 6.99, 5.78 1.08, 15.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(23.970001220703125 133) rotate(0 74.02999877929688 12.5)"><text x="74.02999877929688" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">P Q R S T U V</text></g><g stroke-linecap="round" transform="translate(286 108) rotate(0 38 30.5)"><path d="M15.25 0 C26.64 2.2, 35.43 -0.02, 60.75 0 M15.25 0 C30.3 -0.13, 42.88 0.26, 60.75 0 M60.75 0 C69 1, 76.19 4.47, 76 15.25 M60.75 0 C69.41 0.58, 74.64 3.66, 76 15.25 M76 15.25 C75.25 24.8, 74.65 37.37, 76 45.75 M76 15.25 C76.08 26.17, 75.34 38.4, 76 45.75 M76 45.75 C75.67 56, 69.8 60.25, 60.75 61 M76 45.75 C75.12 55.4, 71.54 61.69, 60.75 61 M60.75 61 C50.34 59.51, 37.13 61.08, 15.25 61 M60.75 61 C44.6 61.57, 27.79 60.11, 15.25 61 M15.25 61 C7.03 61.61, -0.71 57.59, 0 45.75 M15.25 61 C3.41 58.89, 1.41 58.01, 0 45.75 M0 45.75 C-1.6 38.1, -2.2 32.12, 0 15.25 M0 45.75 C1.21 36.27, -0.42 25.91, 0 15.25 M0 15.25 C1.29 5.26, 7.05 1.37, 15.25 0 M0 15.25 C-0.32 4.89, 6.12 -1.99, 15.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(293.9799995422363 126) rotate(0 30.020000457763672 12.5)"><text x="30.020000457763672" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">P Q R</text></g><g stroke-linecap="round" transform="translate(411 108) rotate(0 38 30.5)"><path d="M15.25 0 C30.58 -0.44, 42.98 -0.48, 60.75 0 M15.25 0 C28.82 0.57, 43.7 -1.23, 60.75 0 M60.75 0 C69.23 -1.26, 76.14 3.51, 76 15.25 M60.75 0 C69.89 -0.38, 78.21 5.3, 76 15.25 M76 15.25 C75.76 25.53, 76.06 39.74, 76 45.75 M76 15.25 C76.33 22.8, 76.3 28.4, 76 45.75 M76 45.75 C74.41 56.98, 69.17 59.9, 60.75 61 M76 45.75 C76.25 53.85, 71.95 58.71, 60.75 61 M60.75 61 C48.01 61.61, 36.42 60.88, 15.25 61 M60.75 61 C46.36 61.21, 30.23 60.2, 15.25 61 M15.25 61 C4.93 60.31, 1.83 54.29, 0 45.75 M15.25 61 C7.08 62.25, 0.98 54.7, 0 45.75 M0 45.75 C1.28 36.51, 0.78 27.2, 0 15.25 M0 45.75 C-0.6 34.34, -0.01 22.33, 0 15.25 M0 15.25 C-0.3 5.28, 5.82 0.5, 15.25 0 M0 15.25 C2.17 4.25, 5.13 -0.43, 15.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(419.2100009918213 126) rotate(0 29.78999900817871 12.5)"><text x="29.78999900817871" y="17.619999999999997" font-family="Excalifont, Xiaolai, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">T U V</text></g><g stroke-linecap="round"><g transform="translate(92 67.5) rotate(0 1.1889074526092145 23.25)"><path d="M0.01 -0.16 C0.46 7.53, 2.21 38.44, 2.55 46.29 M-0.65 -0.71 C-0.25 7.04, 1.84 39.08, 2.21 46.89" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(92 67.5) rotate(0 1.1889074526092145 23.25)"><path d="M-6.99 25.5 C-4.69 31.77, -2.22 36.86, 2.21 46.89 M-6.99 25.5 C-3.59 33.31, -0.19 42.31, 2.21 46.89" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(92 67.5) rotate(0 1.1889074526092145 23.25)"><path d="M8.9 24.59 C6.79 31.11, 4.84 36.45, 2.21 46.89 M8.9 24.59 C6.18 32.8, 3.46 42.16, 2.21 46.89" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(365.55828232856084 67.5) rotate(0 -20.412556225982314 18.25)"><path d="M0.49 -0.32 C-6.25 5.74, -34.05 30.36, -40.95 36.57 M0.08 0.71 C-6.7 6.85, -34.56 31.41, -41.44 37.31" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(365.55828232856084 67.5) rotate(0 -20.412556225982314 18.25)"><path d="M-29.35 15.43 C-32.41 19.82, -34.21 24.53, -41.44 37.31 M-29.35 15.43 C-34.3 24.09, -38.95 32.36, -41.44 37.31" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(365.55828232856084 67.5) rotate(0 -20.412556225982314 18.25)"><path d="M-18.11 28.32 C-23.53 30.07, -27.68 32.1, -41.44 37.31 M-18.11 28.32 C-27.39 32.08, -36.32 35.44, -41.44 37.31" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(172 70.5) rotate(0 54 0.5)"><path d="M0.5 -0.7 C18.4 -0.45, 90.05 1.52, 107.93 1.83 M-0.69 1.54 C16.99 1.44, 89.01 -0.03, 106.94 0.14" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(172 70.5) rotate(0 54 0.5)"><path d="M83.51 8.86 C90.41 5.63, 101.05 2.35, 106.94 0.14 M83.51 8.86 C90.87 5.98, 98.76 2.81, 106.94 0.14" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(172 70.5) rotate(0 54 0.5)"><path d="M83.38 -8.24 C90.37 -5.56, 101.05 -2.94, 106.94 0.14 M83.38 -8.24 C90.93 -5.78, 98.86 -3.61, 106.94 0.14" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(403 68.5) rotate(0 22.5 18.5)"><path d="M-0.55 -0.3 C6.96 5.99, 37.52 31.12, 45.09 37.36 M0.17 0.74 C7.61 6.9, 37.32 30.4, 44.7 36.58" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(403 68.5) rotate(0 22.5 18.5)"><path d="M21.07 28.42 C27.82 31.22, 36.34 33.36, 44.7 36.58 M21.07 28.42 C28.79 30.7, 36.46 34.16, 44.7 36.58" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(403 68.5) rotate(0 22.5 18.5)"><path d="M31.84 15.14 C35.21 22, 40.39 28.26, 44.7 36.58 M31.84 15.14 C36.06 21.71, 40.22 29.51, 44.7 36.58" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask>
</svg>

<p>A função <code class="language-plaintext highlighter-rouge">split</code> faz esse processo de rotação das chaves, que é realizado antes da inclusão em um nó cheio:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">split</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">pivot</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">order</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">left</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="nn">Node</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.children</span><span class="p">[</span><span class="n">pivot</span><span class="p">]);</span>
    <span class="k">let</span> <span class="n">key</span> <span class="o">=</span> <span class="n">left</span><span class="py">.keys</span><span class="p">[</span><span class="n">order</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span><span class="nf">.clone</span><span class="p">();</span>

    <span class="k">let</span> <span class="n">right</span> <span class="o">=</span> <span class="n">Node</span> <span class="p">{</span>
        <span class="n">keys</span><span class="p">:</span> <span class="n">left</span><span class="py">.keys</span><span class="p">[</span><span class="n">order</span><span class="o">..</span><span class="n">left</span><span class="py">.keys</span><span class="nf">.len</span><span class="p">()]</span><span class="nf">.to_owned</span><span class="p">(),</span>
        <span class="n">children</span><span class="p">:</span> <span class="k">match</span> <span class="n">left</span><span class="py">.leaf</span> <span class="p">{</span>
            <span class="k">true</span> <span class="k">=&gt;</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">with_capacity</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">order</span><span class="p">),</span>
            <span class="k">false</span> <span class="k">=&gt;</span> <span class="n">left</span><span class="py">.children</span><span class="p">[</span><span class="n">order</span><span class="o">..</span><span class="n">left</span><span class="py">.children</span><span class="nf">.len</span><span class="p">()]</span><span class="nf">.to_owned</span><span class="p">(),</span>
        <span class="p">},</span>
        <span class="n">leaf</span><span class="p">:</span> <span class="n">left</span><span class="py">.leaf</span><span class="p">,</span>
        <span class="n">filename</span><span class="p">:</span> <span class="nn">Node</span><span class="p">::</span><span class="nf">filename</span><span class="p">(</span><span class="n">path</span><span class="p">),</span>
    <span class="p">};</span>

    <span class="n">right</span><span class="nf">.save</span><span class="p">();</span>

    <span class="n">left</span><span class="py">.keys</span><span class="nf">.resize</span><span class="p">(</span><span class="n">order</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="nn">Key</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)));</span>

    <span class="k">if</span> <span class="o">!</span><span class="n">left</span><span class="py">.leaf</span> <span class="p">{</span>
        <span class="n">left</span><span class="py">.children</span><span class="nf">.resize</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="s">""</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="n">left</span><span class="nf">.save</span><span class="p">();</span>

    <span class="k">self</span><span class="py">.keys</span><span class="nf">.insert</span><span class="p">(</span><span class="n">pivot</span><span class="p">,</span> <span class="n">key</span><span class="p">);</span>
    <span class="k">self</span><span class="py">.children</span><span class="nf">.insert</span><span class="p">(</span><span class="n">pivot</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">right</span><span class="py">.filename</span><span class="p">);</span>

    <span class="k">self</span><span class="nf">.save</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Em várias momentos, as estruturas são persistidas e lidas do armazenamento, usando as funções <code class="language-plaintext highlighter-rouge">save</code>e <code class="language-plaintext highlighter-rouge">load</code> respectivamente. Essas funções serializam e desserializam o objeto <code class="language-plaintext highlighter-rouge">Node</code> no formato json, usando a biblioteca <a href="https://docs.rs/serde_json/latest/serde_json/">serde_json</a>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">save</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.filename</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="n">file</span><span class="nf">.write_all</span><span class="p">(</span><span class="nn">serde_json</span><span class="p">::</span><span class="nf">to_string</span><span class="p">(</span><span class="k">self</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.as_bytes</span><span class="p">())</span>
        <span class="nf">.unwrap</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">load</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Node</span> <span class="p">{</span>
    <span class="nn">serde_json</span><span class="p">::</span><span class="nf">from_slice</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">fs</span><span class="p">::</span><span class="nf">read</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">())</span><span class="nf">.unwrap</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Cada nó é salvo em um arquivo com nome aleatório, gerado pela função <code class="language-plaintext highlighter-rouge">filename</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">filename</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span>
    <span class="nd">format!</span><span class="p">(</span><span class="s">"{}/{}.json"</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>O formato json não é o ideal em termos de performance, mas é legível por humanos e prático para ser utilizado em um código para estudos. Abaixo, um exemplo de nó salvo em disco, gerado pelos testes unitários:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"keys"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9b35db6e-49d9-4d91-b5f6-973b8a9111f4"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="mi">0</span><span class="p">,</span><span class="w">
                </span><span class="mi">0</span><span class="w">
            </span><span class="p">]</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d47fbd93-cb1a-4e01-82b2-4325aa8cb1a3"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="mi">0</span><span class="p">,</span><span class="w">
                </span><span class="mi">0</span><span class="w">
            </span><span class="p">]</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"btree_test_search/d1740cf9-8fa9-471d-9ba6-99ff1462f5c6.json"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"btree_test_search/9ba9bd01-6b3e-4441-a70b-253c86424cd3.json"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"btree_test_search/7248b1e6-88f6-4f8e-a07b-0952347adc47.json"</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"leaf"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"btree_test_search/1c3df73f-f321-4b3a-a545-9917ad2ecb4e.json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="buscando-na-árvore">Buscando na árvore</h2>

<p>A busca é feito de forma recursiva, como é comum em estruturas de árvore. A função pública <code class="language-plaintext highlighter-rouge">search</code> da <code class="language-plaintext highlighter-rouge">Btree</code> tem apenas o parâmetro <code class="language-plaintext highlighter-rouge">value</code> com o valor da chave a ser procurado, a função <code class="language-plaintext highlighter-rouge">search_tree</code> é a que de fato realiza a busca recursiva.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">search_tree</span><span class="p">(</span><span class="n">node</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Node</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Key</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span> <span class="k">in</span> <span class="n">node</span><span class="py">.keys</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.enumerate</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">key</span><span class="py">.value</span> <span class="o">==</span> <span class="n">value</span> <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Some</span><span class="p">(</span><span class="n">key</span><span class="nf">.clone</span><span class="p">());</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">key</span><span class="py">.value</span> <span class="o">&gt;</span> <span class="n">value</span> <span class="p">{</span>
            <span class="k">if</span> <span class="n">node</span><span class="py">.leaf</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nn">BTree</span><span class="p">::</span><span class="nf">search_tree</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">Node</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="n">node</span><span class="py">.children</span><span class="p">[</span><span class="n">i</span><span class="p">]),</span> <span class="n">value</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="n">node</span><span class="py">.leaf</span> <span class="p">{</span>
        <span class="nb">None</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nn">BTree</span><span class="p">::</span><span class="nf">search_tree</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">Node</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="n">node</span><span class="py">.children</span><span class="p">[</span><span class="n">node</span><span class="py">.keys</span><span class="nf">.len</span><span class="p">()]),</span> <span class="n">value</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">search</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="n">Key</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">BTree</span><span class="p">::</span><span class="nf">search_tree</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="py">.root</span><span class="p">,</span> <span class="n">value</span><span class="nf">.to_string</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Com a busca e a inserção criadas, já temos o mínimo para fazer o indexador.</p>

<h2 id="indexando-os-csvs">Indexando os CSVs</h2>

<p>Os <a href="https://en.wikipedia.org/wiki/Comma-separated_values">arquivos CSVs</a> são muito utilizados para lidar com dados tabulares. A organização física desses arquivos é parecida com a de uma tabela em um banco relacional, sendo orientado a linhas e com marcadores utilizados para indetificar início e fim dos campos e registros.</p>

<p>A função <code class="language-plaintext highlighter-rouge">index_file</code> recebe um arquivo e uma árvore, itera linha-a-linha no arquivo e armazena três informações na árvore: o valor da chave, a posição da linha no arquivo e a quantidade de bytes por linha.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">index_file</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">File</span><span class="p">,</span> <span class="n">tree</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">BTree</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">reader</span> <span class="o">=</span> <span class="nn">BufReader</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">buf</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">offset</span><span class="p">:</span> <span class="nb">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">loop</span> <span class="p">{</span>
        <span class="n">buf</span><span class="nf">.clear</span><span class="p">();</span>

        <span class="k">let</span> <span class="n">size</span><span class="p">:</span> <span class="nb">u64</span> <span class="o">=</span> <span class="n">reader</span>
            <span class="nf">.read_line</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="n">buf</span><span class="p">)</span>
            <span class="nf">.expect</span><span class="p">(</span><span class="s">"reading from cursor shouldn't fail"</span><span class="p">)</span>
            <span class="nf">.try_into</span><span class="p">()</span>
            <span class="nf">.unwrap</span><span class="p">();</span>

        <span class="k">if</span> <span class="n">size</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">let</span> <span class="n">key_value</span> <span class="o">=</span> <span class="k">match</span> <span class="nf">get_key</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">buf</span><span class="p">)</span> <span class="p">{</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="n">offset</span> <span class="o">+=</span> <span class="n">size</span><span class="p">;</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">value</span><span class="p">,</span>
        <span class="p">};</span>

        <span class="n">tree</span><span class="nf">.insert</span><span class="p">(</span><span class="nn">Key</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="n">key_value</span><span class="p">,</span> <span class="p">(</span><span class="n">offset</span><span class="p">,</span> <span class="n">size</span><span class="p">)));</span>
        <span class="n">offset</span> <span class="o">+=</span> <span class="n">size</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Para recuperar as informações do arquivo, a função recebe o arquivo e uma tupla contendo a posição da linha e seu tamanho.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">read_line</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">File</span><span class="p">,</span> <span class="n">position</span><span class="p">:</span> <span class="p">(</span><span class="nb">u64</span><span class="p">,</span> <span class="nb">u64</span><span class="p">))</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="nn">error</span><span class="p">::</span><span class="n">Error</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="n">offset</span><span class="p">)</span> <span class="o">=</span> <span class="n">position</span><span class="p">;</span>
    <span class="n">file</span><span class="nf">.seek</span><span class="p">(</span><span class="nn">SeekFrom</span><span class="p">::</span><span class="nf">Start</span><span class="p">(</span><span class="n">start</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">read_buf</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span><span class="mi">0</span><span class="p">;</span> <span class="n">offset</span><span class="nf">.try_into</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()];</span>
    <span class="n">file</span><span class="nf">.read_exact</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="n">read_buf</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="k">match</span> <span class="nn">String</span><span class="p">::</span><span class="nf">from_utf8</span><span class="p">(</span><span class="n">read_buf</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">Err</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">e</span><span class="p">)),</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">line</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">line</span><span class="p">),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="e-a-performance">E a performance?</h2>

<p>Para testar o indexador, gerei um arquivo CSVs com 10.000.000 de registros e aproximadamente 500MB de tamanho. Abaixo, o script para indexar esse arquivo usando nós de ordem 1.000:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>

    <span class="k">let</span> <span class="n">filename</span> <span class="o">=</span> <span class="s">"/home/gdarruda/Projects/sandbox/clients.csv"</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">open</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">tree</span> <span class="o">=</span> <span class="nn">index</span><span class="p">::</span><span class="nn">btree</span><span class="p">::</span><span class="nn">BTree</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="s">"/home/gdarruda/btree_files"</span><span class="p">);</span>
    <span class="nn">csv</span><span class="p">::</span><span class="nf">index_file</span><span class="p">(</span><span class="o">&amp;</span><span class="n">file</span><span class="p">,</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">tree</span><span class="p">);</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Para indexar esse arquivo, foram necessárias 3:17 horas, mas não passou de 3MB de consumo total de memória. Um tempo alto para indexar, teríamos várias oportunidades de otimização – melhorar serialização; não forçar escrita em disco para cada inclusão; otimizar os tamanhos dos nós; usar cache – mas esse não é o ponto do exercício proposto.</p>

<p>Olhando para o desempenho da busca – considerando o pior caso, que é procurar e não encontrar um registro – procurando por 1.000 chaves aleatórias não existentes, o tempo médio de busca foi de 474µs com desvio de 26µs.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="nb">Result</span><span class="o">&lt;</span><span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>

    <span class="k">let</span> <span class="n">filename</span> <span class="o">=</span> <span class="s">"/home/gdarruda/Projects/sandbox/clients.csv"</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">open</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">tree</span> <span class="o">=</span> <span class="nn">index</span><span class="p">::</span><span class="nn">btree</span><span class="p">::</span><span class="nn">BTree</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="s">"/home/gdarruda/btree_files"</span><span class="p">);</span>

    <span class="k">for</span> <span class="n">uuid</span> <span class="k">in</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">1000</span><span class="p">)</span><span class="nf">.map</span><span class="p">(|</span><span class="n">_</span><span class="p">|</span> <span class="nn">Uuid</span><span class="p">::</span><span class="nf">new_v4</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">())</span> <span class="p">{</span>

        <span class="k">let</span> <span class="n">now</span> <span class="o">=</span> <span class="nn">SystemTime</span><span class="p">::</span><span class="nf">now</span><span class="p">();</span>

        <span class="k">match</span> <span class="n">tree</span><span class="nf">.search</span><span class="p">(</span><span class="o">&amp;</span><span class="n">uuid</span><span class="p">)</span> <span class="p">{</span>
            <span class="nb">None</span> <span class="k">=&gt;</span> <span class="p">{},</span>
            <span class="nf">Some</span><span class="p">(</span><span class="n">key</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="k">match</span> <span class="nn">csv</span><span class="p">::</span><span class="nf">read_line</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="n">file</span><span class="p">,</span> <span class="n">key</span><span class="py">.position</span><span class="p">)</span> <span class="p">{</span>
                    <span class="nf">Err</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span><span class="nd">println!</span><span class="p">(</span><span class="s">"Error: {}"</span><span class="p">,</span> <span class="n">e</span><span class="p">)},</span>
                    <span class="nf">Ok</span><span class="p">(</span><span class="n">line</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span><span class="nd">println!</span><span class="p">(</span><span class="s">"Found line: {}"</span><span class="p">,</span> <span class="n">line</span><span class="p">)}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">};</span>

        <span class="k">match</span> <span class="n">now</span><span class="nf">.elapsed</span><span class="p">()</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(</span><span class="n">elapsed</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">elapsed</span><span class="nf">.as_micros</span><span class="p">());</span>
            <span class="p">}</span>
            <span class="nf">Err</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="nd">println!</span><span class="p">(</span><span class="s">"Error: {e:?}"</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>

    <span class="p">}</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pode-se pensar em otimizações para essa parte da busca também, mas essa implementação simplória já demonstra a utilidade de uma estrutura como essa para grandes base de dados: boa performance de busca, baixíssimo consumo de memória.</p>

<h2 id="por-que-fazer-isso">Por que fazer isso?</h2>

<p>Não sou a favor que os programadores fiquem recriando banco de dados e frameworks: apesar de ser uma ótima forma de aprender profundamente sobre um tema, provavelmente não é a forma mais eficiente. De qualquer forma, acho fundamental ter uma noção intuitiva de como as coisas funcionam e não focar apenas nas APIs das ferramentas.</p>

<p>Mais importante que entender os detalhes da <a href="https://github.com/gdarruda/csv_indexer">minha implementação</a>, eu gostaria que os programadores conseguissem responder perguntas sobre índices basedos em árvore:</p>

<ul>
  <li>Se vou ler 50% da tabela, um índice me ajuda? E se for 75%? E se for 5%?</li>
  <li>Índices fazem sentido para armazenamento colunar, como um arquivo parquet por exemplo?</li>
  <li>Quais as vantagens e desvantagens de colocar múltiplas colunas em um único índice?</li>
  <li>Em uma coluna com poucos valores distintos, faz mais sentido usar ela como partição ou criar um índice? Por quê?</li>
</ul>

<p>Obviamente, todas essas perguntas têm vários “depende” em uma situação real, mas é possível ter uma noção das respostas pelo conhecimento teórico. São perguntas que realmente aparecem no dia-a-dia, escolher a melhor solução de armazenamento costumar ser uma <a href="https://www.reddit.com/r/coolguides/comments/18t1a92/a_cool_guide_to_jeff_bezoss_decisionmaking_model/">porta de sentido único</a>.</p>

<p>Quando eu comecei a carreira, o padrão era usar bancos relacionais, no máximo a discussão era qual deles escolher. Hoje em dia, pode-se recorrer a uma miríade de soluções especializadas para cada caso de uso: desde armazenar dados como arquivos em storage, passando por bancos relacionais e diversos tipos de bancos NoSQL especializados (<em>e.g.</em> grafos, chave-valor, documento, MPP).</p>

<p>As soluções especializadas podem ser mais escaláveis e nem sempre usar <a href="https://github.com/Olshansk/postgres_for_everything">Postgres para tudo</a> é a melhor opção, mas ferramentas especializadas podem ser inutilizáveis quando adaptadas para cenários <a href="https://broot.ca/kafka-at-the-low-end.html">fora de seu caso de uso</a>. Aprender sobre todas as soluções de todos provedores de cloud é inviável, focar em conhecer <a href="https://www.youtube.com/watch?v=yvBR71D0nAQ&amp;t=412s">os conceitos das ferramentas</a> é melhor que ler infinitos artigos no Medium comparando as soluções em termos de “prós e contras”.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Criando um índices para arquivos CSV]]></summary></entry><entry><title type="html">Processos, threads e co-rotinas em Python</title><link href="/2024/08/27/concorrencia.html" rel="alternate" type="text/html" title="Processos, threads e co-rotinas em Python" /><published>2024-08-27T00:00:00+00:00</published><updated>2024-08-27T00:00:00+00:00</updated><id>/2024/08/27/concorrencia</id><content type="html" xml:base="/2024/08/27/concorrencia.html"><![CDATA[<p>Para explicar o conceito de algoritmo, é normal descrever informalmente como uma sequência de passos para resolver um problema. Ao lidar com concorrência e paralelismo, ainda temos passos a serem seguidos, mas eles não estão mais em ordem necessariamente.</p>

<p>Há várias camadas de abstração – para que o desenvolvedor posso pensar em seus algoritmos como uma sequência de passos – mas na prática muita coisa é executada fora de ordem. Nem mesmo o processador seguem a ordem das instruções, execução <a href="https://en.wikipedia.org/wiki/Out-of-order_execution">out of order</a> é implementado em todas arquiteturas modernas para evitar desperdício de ciclos.</p>

<p>Essas abstrações funcionam muito bem em certos cenários, como é o caso de servidores web e sistemas de banco de dados por exemplo. Entretanto, algumas vezes é necessário lidar diretamente com concorrência e paralelismo.</p>

<p>Não é fácil trabalhar com esses conceitos – é uma mudança fundamental no modelo mental do programador – não surpreende que seja um tópico intimidador para muita gente. Minha ideia é escrever esse post tentando explicar de forma didática, como fazer esse tipo de aplicação em Python.</p>

<h2 id="conceitos-básicos">Conceitos básicos</h2>

<p>O que complica (ainda mais) lidar com de problemas de concorrência e paralelismo, é a abundância de conceitos e detalhes de implementação de cada linguagem. Por isso, é bom alinhar alguns conceitos, antes de entrar na implementação.</p>

<p>A ideia é realmente definir o mínimo, tudo bem se a explicação resumida não fizer completo sentido, não acho que seja impeditivo para o entendimento das implementações.</p>

<h1 id="processos">Processos</h1>

<p>No contexto de sistemas operacionais, um <a href="https://en.wikipedia.org/wiki/Process_(computing)">processo</a> é uma instância do programa sendo executado. Pontos importante sobre processos:</p>

<ul>
  <li>os processos têm memória isolada, o que está na memória de um processo não pode ser visto por outro;</li>
  <li>o sistema operacional é responsável por escalonar os processos, alternando a execução para que todos tenham algum tempo de execução de acordo com a sua prioridade;</li>
  <li>um processo em execução pode ser interrompido por I/O, entrando em espera quando precisa interagir com algo externo (e.g. rede, armazenamento);</li>
  <li>existe um custo para alternar entre diferentes processos no processador, como limpar os registradores e mudar de pilha.</li>
</ul>

<h1 id="threads-kernel-threads">Threads (kernel threads)</h1>

<p>Um processo pode conter uma ou múltiplas threads, são conceitos parecidos, mas com diferenças importantes:</p>

<ul>
  <li>threads de um mesmo processo podem compartilhar recursos entre si, como conexões de rede e dados na memória;</li>
  <li>o custo de alternar entre threads do mesmo processo é mais baixo que alternar entre diferentes processos;</li>
  <li>dependendo do sistema operacional, o custo de criar e destruir threads é muito mais baixo que processos.</li>
</ul>

<h1 id="co-rotinas-e-tarefas">Co-rotinas e tarefas</h1>

<p>Os processos e threads são conceitos a nível do sistema operacional, mas é comum as linguagens implementaram algo similar e mais enxuto no próprio runtime. O Python tem as <a href="https://docs.python.org/3/library/asyncio-task.html">co-rotinas e tarefas</a>, são mais leves para criar/destruir que threads e executadas de <a href="https://en.wikipedia.org/wiki/Cooperative_multitasking">forma cooperativa</a> ao invés de preemptiva.</p>

<p>As implementações diferem entre linguagens, as ideias e estratégias discutidas nesse post não se aplicam as co-rotinas de outras linguagens necessariamente.</p>

<h1 id="gil-global-interpreter-lock">GIL (Global Interpreter Lock)</h1>

<p>A <a href="https://en.wikipedia.org/wiki/CPython">implementação padrão</a> do Python usa o GIL (Global Interpreter Lock), que não permite a execução paralela de threads de um mesmo processo. A presença do GIL simplifica o desenvolvimento da linguagem, mas impede o paralelismo de processamento a nível de thread.</p>

<p>A proposta de deixar o <a href="https://peps.python.org/pep-0703/">GIL opcional</a> foi aceita, será possível desabilita-ló no Python 3.13. É necessário esperar para ver como será a migração do ecossistema, mas imagino que as limitações de paralelismo serão realidade por muito tempo ainda.</p>

<h2 id="paralelismo-de-dados">Paralelismo de dados</h2>

<p>Raramente é simples transformar um problema sequencial em paralelizável, muitas vezes é simplesmente impossível. Uma estratégia alternativa é aplicar o paralelismo de dados, que consiste em dividir a entrada e combinar os resultados posteriormente.</p>

<p>Um candidato ideal para essa estratégia, é o script que utilizei em <a href="/2023/03/04/engenharia-dados.html">outro post</a> para gerar uma massa de dados aleatória, simulando a saída de um modelo de machine learning. Abaixo, a função que gera uma base de amostra:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">num_rows</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>

    <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nc">DataFrame</span><span class="p">(</span>
        <span class="p">{</span>
            <span class="o">**</span><span class="p">{</span><span class="sh">"</span><span class="s">id_client</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="nf">str</span><span class="p">(</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">())</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_rows</span><span class="p">)]},</span>
            <span class="o">**</span><span class="p">{</span><span class="sa">f</span><span class="sh">"</span><span class="s">class_</span><span class="si">{</span><span class="n">c</span><span class="si">}</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">uniform</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="n">num_rows</span><span class="p">)</span>
               <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_classes</span><span class="p">)}</span>
        <span class="p">}</span>
    <span class="p">)</span>

    <span class="n">df</span><span class="p">.</span><span class="nf">to_parquet</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">path</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">num_rows</span><span class="si">}</span><span class="s">-</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">()</span><span class="si">}</span><span class="s">.parquet</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>A estratégia mais simples para paralelizar esse processo, é executar essa função múltiplas vezes de forma independente: podemos executar a função para gerar \(n\) amostras ou executar \(k\) vezes gerando \(n/k\) amostras por execução, é o mesmo resultado em termos de funcionalidade.</p>

<p>Um dos jeitos mais simples de aplicar paralelismo de dados, é utilizando a ideia de <a href="https://en.wikipedia.org/wiki/Thread_pool">piscina de threads</a>. A “piscina” é um conjunto fixo de threads que ficam disponíveis para uso, as tarefas são enfileiradas e entram em processamento quando uma thread fica disponível. Abaixo, uma ilustração dessa estratégia:</p>

<figure>
  <img src="/assets/images/paralelismo/pool.gif" />
  <figcaption>Figura 1 – Ilustração de um Thread Pool</figcaption>
</figure>

<p>Para implementá-la, é necessário separar o problemas em pedaços menores, o que é trivial nesse caso. Basta dividir o total de linhas pelo tamanho desejado para cada batch:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_build_batches</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">num_rows</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>

    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_rows</span><span class="o">//</span><span class="n">self</span><span class="p">.</span><span class="n">batch_size</span><span class="p">):</span>
        <span class="k">yield</span> <span class="n">self</span><span class="p">.</span><span class="n">batch_size</span>
    
    <span class="n">remainder</span> <span class="o">=</span> <span class="n">num_rows</span> <span class="o">%</span> <span class="n">self</span><span class="p">.</span><span class="n">batch_size</span>

    <span class="k">if</span> <span class="n">remainder</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">yield</span> <span class="n">remainder</span>
</code></pre></div></div>

<p>O tamanho do batch não é muito importante, exceto pelo consumo de memória. Cada chamada da função gera um DataFrame com o tamanho do batch, é necessário ter memória suficiente para que todas as threads possam manter o DataFrame em memória simultaneamente.</p>

<p>Com os batches criados – usando a função <code class="language-plaintext highlighter-rouge">map</code> de um <code class="language-plaintext highlighter-rouge">ThreadPool</code> – <code class="language-plaintext highlighter-rouge">generate</code> é chamada para cada item de <code class="language-plaintext highlighter-rouge">batches</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate_parallel</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                      <span class="n">num_rows</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>

    <span class="n">batches</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_build_batches</span><span class="p">(</span><span class="n">num_rows</span><span class="p">)</span>

    <span class="k">with</span> <span class="nc">ThreadPool</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="nf">cpu_count</span><span class="p">())</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
        <span class="n">p</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">generate</span><span class="p">,</span> <span class="n">batches</span><span class="p">)</span>
</code></pre></div></div>

<p>Essa implementação está tecnicamente correta, mas não faz sentido devido ao GIL. No final, <code class="language-plaintext highlighter-rouge">generate_parallel</code> é mais lenta que <code class="language-plaintext highlighter-rouge">generate</code>, ja que as threads não podem ser executadas paralelamente e ainda é necessário coordená-las:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">NUM_ROWS</span> <span class="o">=</span> <span class="mi">1_000_000</span>
<span class="n">BATCH_SIZE</span> <span class="o">=</span> <span class="mi">5_000</span>
<span class="n">NUM_CLASSES</span> <span class="o">=</span> <span class="mi">10</span>

<span class="n">generator</span> <span class="o">=</span> <span class="nc">SampleGenerator</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">,</span> <span class="sh">"</span><span class="s">samples</span><span class="sh">"</span><span class="p">)</span>
<span class="o">%</span><span class="n">timeit</span> <span class="n">generator</span><span class="p">.</span><span class="nf">generate_parallel</span><span class="p">(</span><span class="n">NUM_ROWS</span><span class="p">)</span>
<span class="o">%</span><span class="n">timeit</span> <span class="n">generator</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">NUM_ROWS</span><span class="p">)</span>

<span class="c1"># 4.8 s ± 51.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 3.73 s ± 16.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
</span></code></pre></div></div>
<p>A solução para esse caso, é simplesmente trocar o tipo de pool, usando processos ao invés de threads. O código é idêntico, bastando alterar o tipo de contexto utilizado, de <code class="language-plaintext highlighter-rouge">ThreadPool</code> para <code class="language-plaintext highlighter-rouge">Pool</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate_parallel</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                      <span class="n">num_rows</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">:</span>

    <span class="n">batches</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_build_batches</span><span class="p">(</span><span class="n">num_rows</span><span class="p">)</span>

    <span class="k">with</span> <span class="nc">Pool</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="nf">cpu_count</span><span class="p">())</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
        <span class="n">p</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">generate</span><span class="p">,</span> <span class="n">batches</span><span class="p">)</span>
</code></pre></div></div>

<p>Usando um processador de 6 núcleos com <a href="https://en.wikipedia.org/wiki/Simultaneous_multithreading">SMT</a>, criar as amostras é ~ 4,29 vezes mais rápido, trazendo o aumento de desempenho esperado:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">NUM_ROWS</span> <span class="o">=</span> <span class="mi">1_000_000</span>
<span class="n">BATCH_SIZE</span> <span class="o">=</span> <span class="mi">5_000</span>
<span class="n">NUM_CLASSES</span> <span class="o">=</span> <span class="mi">10</span>

<span class="n">generator</span> <span class="o">=</span> <span class="nc">SampleGenerator</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">,</span> <span class="sh">"</span><span class="s">samples</span><span class="sh">"</span><span class="p">)</span>
<span class="o">%</span><span class="n">timeit</span> <span class="n">generator</span><span class="p">.</span><span class="nf">generate_parallel</span><span class="p">(</span><span class="n">NUM_ROWS</span><span class="p">)</span>
<span class="o">%</span><span class="n">timeit</span> <span class="n">generator</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">NUM_ROWS</span><span class="p">)</span>

<span class="c1"># 873 ms ± 2.23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 3.75 s ± 25.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
</span></code></pre></div></div>

<p>Algumas perguntas surgem com esses resultados. Se é tão simples, usar processos ao invés de threads, por que usar threads? Para que servem as threads, se existe o GIL?</p>

<p>O problema proposto tem características que o tornam perfeitos para ser utilizado em múltiplos processos:</p>

<ul>
  <li>
    <p>pode ser separado e combinado a custo zero, a função recebe apenas um inteiro de entrada e não precisa se comunicar com os demais processos;</p>
  </li>
  <li>
    <p>é um processo que demanda muito processamento e pouca movimentação dados, o GIL anula a utilidade de trabalhar com múltiplas threads.</p>
  </li>
</ul>

<p>Nem sempre é o caso, existem cenários em que threads podem ser úteis, como é o caso dos cenários de aplicação descritas como <a href="https://en.wikipedia.org/wiki/I/O_bound">I/O bound</a>.</p>

<h2 id="o-cenário-io-bound">O cenário “I/O bound”</h2>

<p>Muitos problemas do dia-a-dia não são limitados por tempo de processamento, mas por movimentação de dados fora da memória: escrita em banco de dados, comunicação por rede, escrita em storage, postagem em filas, etc. Essas operações são <a href="https://gist.github.com/jboner/2841832">ordens de grandeza mais lentas</a> que o acesso a memória, são nesses cenários que o uso de threads e co-rotinas fazem sentido.</p>

<p>Para ilustrar esse cenário, o problema proposta agora é salvar essas amostras aleatórias em uma tabela no PostgreSQL. Para ter como baseline, as classes <code class="language-plaintext highlighter-rouge">DatabaseNaive</code> e <code class="language-plaintext highlighter-rouge">LoaderNaive</code> são a uma solução trivial para esse problema:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DatabaseNaive</span><span class="p">(</span><span class="n">Database</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">connection_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
                 <span class="n">num_classes</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">(</span><span class="n">connection_url</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">conn</span> <span class="o">=</span> <span class="n">psycopg</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">connection_url</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">save_message</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">prediction</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>

        <span class="k">with</span> <span class="n">self</span><span class="p">.</span><span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
            <span class="n">cur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o">*</span><span class="n">self</span><span class="p">.</span><span class="nf">_build_insert</span><span class="p">(</span><span class="n">prediction</span><span class="p">))</span>

        <span class="n">self</span><span class="p">.</span><span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">LoaderNaive</span><span class="p">():</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">db</span><span class="p">:</span> <span class="n">DatabaseNaive</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">db</span> <span class="o">=</span> <span class="n">db</span>

    <span class="k">def</span> <span class="nf">load</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">):</span>

        <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="nf">iterrows</span><span class="p">():</span>
            <span class="n">self</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="nf">save_message</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="nf">to_dict</span><span class="p">())</span>
</code></pre></div></div>

<p>A solução acima salva um registro de cada vez, esperando a escrita no banco de dados, antes de passar para o próximo. Para 100.000 registros, esse processo demora aproximadamente 6 minutos:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%%</span><span class="n">timeit</span>

<span class="n">NUM_CLASSES</span> <span class="o">=</span> <span class="mi">10</span>

<span class="n">db</span> <span class="o">=</span> <span class="nc">DatabaseNaive</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">create_table</span><span class="p">()</span>

<span class="n">loader</span> <span class="o">=</span> <span class="nc">LoaderNaive</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="n">loader</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">predictions</span><span class="p">)</span>

<span class="n">db</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

<span class="c1"># 5min 54s ± 6.42 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
</span></code></pre></div></div>

<h1 id="threads-sendo-úteis">Threads sendo úteis</h1>

<p>A estratégia de threads funciona nesse cenário, porque escrever em banco de dados depende muito de I/O, diferente de gerar amostras que é um processo que demanda muito tempo do processador.</p>

<p>Uma preocupação ao lidar com threads, é garantir que os objetos compartilhados sejam <a href="https://en.wikipedia.org/wiki/Thread_safety">thread safety</a>. Nesse caso, o objeto que precisa dessa garantia é a conexão de banco de dados, compartilhado entre as threads.</p>

<p>O conector de PostgreSQL para Python tem a opção de usar o <code class="language-plaintext highlighter-rouge">ConnectionPool</code>, que cria uma piscina de conexões disponíveis para serem alocadas. Dessa forma, cada thread pode utilizar uma conexão distinta, o que elimina os possíveis problemas de concorrência (<em>e.g.</em> condição de corrida, corrupção de dados). Para abstrair esse objeto, foi criada uma classe <code class="language-plaintext highlighter-rouge">DatabasePool</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DatabasePool</span><span class="p">(</span><span class="n">Database</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">connection_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
                 <span class="n">num_classes</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
                 <span class="n">num_threads</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="n">self</span><span class="p">.</span><span class="n">connection_url</span> <span class="o">=</span> <span class="n">connection_url</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_classes</span> <span class="o">=</span> <span class="n">num_classes</span>

        <span class="n">self</span><span class="p">.</span><span class="n">pool</span> <span class="o">=</span> <span class="nc">ConnectionPool</span><span class="p">(</span><span class="n">connection_url</span><span class="p">,</span>
                                   <span class="n">min_size</span><span class="o">=</span><span class="n">num_threads</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">create_table</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">with</span> <span class="n">self</span><span class="p">.</span><span class="n">pool</span><span class="p">.</span><span class="nf">connection</span><span class="p">()</span> <span class="k">as</span> <span class="n">conn</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="nf">_create_table</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">save_message</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">prediction</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>

        <span class="k">with</span> <span class="n">self</span><span class="p">.</span><span class="n">pool</span><span class="p">.</span><span class="nf">connection</span><span class="p">()</span> <span class="k">as</span> <span class="n">conn</span><span class="p">:</span>
            <span class="k">with</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
                <span class="n">cur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o">*</span><span class="n">self</span><span class="p">.</span><span class="nf">_build_insert</span><span class="p">(</span><span class="n">prediction</span><span class="p">))</span>
                <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
</code></pre></div></div>

<p>Estendendo a implementação <code class="language-plaintext highlighter-rouge">LoaderNaive</code> e adicionando a função <code class="language-plaintext highlighter-rouge">load_parallel</code>, é possível fazer a carga no banco de dados com múltiplas threads. A função <code class="language-plaintext highlighter-rouge">load_parallel</code> separa o DataFrame em pedaços, para serem posteriormente processados pelo método <code class="language-plaintext highlighter-rouge">load</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LoaderMultiThread</span><span class="p">(</span><span class="n">LoaderNaive</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">batch_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
                 <span class="n">db</span><span class="p">:</span> <span class="n">DatabasePool</span><span class="p">,</span>
                 <span class="n">num_threads</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="n">self</span><span class="p">.</span><span class="n">db</span> <span class="o">=</span> <span class="n">db</span>
        <span class="n">self</span><span class="p">.</span><span class="n">batch_size</span> <span class="o">=</span> <span class="n">batch_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_threads</span> <span class="o">=</span> <span class="n">num_threads</span>

    <span class="k">def</span> <span class="nf">load_parallel</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">):</span>

        <span class="n">dfs</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_make_slices</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>

        <span class="k">with</span> <span class="nc">ThreadPool</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_threads</span><span class="p">)</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
            <span class="n">p</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">load</span><span class="p">,</span> <span class="n">dfs</span><span class="p">)</span>
</code></pre></div></div>

<p>Diferentemente de gerar as amostras, nesse problema existe um ganho de desempenho com o uso de threads, o processo é executado em cerca de 1 minuto e 30 segundos ao invés de 6 minutos.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%%</span><span class="n">timeit</span>

<span class="n">NUM_THREADS</span> <span class="o">=</span> <span class="mi">48</span>
<span class="n">BATCH_SIZE</span> <span class="o">=</span> <span class="mi">1_000</span>

<span class="n">db</span> <span class="o">=</span> <span class="nc">DatabasePool</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">,</span> <span class="n">NUM_THREADS</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">create_table</span><span class="p">()</span>

<span class="n">loader</span> <span class="o">=</span> <span class="nc">LoaderMultiThread</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">db</span><span class="p">,</span> <span class="n">NUM_THREADS</span><span class="p">)</span>
<span class="n">loader</span><span class="p">.</span><span class="nf">load_parallel</span><span class="p">(</span><span class="n">predictions</span><span class="p">)</span>

<span class="n">db</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

<span class="c1"># 1min 32s ± 14.4 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
</span></code></pre></div></div>

<p>O ganho desse desempenho não veio do aumento de paralelismo, mas de possibilitar concorrência: quando uma thread precisa aguardar o retorno do banco de dados e fica bloqueada, o sistema operacional alterna para executar outra thread.</p>

<figure>
  <img src="/assets/images/paralelismo/threads.png" />
  <figcaption>Figura 2 – Threads se alternando</figcaption>
</figure>

<p>Por que usei 48 threads?  É difícil estimar esse número porque tem muitas variáveis: performance do banco de dados, latência de rede, operações concorrentes, etc. A estratégia foi experimentar diversos valores, portanto esse valor só vale para esse cenário.</p>

<p>Apesar de ser efetivo usar threads, essa estratégia faz mais sentido quando não é possível usar co-rotinas e tarefas. Usar interrupções para escalonar as threads talvez não seja o melhor cenário, além de envolver decisões complicadas como escolher o número ideal de threads.</p>

<h1 id="co-rotinas-mais-simples-e-rápido">Co-rotinas: mais simples e rápido</h1>

<p>O <code class="language-plaintext highlighter-rouge">pyscopg</code> implementa comunicação assíncrona com <code class="language-plaintext highlighter-rouge">asyncio</code>, que é um jeito mais elegante e nativo de resolver o problema da concorrência. O <code class="language-plaintext highlighter-rouge">asyncio</code> tem vários conceitos como tarefas, corotinas, etc – eu mesmo tenho uma compreensão superficial – então recomendo a <a href="https://docs.python.org/3/library/asyncio-task.html#coroutine">documentação</a> e <a href="https://realpython.com/python-async-features/">outros materiais</a>, caso o leitor queira entender melhor o que está sendo feito.</p>

<!-- Nem todo o ecossistema Python implementa esse recurso, então o uso de threads pode ser a única alternativa nesses cenários. -->

<p>Para esse cenário, tentei manter a interface de uso o mais parecido possível com a versão síncrona, mas usando a implementação assíncrona do <code class="language-plaintext highlighter-rouge">psycopg</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DatabaseAsync</span><span class="p">(</span><span class="n">Database</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">connection_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
                 <span class="n">num_classes</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="n">self</span><span class="p">.</span><span class="n">connection_url</span> <span class="o">=</span> <span class="n">connection_url</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_classes</span> <span class="o">=</span> <span class="n">num_classes</span>

    <span class="k">def</span> <span class="nf">create_table</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">conn</span> <span class="o">=</span> <span class="n">psycopg</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">connection_url</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">_create_table</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
        <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">get_conn</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">psycopg</span><span class="p">.</span><span class="n">AsyncConnection</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">connection_url</span><span class="p">)</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">save_message</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">,</span> <span class="n">prediction</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
        <span class="k">async</span> <span class="k">with</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
            <span class="k">await</span> <span class="n">cur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o">*</span><span class="n">self</span><span class="p">.</span><span class="nf">_build_insert</span><span class="p">(</span><span class="n">prediction</span><span class="p">))</span>

        <span class="k">await</span> <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">await</span> <span class="n">self</span><span class="p">.</span><span class="nf">get_conn</span><span class="p">().</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">LoaderAsync</code> é muito parecido com o <code class="language-plaintext highlighter-rouge">LoaderNaive</code>, processando todos os dados de uma vez ao invés de separá-los em batches menores. Ao invés de trabalhar com um número fixo de threads reaproveitáveis, uma tarefa é criada para cada amostra a ser inserida. As tarefas são baratas de criar e destruir, por isso é possível fazer essa implementação mais simples:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LoaderAsync</span><span class="p">(</span><span class="n">LoaderNaive</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">db</span><span class="p">:</span> <span class="n">DatabaseAsync</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">self</span><span class="p">.</span><span class="n">db</span> <span class="o">=</span> <span class="n">db</span>

    <span class="k">async</span> <span class="k">def</span> <span class="nf">load_async</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">):</span>

        <span class="n">conn</span> <span class="o">=</span> <span class="k">await</span> <span class="n">self</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="nf">get_conn</span><span class="p">()</span>

        <span class="k">async</span> <span class="k">with</span> <span class="n">conn</span><span class="p">:</span>
            <span class="k">async</span> <span class="k">with</span> <span class="n">asyncio</span><span class="p">.</span><span class="nc">TaskGroup</span><span class="p">()</span> <span class="k">as</span> <span class="n">tg</span><span class="p">:</span>
                <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="nf">iterrows</span><span class="p">():</span>
                    <span class="n">tg</span><span class="p">.</span><span class="nf">create_task</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="nf">save_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">row</span><span class="p">.</span><span class="nf">to_dict</span><span class="p">()))</span>
</code></pre></div></div>

<p>O <code class="language-plaintext highlighter-rouge">TaskGroup</code> oferece uma abstração similar ao <code class="language-plaintext highlighter-rouge">ThreadPool</code>, inicializando as tarefas e esperando o término de todas que foram criadas sob o contexto. Em termos de performance, foi ainda mais rápido que a solução multithread, com tempo de execução próximo de 25 segundos:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span> <span class="o">=</span> <span class="nc">DatabaseAsync</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">)</span>
<span class="n">db</span><span class="p">.</span><span class="nf">create_table</span><span class="p">()</span>

<span class="n">loader</span> <span class="o">=</span> <span class="nc">LoaderAsync</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
<span class="k">await</span> <span class="n">loader</span><span class="p">.</span><span class="nf">load_async</span><span class="p">(</span><span class="n">predictions</span><span class="p">)</span>
</code></pre></div></div>
<p>A solução continua sujeita ao GIL, os ganhos em relação ao multithread devem ser provenientes do escalonamento colaborativo, provavelmente mais efetivo para o problema. Entretanto, é bom ressaltar que boa parte do ecossistema Python não suporta <code class="language-plaintext highlighter-rouge">asyncio</code>, então nem sempre é possível optar por essa estratégia.</p>

<p>Apesar dos ganhos expressivos, estamos utilizando apenas um núcleo do processador. Separando o problema em  vários processos – temos uma execução assíncrona menos eficiente, conforme observado nos testes – mas que pode tirar melhor proveito do hardware disponível.</p>

<h1 id="processos-no-lugar-de-threads">Processos no lugar de Threads</h1>

<p>No primeiro exemplo, substituir processos por threads foi uma questão de trocar o contexto de <code class="language-plaintext highlighter-rouge">ThreadPool</code> para <code class="language-plaintext highlighter-rouge">Pool</code>. Nesse caso, fazer essa mudança gera um erro pouco claro para o usuário:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span> <span class="o">=</span> <span class="nc">DatabaseNaive</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">)</span>
<span class="n">loader</span> <span class="o">=</span> <span class="nc">LoaderMultiProcess</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">NUM_THREADS</span><span class="p">,</span> <span class="n">db</span><span class="p">)</span>
<span class="n">loader</span><span class="p">.</span><span class="nf">load_parallel</span><span class="p">(</span><span class="n">predictions</span><span class="p">)</span>

<span class="n">db</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

<span class="c1"># TypeError: no default __reduce__ due to non-trivial __cinit__
</span></code></pre></div></div>

<p>É comum ter esses erros estranhos ao trabalhar com múltiplos processos, normalmente são devidos à serialização. Lembre-se que os processos têm memória isolada, para compartilhar objetos entre eles, pode ser necessário traduzir em um formato binário transportável. O problema é que muitos objetos não podem ser transformados, uma conexão de banco de dados tem conexões de rede abertas que não podem ser migradas entre processos distintos.</p>

<p>Para lidar com essa limitação, existe um “truque” que consiste em atrasar a criação do objeto complexo. Essa é a diferença da classe <code class="language-plaintext highlighter-rouge">DatabaseLazy</code> para <code class="language-plaintext highlighter-rouge">DatabaseNaive</code>, o construtor da <code class="language-plaintext highlighter-rouge">DatabaseLazy</code> não cria a conexão no construtor:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DatabaseLazy</span><span class="p">(</span><span class="n">Database</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">connection_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
                 <span class="n">num_classes</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">(</span><span class="n">connection_url</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">conn</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">_get_conn</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>

        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">conn</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">conn</span> <span class="o">=</span> <span class="n">psycopg</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">connection_url</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">conn</span>

    <span class="k">def</span> <span class="nf">create_table</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">_create_table</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="nf">_get_conn</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">save_message</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">prediction</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>

        <span class="n">conn</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_get_conn</span><span class="p">()</span>

        <span class="k">with</span> <span class="n">self</span><span class="p">.</span><span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
            <span class="n">cur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o">*</span><span class="n">self</span><span class="p">.</span><span class="nf">_build_insert</span><span class="p">(</span><span class="n">prediction</span><span class="p">))</span>

        <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">_get_conn</span><span class="p">().</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>A conexão só é criada na primeira chamada de <code class="language-plaintext highlighter-rouge">_get_conn</code>, antes disso é um objeto <code class="language-plaintext highlighter-rouge">None</code>. A cópia do objeto <code class="language-plaintext highlighter-rouge">DatabaseLazy</code> é feita antes dessa primeira chamada, na abertura do <code class="language-plaintext highlighter-rouge">Pool</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">LoaderMultiProcess</span><span class="p">(</span><span class="n">LoaderNaive</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span>
                 <span class="n">batch_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
                 <span class="n">num_threads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
                 <span class="n">db</span><span class="p">:</span> <span class="n">DatabaseLazy</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>

        <span class="n">self</span><span class="p">.</span><span class="n">db</span> <span class="o">=</span> <span class="n">db</span>
        <span class="n">self</span><span class="p">.</span><span class="n">batch_size</span> <span class="o">=</span> <span class="n">batch_size</span>
        <span class="n">self</span><span class="p">.</span><span class="n">num_threads</span> <span class="o">=</span> <span class="n">num_threads</span>

    <span class="k">def</span> <span class="nf">load_parallel</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">df</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">):</span>

        <span class="n">dfs</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_make_slices</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>

        <span class="k">with</span> <span class="nc">Pool</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">num_threads</span><span class="p">)</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
            <span class="n">p</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">load</span><span class="p">,</span> <span class="n">dfs</span><span class="p">)</span>
</code></pre></div></div>

<p>Usando <code class="language-plaintext highlighter-rouge">LoaderMultiProcess</code> temos o melhor tempo de execução, ficando abaixo de 10 segundos em média, estamos falando de uma solução ~41,11 vezes mais rápida que a solução inicial:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%%</span><span class="n">timeit</span>
<span class="kn">import</span> <span class="n">os</span>

<span class="n">db_temp</span> <span class="o">=</span> <span class="nc">DatabaseNaive</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">)</span>
<span class="n">db_temp</span><span class="p">.</span><span class="nf">create_table</span><span class="p">()</span>
<span class="n">db_temp</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

<span class="n">db</span> <span class="o">=</span> <span class="nc">DatabaseLazy</span><span class="p">(</span><span class="n">CONNECTION_URL</span><span class="p">,</span> <span class="n">NUM_CLASSES</span><span class="p">)</span>
<span class="n">loader</span> <span class="o">=</span> <span class="nc">LoaderMultiProcess</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">NUM_THREADS</span><span class="o">*</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="nf">cpu_count</span><span class="p">()</span><span class="o">//</span><span class="mi">2</span><span class="p">),</span> <span class="n">db</span><span class="p">)</span>
<span class="n">loader</span><span class="p">.</span><span class="nf">load_parallel</span><span class="p">(</span><span class="n">predictions</span><span class="p">)</span>

<span class="n">db</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

<span class="c1"># 8.61 s ± 13.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
</span></code></pre></div></div>
<p>Faz sentido explorar essa abordagem quando há muitos núcleos disponíveis e os ganhos do paralelismo sobrepujam a eficiência das co-rotinas. Em outras linguagens, as tarefas normalmente podem ser processadas paralelamente, sem demandar esse trabalho extra.</p>

<p>Para quem tiver curiosidade, os códigos dos experimentos estão <a href="https://github.com/gdarruda/process-threads-tasks-samples">nesse repositório</a>.</p>

<h2 id="alternativas-e-futuro">Alternativas e futuro</h2>

<p>A maioria das linguagens foi criada sem pensar em concorrência e execução assíncrona, o que acaba em soluções não ideais para o problema. A presença do GIL no Python, adiciona mais camadas de complexidade para uma questão que já é inerentemente difícil em quase todos os ambientes de desenvolvimento.</p>

<p>A ideia de trabalhar com múltiplos processos é muito parecido com computação distribuída, o que faz o <a href="https://spark.apache.org/docs/latest/api/python/index.html">PySpark</a> parecer uma alternativa interessante em um primeiro momento. Uso bastante e gosto quando a solução pode ser escrita em  <a href="https://spark.apache.org/sql/">Spark SQL</a>, mas não faz sentido usar exclusivamente para paralelizar código Python devido ao <em>overhead</em> de integração com a JVM.</p>

<p>O <a href="https://www.dask.org">Dask</a> parece ser a melhor alternativa, se a ideia é paralelizar código Python e até distribuí-lo. Não tenho experiência com o framework, mas ele ainda é uma abstração complexa para contornar algo que a maioria das linguagens suporta nativamente.</p>

<p>Dado esse cenário, fico ansioso para acompanhar como será a adoção do ecossistema para execução sem o GIL. Existem muitas iniciativas(<a href="https://pypy.org">1</a>, <a href="https://github.com/facebookincubator/cinder">2</a> e <a href="https://devblogs.microsoft.com/python/python-311-faster-cpython-team/">3</a>) para acelerar o Python, mas a remoção do GIL depende de uma adoção pelos usuários, frameworks e bibliotecas para vingar.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Discutindo as opções para programação paralela e concorrente em Python]]></summary></entry><entry><title type="html">Código limpo, positivismo e fotografia</title><link href="/2024/06/26/codigo-limpo-positivismo-fotografia.html" rel="alternate" type="text/html" title="Código limpo, positivismo e fotografia" /><published>2024-06-26T00:00:00+00:00</published><updated>2024-06-26T00:00:00+00:00</updated><id>/2024/06/26/codigo-limpo-positivismo-fotografia</id><content type="html" xml:base="/2024/06/26/codigo-limpo-positivismo-fotografia.html"><![CDATA[<p>Os conhecimentos em engenharia de software são um requisito universal para qualquer tipo de programador, especialmente os mais experientes. Diferente dos <a href="https://x.com/mxcl/status/608682016205344768?lang=en">desafios de programação</a>, não há muita discussão sobre a relevância da engenharia de software no dia-a-dia. Há um consenso sobre a importância de escrever um código que seja “limpo”: não basta funcionar, precisa ser fácil de evoluir e sustentar. A questão complicada, é como definir e construir código limpo.</p>

<p>Imagino que a maioria aceite que existe um grau de subjetividade no que tange qualidade de código. Não existe um “ideal platônico” de código, que pode ser criado sistematicamente seguindo uma série de passos. Mas o quanto a engenharia de software pode nos ajudar a reduzir essa subjetividade?</p>

<h2 id="o-positivismo-da-engenharia-de-software">O positivismo da engenharia de software</h2>

<p>As várias <a href="https://en.wikipedia.org/wiki/Software_engineering#Definition">definições de engenharia de software</a>, normalmente destacam seu caráter sistemático e científico, como a definição da IEEE por exemplo:</p>

<blockquote>
  <p>The systematic application of scientific and technological knowledge, methods, and experience to the design, implementation, testing, and documentation of software</p>
</blockquote>

<p>Quando se discute as limitações da engenharia de software, normalmente é levado em conta o fato de ser uma nova área de conhecimento. Aos poucos, esse rol de métodos e conhecimentos da engenharia de software está sendo construído, os problemas atuais são uma questão de maturidade.</p>

<p>É uma perspectiva <a href="https://en.wikipedia.org/wiki/Positivism">positivista</a> da situação: seja pela ideia de seguir uma metodologia científica para construção do conhecimento real (positivo), seja pela perspectiva de estarmos nos primeiros passos de uma área  <a href="https://en.wikipedia.org/wiki/Auguste_Comte#/media/File:Theory_of_Science_by_A._Comte.svg">complexa e específica</a> na hierarquia de Comte.</p>

<p>Uma área próxima, que evoluiu de acordo com o positivismo, é o design de banco de dados. Existiam várias abordagens para o problema, mas o modelo de <a href="https://en.wikipedia.org/wiki/Relational_model">representação relacional</a> se consolidou nos anos 70 e os conceitos se mantiveram até os dias de hoje. Surgiram alternativas aos bancos relacionais, mas o aspecto interessante é como dominamos o seu uso: os métodos são bem definidos, já vi modelos lógicos criados na década de 80 que eu desenharia da mesma forma hoje.</p>

<p>A minha percepção no início da carreira – fim dos anos 2000, início dos anos 2010 – era que a orientação a objetos seguiria o mesmo caminho do modelo relacional. Pode ter sido ser sido a ingenuidade de um iniciante, mas eu imaginava que existiria um análogo às <a href="https://en.wikipedia.org/wiki/Database_normalization">regras de normalização</a> para modelagem de classes.</p>

<p>Alguém lembra de <a href="https://en.wikipedia.org/wiki/Unified_Modeling_Language">UML</a>? A ideia era que poderíamos ter uma documentação tão precisa, que alguém poderia traduzir em código em uma fábrica de software do outro lado do mundo. Uma solução temporária, porque logo mais seria possível gerar <a href="https://www.ibm.com/support/pages/how-create-uml-code-transformation-rsa">código automaticamente</a> a partir de UML, alcançado o sonhado low-code que <a href="/2024/01/20/low-code-dilema">já discuti</a> em outra oportunidade.</p>

<p>A orientação a objetos foi largamente adotada no desenvolvimento de sistemas e trouxe avanços, mas não se chegou a um método rigoroso e sistemático. Pode-se enxergar como um erro de percurso – que podemos evoluir a orientação a objetos, misturar com outros paradigmas, criar um completamente novo – ou entender que não existe um percurso.</p>

<p>É completamente razoável, a ideia de sistematizar uma área derivada da computação, mas talvez a parcela de computação do problema que queremos resolver seja muito pequena. A engenharia de software, não estaria sofrendo de <a href="https://en.wikipedia.org/wiki/Physics_envy">inveja da física</a>?</p>

<h2 id="software-como-processo-criativo">Software como processo criativo</h2>

<p>O livro Clean Code era uma recomendação padrão sobre  engenharia de software, mas nos últimos anos tem sido questionado [<a href="https://qntm.org/clean">1</a>, <a href="https://overreacted.io/goodbye-clean-code/">2</a>, <a href="https://theaxolot.wordpress.com/2024/05/08/dont-refactor-like-uncle-bob-please/">3</a>], discussões sobre Uncle Bob e o livro tendem a ser acaloradas  [<a href="https://www.reddit.com/r/csharp/comments/1cwbv37/is_clean_code_dead/">1</a>, <a href="https://www.reddit.com/r/programming/comments/hhlvqq/its_probably_time_to_stop_recommending_clean_code/">2</a>, <a href="https://news.ycombinator.com/item?id=22022466">3</a>, <a href="https://news.ycombinator.com/item?id=34966137">4</a>, <a href="https://www.hillelwayne.com/post/uncle-bob/">…</a>].</p>

<p>Não quero discutir o mérito do livro em si – se ele ficou datado ou se nunca foi bom para início de conversa – mas pensar na forma que o consumimos. Seria um debate menos polêmico, se tratássemos desenvolvimento como um processo criativo, mais parecido com fotografia<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> que construção de pontes.</p>

<p>O Clean Code não deve ser lido como um guia de práticas, mas uma obra influente na época em que foi escrita. Ler como formação de repertório, para entender como as coisas eram feitas e como chegamos aqui, da mesma forma que tratamos os clássicos do cinema e literatura por exemplo.</p>

<p>No capítulo introdutório, Uncle Bob até faz uma analogia de programação com o processo criativo.</p>

<blockquote>
  <p>Considere esse livro como uma descrição da Escola de Código Limpo da Object Mentor. As técnicas e ensinamentos são a maneira pela qual praticamos nossa arte. Estamos dispostos a alegar que se você seguir esses ensinamentos, desfrutará de benefícios que também aproveitamos e aprenderá a escrever códigos limpos e profissionais. Mas não pense que estamos 100% “certos”. Provavelmente há outras escolas e mestres que têm tanto para oferecer quanto nós. O correto seria que você aprendesse com elas também.</p>
</blockquote>

<p>Por outro lado, o restante do livro tem outro tom. São muitas afirmações assertivas e prescritivas de como as coisas devem ser, conclusões unilaterais sobre aspectos subjetivos. Um exemplo dessa postura, é essa afirmação sobre parâmetros das funções:</p>

<blockquote>
  <p>A quantidade ideal de parâmetros para uma função é zero (nulo). Depois vem um (mônade), seguido de dois (díade). Sempre que possível devem-se evitar três (tríade) parâmetros. Para mais de três (políade) deve ter um motivo muito especial – mesmo assim não devem ser usados.</p>
</blockquote>

<p>É uma preferência do autor, mas escrita como um lema matemático. Uma opinião que está no contexto da linguagem Java de 2008: sem parâmetros com valor padrão, orientada a objetos e sem suporte a funções de alta ordem. A escrita reflete a visão positivista, apesar de não ser um conhecimento de caráter científico.</p>

<p>Pessoalmente, acho (bem) estranha essa aversão a parâmetros, mesmo para a época em que foi escrito o livro. Posso citar vários argumentos sobre o porquê discordo, mas é possível encerrar essa discussão? A proposição não é sustentada por um experimento ou algum outro tipo de evidência científica, mas meus argumentos contrários também não.</p>

<p>Mesmo que você também discorde dessa ideia, ainda é interessante conhecê-la. Dada a influência do livro, muitos códigos foram criados seguindo essa premissa e novos seguirão sendo criados, independente da opinião individual sobre o tema.</p>

<p>Seria melhor, se tratássemos esse tipo de afirmação mais como “regras” de composição da fotografia (<em>e.g.</em> <a href="https://www.adobe.com/br/creativecloud/photography/discover/rule-of-thirds.html#:~:text=O%20que%20é%20a%20regra,fotos%20atraentes%20e%20bem%20estruturadas.">regra dos terços</a>, <a href="https://www.epics.com.br/blog/o-que-e-espaco-negativo-na-fotografia">espaço negativo</a> e <a href="https://annphoto.net/fotografia/quais-sao-as-linhas-principais-e-como-usa-los-em-fotos/">linhas principais</a>), do que como um método de engenharia.</p>

<p>A regra dos terços tem uma <a href="https://petapixel.com/2024/06/27/the-true-photographic-history-of-the-rule-of-thirds-and-golden-mean/">longa história</a>, originando-se na <a href="https://en.wikipedia.org/wiki/Pictorialism">escola pictorialista</a> dos primórdios da fotografia. Voltou a tona com a popularização das câmeras fotográficas entre fotógrafos amadores – é uma regra objetiva e prática, assim como a questão de parâmetros – que a torna atraente para iniciantes. O reducionismo da regra já era era questionado na década de 50, pelo famoso fotógrafo <a href="https://en.wikipedia.org/wiki/Henri_Cartier-Bresson">Henri Catier-Bresson</a>:</p>

<blockquote>
  <p>In applying the Golden Rule, the only pair of compasses at the photographer’s disposal is his own pair of eyes. Any geometrical analysis, any reducing of the picture to a schema, can be done only (because of its very nature) after the photograph has been taken, developed, and printed — and then it can be used only for a postmortem examination of the picture. I hope we will never see the day when photo shops sell little schema grills to clamp onto our viewfinders; and the Golden Rule will never be found etched on our ground glass.</p>
</blockquote>

<p>Não existe uma ciência por trás das técnicas de composição, mas também não existe a ambição de ser algo científico. Sem essa pretensão, os debates ficam mais leves e não se espera uma aplicação sistemática sem reflexão. Um fotógrafo experiente conhece as “regras”, mas as quebra quando necessário.</p>

<figure>
  <img src="/assets/images/fotografia/stravinsky-original.jpg" />
  <img src="/assets/images/fotografia/stravinsky-final.jpg" />
  <figcaption>Figura 1 – Arnold Newman ignorando regras ao fazer esse famoso retrato do Stravinsky</figcaption>
</figure>

<p>O Clean Code é um conteúdo indiscutivelmente importante pela relevância – mas que deve ser consumido com parcimônia e olhar crítico – e nós programadores não estamos acostumados com isso.</p>

<h2 id="diferentes-escolas">Diferentes escolas</h2>

<p>É uma situação complicada, quando sua opinião vai em direção oposta a alguém renomado. Quem sou eu para discordar do Uncle Bob, autor de vários livros influentes? Talvez, essa anedota do encontro do fotógrafo <a href="https://en.wikipedia.org/wiki/William_Eggleston">Willian Eggleston</a> com o supracitado Henri Catier-Bresson, tenha algo a nos ensinar:</p>

<blockquote>

<strong>William Eggleston</strong>: You know, I had a meeting with him [Henri Cartier-Bresson], one in particular, it was at this party in Lyon. Big event, you know. I was seated with him and a couple of women. You’ll never guess what he said to me.<br />
<br />
<strong>Drew Barrymore</strong>: What?<br />
<br />
<strong>William Eggleston</strong>: “William, color is bullshit.” End of conversation. Not another word. And I didn’t say anything back. What can one say? I mean, I felt like saying I’ve wasted a lot of time. As this happened, I’ll tell you, I noticed across the room this really beautiful young lady, who turned out to be crazy. So I just got up, left the table, introduced myself, and I spent the rest of the evening talking to her, and she never told me color was bullshit.

</blockquote>

<figure>
  <img src="/assets/images/fotografia/the-black-keys-delta-kream.jpg" />
  <figcaption>Figura 2 – Capa do disco Delta Kream dos Black Keys, usando a foto de Willian Eggleston de uma loja no Mississippi. A foto funcionaria sem cores? </figcaption>
</figure>

<p>Esse tipo de dissidência é comum no processo criativo, é pressuposto e importante que surjam diversas linhas de pensamento. Em computação, estamos sempre procurando convergir para uma solução ótima, é desconfortável aceitar que existam múltiplos caminhos sem um vencedor claro. É importante estar confortável com divergências, porque qualidade de código é sobre os outros.</p>

<p>Se formos definir o <a href="https://www.youtube.com/watch?v=ug8XX2MpzEw&amp;t=2224s">principal motivo</a> para fazer códigos limpos, é que seja possível modificá-lo no futuro. Um código que nunca será alterado, é indiferente a essa questão. Em geral, a ideia é que várias pessoas consigam mexer no código, não somente o autor. Logo, a sua ideia de código limpo precisa ser compartilhada com outras pessoas que trabalharão nele, senão ela perde o sentido.</p>

<p>Quando comecei a trabalhar na área de dados, passei a lidar com profissionais e problemas mais heterogêneos e isso me ajudou a enxergar a importância do contexto nas discussões sobre código. Por exemplo, o que é código limpo para esse cenários:</p>

<ol>
  <li>
    <p>cientistas de dados acostumados com <a href="https://en.wikipedia.org/wiki/Array_programming">programação vetorial</a>, preparando dados para entrada em uma rede neural;</p>
  </li>
  <li>
    <p>analista de negócio, querendo apenas substituir uma planilha Excel cheia de macros por um Jupyter Notebook;</p>
  </li>
  <li>
    <p>desenvolvedor back-end acostumado com conceitos como DTOs, polimorfismo, interfaces, injeção de dependência, etc.</p>
  </li>
</ol>

<p>A melhor solução para cada problema, provavelmente é um <em>anti-pattern</em> para os demais. Por exemplo, normalmente prefiro manter códigos repetidos ao invés de usar polimorfismo no cenário (2), o que não faria sentido para o cenário (3).</p>

<p>Obviamente tenho minhas preferências, mas elas não sobrepõem as necessidades dos  “clientes” do meu código, as pessoas que precisarão mantê-lo. Não existe código limpo <em>a priori</em> – o que está sendo feito e para quem – definem o que é ser limpo.</p>

<h2 id="não-se-abstenha">Não se abstenha</h2>

<p>O meu argumento é que devemos conviver melhor com diversas perspectivas de código limpo, mas há um risco em adotar uma postura muito flexível. Abster-se de uma decisão e acabar com um código insustentável para manter.</p>

<p>É importante identificar as brigas corretas, afinal discussões podem ser cansativas e gerar desgastes, não queremos gastar energia com debates infrutíferos. Infelizmente, não sei dizer um modo de diferenciar os cenários: esse código é ruim e insustentável ou é simplesmente diferente do que estou acostumado?</p>

<p>Só posso sugerir criação de repertório para responder essa pegunta, explorar problemas e tecnologias diferentes com a cabeça aberta. Dar-se um tempo para se sentir confortável, tomar cuidado para não adotar posições dogmáticas prematuras. Por fim, o mais difícil: aceitar que nem sempre iremos gostar.</p>

<p>Willian Eggleston pôde seguir com sua fotografia colorida, a despeito da opinião de Henri Cartier-Bresson sobre ela. Você também pode seguir suas preferências em seu projeto pessoal, mas não em um contexto profissional. Assim como artistas fazem concessões em trabalhos comerciais, devemos fazer o mesmo pelo time.</p>

<p>Sugeri inspiração no processo criativo para desenvolvimento de software, mas essa é uma diferença fundamental: o seu código deve ser limpo para os outros, não para você. Seja lá, o que código limpo significa.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Escrita literária provavelmente seria uma analogia melhor, mas conheço mais sobre fotografia. Meu um único traço de personalidade é ter fotografia como hobby, todo o resto é ser um programador. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Refletindo sobre código limpo]]></summary></entry><entry><title type="html">Como ler Kafka?</title><link href="/2024/05/05/kafka-leitura.html" rel="alternate" type="text/html" title="Como ler Kafka?" /><published>2024-05-05T00:00:00+00:00</published><updated>2024-05-05T00:00:00+00:00</updated><id>/2024/05/05/kafka-leitura</id><content type="html" xml:base="/2024/05/05/kafka-leitura.html"><![CDATA[<p>Já escrevi sobre as dores de se trabalhar com <a href="/2023/03/04/engenharia-dados.html">engenharia de dados</a> e muitas delas aparecem quando se utiliza Kafka. Não é uma crítica ao software – tem ótimas sacadas de design para escalabilidade como o <a href="https://kafka.apache.org/documentation/#maximizingefficiency">princípio de zero cópias</a>, <a href="https://kafka.apache.org/documentation/#design_filesystem">usar diretamente o  filesystem</a>, <a href="https://kafka.apache.org/documentation/#design_pull">pull ao invés de push</a> e <a href="https://kafka.apache.org/documentation/#design_loadbalancing">load balancing</a> – mas tudo isso traz uma complexidade intrínseca, que cobra um preço dos usuários.</p>

<p><img src="/assets/images/kafka-leitura/DSCF2043.jpg" alt="" /></p>

<p>Ler dados de um tópico Kafka costuma ser bem rápido, rápido demais inclusive. Ao consumir um tópico, o problema costuma variar entre dois extremos: baixo <em>throughput</em>, porque a aplicação quer manter um consumo <em>exactly-once</em> sem concorrência/paralelismo, ou <em>overflow</em> do consumidor porque ela está lendo mais rápido do que consegue processar.</p>

<p>Quando o assunto Kafka é abordado, discute-se muito as questões de infra do ambiente (<em>e.g.</em> número de partições, réplicas e picos de uso). O que pretendo abordar nesse post, é o lado de desenvolvimento, como construir e escalar uma aplicação que consome os dados a partir de um tópico.</p>

<p>Primeiro, farei uma breve introdução a eventos e conceitos básicos importantes para se trabalhar com Kafka. Depois, propor um problema hipotético e entender as abordagens possíveis. Por fim, implementar as possíveis soluções para tangibilizar a discussão em números e código.</p>

<h2 id="por-que-eventos">Por que eventos?</h2>

<p>A ideia de eventos se popularizou bastante nos últimos anos e um cenário em que ela funciona muito bem, é o problema de mensageria: quando algo acontece no sistema, uma compra confirmada em um sistema de e-commerce por exemplo, é necessário notificar o usuário via e-mail.</p>

<p>A solução imediata é fazer uma integração síncrona via API, confirmando a compra após finalizar todas etapas, incluindo o envio do e-mail.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 744.5 232.4615478515625" width="700" height="200">
  <!-- svg-source:excalidraw -->
  
  <defs>
    <style class="style-fonts">
      @font-face {
        font-family: "Virgil";
        src: url("https://excalidraw.com/Virgil.woff2");
      }
      @font-face {
        font-family: "Cascadia";
        src: url("https://excalidraw.com/Cascadia.woff2");
      }
      @font-face {
        font-family: "Assistant";
        src: url("https://excalidraw.com/Assistant-Regular.woff2");
      }
    </style>
    
  </defs>
  <g stroke-linecap="round" transform="translate(10 115.3233642578125) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C8.86 9.65, 15.43 4.2, 16.91 1.45 M4.44 15.79 C7.59 12.42, 12.34 7.87, 16.91 1.45 M4.57 27.83 C12.53 17.52, 18.04 11.45, 27.54 1.41 M4.57 27.83 C11.07 20.45, 16.43 14.27, 27.54 1.41 M4.71 39.87 C12.52 29.24, 18.49 20.53, 38.17 1.38 M4.71 39.87 C16.81 24.4, 30.7 10.97, 38.17 1.38 M3.53 53.42 C20.88 33.75, 34.23 19.6, 48.8 1.34 M3.53 53.42 C20.94 34.12, 38.59 11.96, 48.8 1.34 M6.94 61.68 C18.71 49.01, 32.25 36.15, 59.43 1.31 M6.94 61.68 C22.13 42.65, 37.89 24.68, 59.43 1.31 M13.64 66.18 C34.55 45.58, 53.64 20.92, 70.06 1.27 M13.64 66.18 C30.06 46.48, 46.49 27.15, 70.06 1.27 M24.27 66.14 C35.04 52.41, 46.86 35.61, 80.03 1.99 M24.27 66.14 C41.96 44.78, 59.9 24.81, 80.03 1.99 M34.9 66.11 C55.44 42.94, 74.62 19.79, 90.66 1.96 M34.9 66.11 C57.06 41.59, 79.07 15.6, 90.66 1.96 M45.53 66.08 C59.29 52.51, 71.58 34.54, 101.29 1.92 M45.53 66.08 C60.58 48.85, 75.05 32.65, 101.29 1.92 M55.5 66.8 C69.72 49.87, 88.67 29.5, 111.92 1.89 M55.5 66.8 C69.11 53.8, 80.28 38.34, 111.92 1.89 M66.13 66.76 C77.3 53.14, 90.7 37.6, 122.55 1.86 M66.13 66.76 C88.08 40.66, 111.7 16, 122.55 1.86 M76.76 66.73 C95.33 43.83, 114.6 23.94, 133.18 1.82 M76.76 66.73 C90.39 50.08, 105.47 33.97, 133.18 1.82 M87.39 66.69 C101.29 53.46, 114.3 36.05, 139.22 7.07 M87.39 66.69 C102.65 48.95, 118.39 29.64, 139.22 7.07 M98.02 66.66 C109.59 53, 118.27 44.19, 143.29 14.58 M98.02 66.66 C113.53 48.79, 129.32 29.31, 143.29 14.58 M108.65 66.62 C119.66 52.69, 131.01 40.19, 143.42 26.62 M108.65 66.62 C115.66 57.45, 122.56 49.98, 143.42 26.62 M118.62 67.34 C125.34 59.63, 131.75 49.3, 142.9 39.42 M118.62 67.34 C123.26 61.45, 128.86 55.82, 142.9 39.42" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C57.7 -2.46, 99.35 -2.85, 125.25 0 M15.75 0 C57.22 0.07, 100.63 -0.03, 125.25 0 M125.25 0 C136.35 0.82, 140.49 4.11, 141 15.75 M125.25 0 C136.1 2.18, 141.13 4.56, 141 15.75 M141 15.75 C139.35 25.64, 143.11 38.28, 141 47.25 M141 15.75 C140.92 24.74, 141.45 33.45, 141 47.25 M141 47.25 C139.07 59.13, 136.45 61.71, 125.25 63 M141 47.25 C142.62 59.57, 134 64.44, 125.25 63 M125.25 63 C90.82 64.73, 54.62 62.57, 15.75 63 M125.25 63 C96.44 62, 68.69 62.1, 15.75 63 M15.75 63 C4.59 61.78, -0.64 59.68, 0 47.25 M15.75 63 C3.84 64.96, 0.28 56.49, 0 47.25 M0 47.25 C-0.09 36.55, 1.12 28.64, 0 15.75 M0 47.25 C0.49 36.71, 0.08 24.86, 0 15.75 M0 15.75 C-0.18 5.13, 6.1 -1.35, 15.75 0 M0 15.75 C0.92 4.73, 5.15 -1.4, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(37.68000030517578 121.8233642578125) rotate(0 42.81999969482422 25)"><text x="42.81999969482422" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Sistema </text><text x="42.81999969482422" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Vendas</text></g><g stroke-linecap="round"><g transform="translate(163 146.51735208528498) rotate(0 38.5 -0.07279846264141465)"><path d="M-0.24 -0.76 C12.56 -0.56, 65.02 0.81, 77.74 0.76 M1.83 1.45 C14.45 1.37, 64.41 -0.61, 77.18 -0.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(163 146.51735208528498) rotate(0 38.5 -0.07279846264141465)"><path d="M53.97 8.4 C61.51 6.17, 73.07 -0.16, 77.18 -0.88 M53.97 8.4 C58.57 6.38, 63.58 3.42, 77.18 -0.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(163 146.51735208528498) rotate(0 38.5 -0.07279846264141465)"><path d="M53.43 -8.69 C61.11 -4.46, 72.88 -4.32, 77.18 -0.88 M53.43 -8.69 C58.17 -6.84, 63.29 -5.93, 77.18 -0.88" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(252 80.4615478515625) rotate(0 117 71)"><path d="M32 0 C81.9 2.23, 130.66 2.71, 202 0 M32 0 C74.94 1.06, 116.43 0.03, 202 0 M202 0 C224.26 -0.53, 233.07 10.79, 234 32 M202 0 C221.09 0.53, 233.18 9, 234 32 M234 32 C233 59.28, 236.1 85.71, 234 110 M234 32 C234.12 58.98, 233.47 86.7, 234 110 M234 110 C232.42 133.28, 224.04 142.61, 202 142 M234 110 C232.06 131.01, 222.53 140.72, 202 142 M202 142 C159.64 142.8, 120.1 142.7, 32 142 M202 142 C142.06 143.47, 83.64 143.78, 32 142 M32 142 C10.59 143.46, 1.4 130.4, 0 110 M32 142 C8.82 142.29, -1.71 130.46, 0 110 M0 110 C-0.61 89.3, 0.02 71.24, 0 32 M0 110 C0.07 81.25, -0.62 54.26, 0 32 M0 32 C-1.92 9.43, 11.54 -0.1, 32 0 M0 32 C-1.46 8.86, 10.29 0.41, 32 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(273 94.8615478515625) rotate(0 96.99998474121094 57.941333333333304)"><text x="0" y="13.321333333333328" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="29.875999999999987" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="46.430666666666646" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="62.9853333333333" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="79.53999999999996" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="96.09466666666663" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="112.64933333333327" font-family="Cascadia, Segoe UI Emoji" font-size="13.795555555555548px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(593.5 112.9615478515625) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C8.15 9.69, 14.53 5.43, 16.91 1.45 M4.44 15.79 C9.25 10.04, 14.39 4.5, 16.91 1.45 M4.57 27.83 C13.37 18.08, 21.89 6.07, 27.54 1.41 M4.57 27.83 C12.96 19.53, 20.49 10.78, 27.54 1.41 M4.71 39.87 C15.55 28.35, 26.88 14.83, 38.17 1.38 M4.71 39.87 C13.31 29.72, 21.01 19.98, 38.17 1.38 M3.53 53.42 C15.36 38.46, 31.09 24.46, 48.8 1.34 M3.53 53.42 C17.38 38.41, 29.65 21.92, 48.8 1.34 M6.94 61.68 C22.8 42.54, 40.83 22.73, 59.43 1.31 M6.94 61.68 C18.96 47.38, 31.23 33.32, 59.43 1.31 M13.64 66.18 C31.87 48.01, 50.28 25.8, 70.06 1.27 M13.64 66.18 C27.62 51.25, 42.68 33.61, 70.06 1.27 M24.27 66.14 C41.54 42.65, 62.32 22.95, 80.03 1.99 M24.27 66.14 C37.46 49.77, 50.16 35.69, 80.03 1.99 M34.9 66.11 C47.83 50.06, 60.74 35.04, 90.66 1.96 M34.9 66.11 C57.58 40.45, 79.17 16.35, 90.66 1.96 M45.53 66.08 C57.3 54.02, 70.17 40.33, 101.29 1.92 M45.53 66.08 C63.73 46.4, 80.15 25.82, 101.29 1.92 M55.5 66.8 C73.54 44.27, 92.25 26.01, 111.92 1.89 M55.5 66.8 C76.7 40.15, 99.58 15.21, 111.92 1.89 M66.13 66.76 C83.4 48.6, 97.07 29.24, 122.55 1.86 M66.13 66.76 C84.01 46.86, 101.29 26.59, 122.55 1.86 M76.76 66.73 C96.18 44.03, 117.27 21.26, 133.18 1.82 M76.76 66.73 C97.15 43.57, 118.41 17.95, 133.18 1.82 M87.39 66.69 C100.45 53.99, 113 39.11, 139.22 7.07 M87.39 66.69 C104.02 45.06, 122.6 25.29, 139.22 7.07 M98.02 66.66 C110.22 56.67, 117.52 42.71, 143.29 14.58 M98.02 66.66 C110.38 51.16, 123.59 37.08, 143.29 14.58 M108.65 66.62 C120.34 50.93, 135.8 35.3, 143.42 26.62 M108.65 66.62 C121.46 51.39, 134.84 36.2, 143.42 26.62 M118.62 67.34 C125.91 60.36, 133.26 52.09, 142.9 39.42 M118.62 67.34 C126.46 56.34, 135.81 47.63, 142.9 39.42" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C51.29 2.67, 87.1 0.93, 125.25 0 M15.75 0 C39.12 -0.41, 63.93 -0.13, 125.25 0 M125.25 0 C136.54 0.11, 139.41 6.67, 141 15.75 M125.25 0 C134.66 2.23, 139.16 5.92, 141 15.75 M141 15.75 C142.95 25.07, 141.19 39.06, 141 47.25 M141 15.75 C140.69 23.32, 141.26 31.95, 141 47.25 M141 47.25 C142.38 58.88, 137.34 63.08, 125.25 63 M141 47.25 C141.52 59.33, 135.1 64.14, 125.25 63 M125.25 63 C89.67 61.35, 56.19 62.89, 15.75 63 M125.25 63 C102.08 63.61, 78.72 61.87, 15.75 63 M15.75 63 C5.6 62.55, 1.38 58.99, 0 47.25 M15.75 63 C7.32 63.32, 2.26 56.07, 0 47.25 M0 47.25 C0.76 33.51, 0.19 22.93, 0 15.75 M0 47.25 C0.71 35.33, 0.11 23.02, 0 15.75 M0 15.75 C-0.1 4.8, 7.11 -0.46, 15.75 0 M0 15.75 C1.62 6.79, 3.99 1.87, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(610.4599990844727 131.9615478515625) rotate(0 53.540000915527344 12.5)"><text x="53.540000915527344" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Mensageria</text></g><g stroke-linecap="round"><g transform="translate(502.0000000000002 149.1158370359096) rotate(0 38.999999999999886 0.4969342584564762)"><path d="M-0.95 0.48 C12.17 0.77, 66.03 0.66, 79.12 0.68 M0.75 -0.31 C13.78 0.24, 66.12 1.72, 78.77 2.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(502.0000000000002 149.1158370359096) rotate(0 38.999999999999886 0.4969342584564762)"><path d="M55.05 9.95 C63.79 9.39, 70.32 4.24, 78.77 2.05 M55.05 9.95 C60.94 8.9, 67.47 5.96, 78.77 2.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(502.0000000000002 149.1158370359096) rotate(0 38.999999999999886 0.4969342584564762)"><path d="M55.52 -7.14 C63.96 -2.04, 70.34 -1.52, 78.77 2.05 M55.52 -7.14 C61.45 -3.62, 67.85 -2, 78.77 2.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(230 10) rotate(0 145.89398193359375 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Comunicação síncrona</text></g></svg>

<p>Mesmo em uma escala pequena, esse tipo de solução normalmente incorre em uma série de problemas:</p>

<ul>
  <li>
    <p>enviar e-mails é um processo demorado e deixar uma requisição “presa” enquanto espera o envio, pode sobrecarregar tanto os sistema de vendas quanto a mensageria;</p>
  </li>
  <li>
    <p>é comum usar serviços externos para envio de e-mails, o que aumenta problemas de latência e riscos de indisponibilidade;</p>
  </li>
  <li>
    <p>nem sempre as comunicações precisam (ou devem) ser enviadas logo após a compra, comunicações e canais podem ter políticas de envio complexas;</p>
  </li>
  <li>
    <p>o e-mail normalmente não é parte crítica do processo: faz mais sentido vender e não enviar o e-mail, do que perder uma venda por não conseguir enviar o e-mail.</p>
  </li>
</ul>

<p>Dado esse cenário, é normal partir para uma solução orientada a eventos, adicionando um <a href="https://en.wikipedia.org/wiki/Message_broker">message broker</a> para mediar a comunicação entre o transacional e a mensageria.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 731.5 301.49017333984375" width="700" height="300">
  <!-- svg-source:excalidraw -->
  
  <defs>
    <style class="style-fonts">
      @font-face {
        font-family: "Virgil";
        src: url("https://excalidraw.com/Virgil.woff2");
      }
      @font-face {
        font-family: "Cascadia";
        src: url("https://excalidraw.com/Cascadia.woff2");
      }
      @font-face {
        font-family: "Assistant";
        src: url("https://excalidraw.com/Assistant-Regular.woff2");
      }
    </style>
    
  </defs>
  <g stroke-linecap="round" transform="translate(10 81.3233642578125) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C2.61 17.02, 8.26 10.5, 17.56 0.69 M0.5 20.31 C4.75 15.06, 9.37 8.58, 17.56 0.69 M0.64 32.35 C9.06 19.89, 21.85 9.64, 28.19 0.66 M0.64 32.35 C6.3 24.24, 13.37 17.08, 28.19 0.66 M0.77 44.4 C14.69 29.01, 23.97 14.41, 38.82 0.62 M0.77 44.4 C8.22 35.06, 15.97 26.14, 38.82 0.62 M2.22 54.93 C15.45 41.41, 27.99 27.02, 49.45 0.59 M2.22 54.93 C18.67 36.99, 33.09 19.58, 49.45 0.59 M7.6 60.93 C21.93 44.88, 35.73 28.26, 60.08 0.55 M7.6 60.93 C25.49 41.04, 43.14 21.62, 60.08 0.55 M16.92 62.41 C34.5 46.44, 47.94 28.09, 70.06 1.27 M16.92 62.41 C33.63 43.09, 51.78 21.98, 70.06 1.27 M27.55 62.37 C41.3 44.4, 56.26 30.73, 80.69 1.24 M27.55 62.37 C44.01 42.91, 60.69 25.35, 80.69 1.24 M38.18 62.34 C49.43 50.5, 61.09 37.08, 91.32 1.2 M38.18 62.34 C60.31 38.51, 80.31 13.21, 91.32 1.2 M48.81 62.3 C65.01 44.32, 77.44 29.84, 101.95 1.17 M48.81 62.3 C65.47 43.3, 81.59 25.23, 101.95 1.17 M58.78 63.02 C81.18 41.38, 101.62 16.75, 112.58 1.14 M58.78 63.02 C78.15 42.22, 97.72 20.29, 112.58 1.14 M69.41 62.99 C81.5 46.51, 94.55 32.68, 123.21 1.1 M69.41 62.99 C90.59 39.02, 109.63 14.68, 123.21 1.1 M80.04 62.95 C95 47.21, 108.79 29.37, 133.18 1.82 M80.04 62.95 C100.04 40.75, 118.03 17.39, 133.18 1.82 M90.67 62.92 C101.94 48.07, 117.27 32.99, 139.22 7.07 M90.67 62.92 C105.43 44.22, 120.5 26.64, 139.22 7.07 M101.3 62.88 C113.14 46.81, 126.04 32.77, 141.32 16.85 M101.3 62.88 C110.47 50.6, 120.91 40.13, 141.32 16.85 M111.93 62.85 C121.81 53.3, 132.33 39.74, 140.8 29.64 M111.93 62.85 C122.38 50.32, 132.39 39.38, 140.8 29.64 M121.9 63.57 C128.36 58.84, 132.26 52.67, 140.93 41.68 M121.9 63.57 C128.66 55.31, 136.73 46.53, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C54.44 1.86, 94.42 1.8, 125.25 0 M15.75 0 C42.57 0.04, 70.27 -1.42, 125.25 0 M125.25 0 C133.79 0.61, 141.85 6.04, 141 15.75 M125.25 0 C136.23 1.35, 140.66 5.71, 141 15.75 M141 15.75 C140.53 23.75, 141.85 36.17, 141 47.25 M141 15.75 C140.83 26.17, 139.86 34.96, 141 47.25 M141 47.25 C139.29 58.72, 135.06 61.73, 125.25 63 M141 47.25 C142.25 56.98, 136.31 62.61, 125.25 63 M125.25 63 C98 64.83, 65.48 63.34, 15.75 63 M125.25 63 C91.62 61.45, 58.93 62.01, 15.75 63 M15.75 63 C6.52 62.35, -0.94 59.02, 0 47.25 M15.75 63 C4.17 62.12, 1.11 59.53, 0 47.25 M0 47.25 C-0.39 36.53, -2 29.65, 0 15.75 M0 47.25 C-0.33 41.33, 0.06 33.93, 0 15.75 M0 15.75 C0.47 5.32, 7.15 -0.92, 15.75 0 M0 15.75 C-2.17 3.31, 5.77 -1.62, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(37.68000030517578 87.8233642578125) rotate(0 42.81999969482422 25)"><text x="42.81999969482422" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Sistema </text><text x="42.81999969482422" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Vendas</text></g><g stroke-linecap="round"><g transform="translate(162.0726611011055 115.73381683193003) rotate(0 61.69805502204872 0.8341756103389741)"><path d="M0.5 -0.6 C21.06 -0.26, 103.55 1.75, 123.92 2.23 M-0.7 1.71 C19.71 1.67, 102.26 0.63, 123.24 0.41" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(162.0726611011055 115.73381683193003) rotate(0 61.69805502204872 0.8341756103389741)"><path d="M99.85 9.23 C106.91 6.96, 116.91 1.86, 123.24 0.41 M99.85 9.23 C107.22 6.61, 116.95 3.56, 123.24 0.41" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(162.0726611011055 115.73381683193003) rotate(0 61.69805502204872 0.8341756103389741)"><path d="M99.65 -7.87 C106.81 -4.25, 116.88 -3.44, 123.24 0.41 M99.65 -7.87 C107.04 -4.4, 116.84 -1.37, 123.24 0.41" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(121 186.79488118489587) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C61.75 -1.65, 99.06 -1.08, 132.33 0 M23.67 0 C46.83 0.06, 69.36 0.25, 132.33 0 M132.33 0 C147.81 0.4, 155.94 7.31, 156 23.67 M132.33 0 C148.65 -0.43, 154.45 8.97, 156 23.67 M156 23.67 C156.21 42.02, 157.97 58.32, 156 71 M156 23.67 C155.25 38.85, 156.14 54.97, 156 71 M156 71 C156.49 86.44, 147.41 93, 132.33 94.67 M156 71 C156.02 88.77, 150.2 92.6, 132.33 94.67 M132.33 94.67 C96.04 94.35, 62.53 96.1, 23.67 94.67 M132.33 94.67 C98.67 94.95, 65.48 93.93, 23.67 94.67 M23.67 94.67 C8.85 96.22, -0.06 88.35, 0 71 M23.67 94.67 C6.23 94.5, -1.57 84.76, 0 71 M0 71 C-1.47 54.12, 1.33 38.06, 0 23.67 M0 71 C0.84 59.23, -1.05 47.53, 0 23.67 M0 23.67 C0.46 6.48, 6.29 1.17, 23.67 0 M0 23.67 C-0.65 10.04, 8.93 1.37, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(135 196.39488118489584) rotate(0 64.66665649414062 38.62755555555552)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(580.5 85.9615478515625) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C3.38 17.17, 9.9 9.57, 17.56 0.69 M0.5 20.31 C4.67 15.5, 7.89 11.38, 17.56 0.69 M0.64 32.35 C6.06 26.2, 14.43 17.27, 28.19 0.66 M0.64 32.35 C10.83 19.26, 22.6 8.54, 28.19 0.66 M0.77 44.4 C11.27 32.64, 20.17 21.21, 38.82 0.62 M0.77 44.4 C14.05 29.05, 26.98 13.53, 38.82 0.62 M2.22 54.93 C20.39 34.84, 34.59 14.36, 49.45 0.59 M2.22 54.93 C15.58 39.58, 30.01 23.03, 49.45 0.59 M7.6 60.93 C27.74 38.85, 43.18 15.95, 60.08 0.55 M7.6 60.93 C27.45 38.41, 46.41 15.7, 60.08 0.55 M16.92 62.41 C27.13 46.98, 41.91 35.39, 70.06 1.27 M16.92 62.41 C34.45 42.49, 51.73 22.03, 70.06 1.27 M27.55 62.37 C45.55 41.88, 59.9 25.44, 80.69 1.24 M27.55 62.37 C43.57 44.51, 58.73 24.26, 80.69 1.24 M38.18 62.34 C54.47 41.78, 72.66 23.03, 91.32 1.2 M38.18 62.34 C58.74 40, 77.2 17.79, 91.32 1.2 M48.81 62.3 C66.24 45.9, 79.03 24.09, 101.95 1.17 M48.81 62.3 C67.44 41.39, 84.67 19.37, 101.95 1.17 M58.78 63.02 C76.42 42.06, 92.91 22.02, 112.58 1.14 M58.78 63.02 C72.01 49.96, 85.03 34.76, 112.58 1.14 M69.41 62.99 C89.55 38.31, 110.65 15.99, 123.21 1.1 M69.41 62.99 C86.37 43.28, 105.07 21.33, 123.21 1.1 M80.04 62.95 C98.17 39.84, 119.93 18.27, 133.18 1.82 M80.04 62.95 C91.11 50.34, 102.5 38.41, 133.18 1.82 M90.67 62.92 C103.32 47.43, 117.61 32.68, 139.22 7.07 M90.67 62.92 C103.33 47.44, 117.91 32.77, 139.22 7.07 M101.3 62.88 C112.11 51.89, 122.01 38.6, 141.32 16.85 M101.3 62.88 C112.41 50.09, 124.76 35.68, 141.32 16.85 M111.93 62.85 C122.47 51.44, 129.44 42.93, 140.8 29.64 M111.93 62.85 C122.98 50.48, 135.29 36.07, 140.8 29.64 M121.9 63.57 C126.37 56.97, 130.25 51.82, 140.93 41.68 M121.9 63.57 C128.84 55.21, 136.77 45.84, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C40.3 -1.63, 69.09 1.18, 125.25 0 M15.75 0 C52.75 -0.44, 90.02 -0.93, 125.25 0 M125.25 0 C136.22 -0.37, 139.65 6.19, 141 15.75 M125.25 0 C137.72 -0.01, 139.46 5.49, 141 15.75 M141 15.75 C141.99 23.63, 142.28 34.24, 141 47.25 M141 15.75 C141.41 23.03, 140.82 29.82, 141 47.25 M141 47.25 C141.02 59.48, 137.57 61.2, 125.25 63 M141 47.25 C141.38 57.9, 135.29 60.81, 125.25 63 M125.25 63 C93.06 65.18, 60 62.97, 15.75 63 M125.25 63 C85.93 63.36, 45.61 63.37, 15.75 63 M15.75 63 C3.81 62.86, -1.37 56, 0 47.25 M15.75 63 C6.53 60.74, 2.24 55.53, 0 47.25 M0 47.25 C-1.36 38.34, -0.06 30.4, 0 15.75 M0 47.25 C0.53 38.55, -0.5 31.84, 0 15.75 M0 15.75 C-0.57 7.12, 6.16 1.19, 15.75 0 M0 15.75 C2.23 3.44, 4.42 2.11, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(597.4599990844727 104.9615478515625) rotate(0 53.540000915527344 12.5)"><text x="53.540000915527344" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Mensageria</text></g><g stroke-linecap="round"><g transform="translate(448.0000000000002 118.38565787327343) rotate(0 61.64009241943421 0.7880712501122673)"><path d="M0.51 0.47 C21.29 0.79, 103.1 2.05, 123.53 2.28 M-0.68 -0.33 C20.07 -0.38, 102.22 -0.07, 122.71 0.52" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(448.0000000000002 118.38565787327343) rotate(0 61.64009241943421 0.7880712501122673)"><path d="M99.1 8.73 C104.62 7.39, 109.77 5.02, 122.71 0.52 M99.1 8.73 C106.89 5.99, 113.54 4.02, 122.71 0.52" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(448.0000000000002 118.38565787327343) rotate(0 61.64009241943421 0.7880712501122673)"><path d="M99.34 -8.36 C104.87 -6.26, 109.97 -5.17, 122.71 0.52 M99.34 -8.36 C107.05 -5.64, 113.62 -2.14, 122.71 0.52" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(208 10) rotate(0 162.83396911621094 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Comunicação assíncrona</text></g><g stroke-linecap="round" transform="translate(295.75 87.99017333984375) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C5.58 13.71, 12.71 4.07, 17.56 0.69 M0.5 20.31 C7.6 12.34, 13.43 4.07, 17.56 0.69 M0.64 32.35 C7.72 20.09, 16.86 13.97, 28.19 0.66 M0.64 32.35 C10.02 21.86, 17.83 12.81, 28.19 0.66 M0.77 44.4 C9.33 34.05, 20.03 23.79, 38.82 0.62 M0.77 44.4 C8.7 34.37, 18.59 25.62, 38.82 0.62 M2.22 54.93 C13.02 44.93, 21.91 31.27, 49.45 0.59 M2.22 54.93 C18.23 36.61, 35.9 16.45, 49.45 0.59 M7.6 60.93 C22.66 41.52, 41.17 21.89, 60.08 0.55 M7.6 60.93 C23.85 41.62, 40.68 22.85, 60.08 0.55 M16.92 62.41 C29.24 48.48, 44.03 29.3, 70.06 1.27 M16.92 62.41 C33.31 43.93, 48.58 24.46, 70.06 1.27 M27.55 62.37 C38.86 50.32, 49.84 35.33, 80.69 1.24 M27.55 62.37 C47.06 39.31, 67.37 16.69, 80.69 1.24 M38.18 62.34 C56.72 39.41, 78.8 18.01, 91.32 1.2 M38.18 62.34 C54.33 44.29, 68.76 25.76, 91.32 1.2 M48.81 62.3 C62.22 48.24, 77 30.96, 101.95 1.17 M48.81 62.3 C68.37 41.07, 86.32 18.89, 101.95 1.17 M58.78 63.02 C72.97 44.53, 91.57 26.89, 112.58 1.14 M58.78 63.02 C70.97 46.97, 84.12 31.86, 112.58 1.14 M69.41 62.99 C92.31 39.61, 112.77 15.02, 123.21 1.1 M69.41 62.99 C82.65 46.66, 97.19 32.14, 123.21 1.1 M80.04 62.95 C98.28 44.68, 114.51 22.91, 133.18 1.82 M80.04 62.95 C100.96 39.87, 121.01 15.91, 133.18 1.82 M90.67 62.92 C109.33 41.3, 127.27 21.74, 139.22 7.07 M90.67 62.92 C104.34 46.27, 119.08 30.07, 139.22 7.07 M101.3 62.88 C117.98 44.15, 129.9 27.15, 141.32 16.85 M101.3 62.88 C113.26 48.53, 126.35 35.03, 141.32 16.85 M111.93 62.85 C122.53 51.92, 132.42 42.28, 140.8 29.64 M111.93 62.85 C118.02 55.32, 124.4 48.56, 140.8 29.64 M121.9 63.57 C126.48 57.43, 128.46 55.35, 140.93 41.68 M121.9 63.57 C129.31 56.47, 135.53 47.82, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C52.69 -0.4, 89.05 -0.68, 125.25 0 M15.75 0 C38.14 0.15, 59.23 0.18, 125.25 0 M125.25 0 C136.24 1.41, 141.45 4.25, 141 15.75 M125.25 0 C135.7 -0.63, 141.18 3.02, 141 15.75 M141 15.75 C140.73 27.56, 139.38 35.5, 141 47.25 M141 15.75 C140.19 27.55, 140.28 41.49, 141 47.25 M141 47.25 C142.89 57.83, 136.97 61.33, 125.25 63 M141 47.25 C140.31 58.76, 133.96 61.77, 125.25 63 M125.25 63 C82.29 64.99, 38.63 62.95, 15.75 63 M125.25 63 C91.59 62.55, 55.74 63.65, 15.75 63 M15.75 63 C4.05 64.54, -0.2 58.58, 0 47.25 M15.75 63 C3.4 64.51, -1.57 56.6, 0 47.25 M0 47.25 C1.43 35.99, 1.08 22.11, 0 15.75 M0 47.25 C-0.18 38.24, -0.41 29.66, 0 15.75 M0 15.75 C0.61 6.47, 6.8 0.23, 15.75 0 M0 15.75 C-1.52 3.47, 4.33 1.76, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(320.11000061035156 94.49017333984375) rotate(0 46.13999938964844 25)"><text x="46.13999938964844" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Message </text><text x="46.13999938964844" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Brocker</text></g><g stroke-linecap="round" transform="translate(288.25 185.15684000651038) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C55.19 -1.03, 88 2.44, 132.33 0 M23.67 0 C61.43 -1.08, 98.5 0.13, 132.33 0 M132.33 0 C148.45 0.26, 154.83 9.69, 156 23.67 M132.33 0 C147.14 2.28, 155.45 7.56, 156 23.67 M156 23.67 C156.03 39.76, 155.67 58.7, 156 71 M156 23.67 C155.28 35.13, 154.97 47.13, 156 71 M156 71 C157.93 88.42, 148.73 96.62, 132.33 94.67 M156 71 C155.48 86.35, 150.21 95.63, 132.33 94.67 M132.33 94.67 C97.57 92.87, 61.05 95.29, 23.67 94.67 M132.33 94.67 C91.99 93.96, 50.68 94.21, 23.67 94.67 M23.67 94.67 C7.35 94.91, 0.37 88.7, 0 71 M23.67 94.67 C6.05 96.13, -1.8 87.78, 0 71 M0 71 C1.71 50.75, 0.76 33.36, 0 23.67 M0 71 C-0.63 56.34, 0.3 43.12, 0 23.67 M0 23.67 C-0.43 9.59, 7.93 -1.27, 23.67 0 M0 23.67 C-1.6 7.52, 6.54 0.78, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(302.25 194.7568400065104) rotate(0 64.6666488647461 38.62755555555552)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 3}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(459.25 184.15684000651038) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C54.41 1.45, 80.75 -0.01, 132.33 0 M23.67 0 C63.44 -0.28, 103.04 -1.32, 132.33 0 M132.33 0 C149.25 -0.56, 157.31 8.45, 156 23.67 M132.33 0 C148.33 1.41, 153.78 9.55, 156 23.67 M156 23.67 C156.06 33.85, 156.4 46.47, 156 71 M156 23.67 C156.66 38.47, 155.22 54.78, 156 71 M156 71 C154.45 85.35, 147.36 93.54, 132.33 94.67 M156 71 C154.5 87.89, 149.1 93.81, 132.33 94.67 M132.33 94.67 C96.25 95.04, 62.2 93.18, 23.67 94.67 M132.33 94.67 C105.46 94.11, 79.87 95.81, 23.67 94.67 M23.67 94.67 C7.1 95.75, 1.68 88.62, 0 71 M23.67 94.67 C8.83 93.07, 1.59 88.27, 0 71 M0 71 C1.1 53.19, -2 38.53, 0 23.67 M0 71 C-0.27 56.63, -0.9 40.52, 0 23.67 M0 23.67 C-1.42 8.58, 6.83 0.68, 23.67 0 M0 23.67 C1.99 6.99, 9.33 -1.29, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(473.25 193.75684000651052) rotate(0 64.6666488647461 38.62755555555552)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "352j-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round"><g transform="translate(296.25 145.49017333984375) rotate(0 -81.5 13)"><path d="M0.98 0.88 C-26 5.25, -134.91 22.11, -162.24 26.48 M0.04 0.3 C-27.01 4.28, -135.48 20.43, -162.8 24.61" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(438.25 144.49017333984375) rotate(0 87 13.5)"><path d="M0.57 0.28 C29.47 4.7, 144.65 21.71, 173.49 26.33 M-0.59 -0.62 C28.09 3.97, 143.4 23.05, 172.28 27.52" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(109.25 173.49017333984375) rotate(0 261 59)"><path d="M29.5 0 C151.16 2.39, 272.88 1.32, 492.5 0 M29.5 0 C132.99 -1.75, 236.82 -1.8, 492.5 0 M492.5 0 C513.71 -1.15, 523.56 8.19, 522 29.5 M492.5 0 C510.19 -1.06, 522.16 10.9, 522 29.5 M522 29.5 C521.05 50.85, 520.64 71.74, 522 88.5 M522 29.5 C521.57 51.17, 521.19 72.86, 522 88.5 M522 88.5 C521.86 108.76, 512.16 119.1, 492.5 118 M522 88.5 C522.08 110.03, 511.9 119.89, 492.5 118 M492.5 118 C365.52 116.44, 239.27 116.73, 29.5 118 M492.5 118 C324.99 117.97, 158.18 118.36, 29.5 118 M29.5 118 C11.67 117.1, -1.79 107.76, 0 88.5 M29.5 118 C9.05 116, 1.44 107.04, 0 88.5 M0 88.5 C0.73 68.81, 2.33 53.76, 0 29.5 M0 88.5 C-1.38 72.88, -0.01 54.64, 0 29.5 M0 29.5 C1.63 9.66, 11.53 0.24, 29.5 0 M0 29.5 C1.47 12.12, 12.1 -2.01, 29.5 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></svg>

<p>Nessa solução, o sistema de vendas coloca na fila os e-mails que precisam ser enviados, mas não aguarda o envio pela mensageria. Ao desacoplar os processos e adotar uma arquitetura orientada a eventos, temos uma série de vantagens:</p>

<ul>
  <li>
    <p>postar mensagens em um broker, costumar ser uma operação simples e estável. Isso torna o sistema de vendas mais rápido, mais fácil de dimensionar e escalar;</p>
  </li>
  <li>
    <p>o envio de e-mails – um processo complexo e demorado – não precisa mais ser dimensionado para o pico, apenas o broker precisa suportar o pior caso;</p>
  </li>
  <li>
    <p>apesar do broker ser uma peça a mais para manter, na prática costuma ser mais simples garantir a disponibilidade de um broker que de um sistema como a mensageria.</p>
  </li>
</ul>

<p>Por outro lado, temos desvantagens, especialmente pensando nas garantias de sucesso:</p>

<ul>
  <li>
    <p>o sistema de vendas não sabe quando o e-mail foi enviado, nem mesmo se houve problemas no envio;</p>
  </li>
  <li>
    <p>a observabilidade é mais difícil por envolver mais peças, especialmente erros que envolvem perda de mensagens;</p>
  </li>
  <li>
    <p>programação assíncrona é mais complexa que síncrona, esse tipo de solução costuma ser um ônus para os desenvolvedores.</p>
  </li>
</ul>

<p>A questão é ponderar esses <em>trade-offs</em>, o exemplo da mensageria é um cenário em que faz bastante sentido usar eventos, mas em outros casos pode ser necessário uma análise mais criteriosa antes de adotar uma abordagem assíncrona.</p>

<p>O cenário proposto é um exemplo de comunicação ponta a ponta usando eventos, mas é comum existir a necessidade de mútiplos consumidores. Por exemplo, além de enviar os eventos para o sistema de mensageria, salvar o que foi enviado em um ambiente analítico. Para lidar com esse cenário, uma opção interessante é usar o conceito de tópicos ao invés de filas.</p>

<h1 id="fila-ou-pubsub">Fila ou PubSub?</h1>

<p>O padrão para comunicação ponta-a-ponta, é utilizar uma fila: o broker mantém as mensagens guardadas até que o consumidor dê um <a href="https://en.wikipedia.org/wiki/Acknowledgement_(data_networks)">“ack”</a>, indicando que uma mensagem específica pode ser removida.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 747 292.49017333984375" width="700" height="300">
  <!-- svg-source:excalidraw -->
  
  <defs>
    <style class="style-fonts">
      @font-face {
        font-family: "Virgil";
        src: url("https://excalidraw.com/Virgil.woff2");
      }
      @font-face {
        font-family: "Cascadia";
        src: url("https://excalidraw.com/Cascadia.woff2");
      }
      @font-face {
        font-family: "Assistant";
        src: url("https://excalidraw.com/Assistant-Regular.woff2");
      }
    </style>
    
  </defs>
  <g stroke-linecap="round" transform="translate(10 77.3233642578125) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C5.49 12.05, 14.48 6.18, 17.56 0.69 M0.5 20.31 C4.72 16.71, 8.26 11.48, 17.56 0.69 M0.64 32.35 C10.1 18.73, 21.54 7.68, 28.19 0.66 M0.64 32.35 C10.73 19.45, 22.1 8.32, 28.19 0.66 M0.77 44.4 C7.64 33.75, 17.39 23.13, 38.82 0.62 M0.77 44.4 C8.26 35.93, 17.46 25.81, 38.82 0.62 M2.22 54.93 C18.59 33.36, 36.11 14.44, 49.45 0.59 M2.22 54.93 C18.41 34.95, 36.51 16.88, 49.45 0.59 M7.6 60.93 C24.57 43.43, 37.74 24.53, 60.08 0.55 M7.6 60.93 C20.99 45.92, 33.97 31.14, 60.08 0.55 M16.92 62.41 C33.84 42.89, 52.58 22.08, 70.06 1.27 M16.92 62.41 C37.65 39.85, 55.82 16.28, 70.06 1.27 M27.55 62.37 C39.93 47.1, 52.42 33.93, 80.69 1.24 M27.55 62.37 C40.69 48.58, 52.5 35.21, 80.69 1.24 M38.18 62.34 C58.23 40.08, 74.81 17.14, 91.32 1.2 M38.18 62.34 C52.58 44.87, 68.11 27.82, 91.32 1.2 M48.81 62.3 C68.29 39.6, 92.54 14.76, 101.95 1.17 M48.81 62.3 C64.72 43.97, 80.43 26.39, 101.95 1.17 M58.78 63.02 C77.08 39.91, 96.12 19.59, 112.58 1.14 M58.78 63.02 C73.4 47.37, 87.31 29.16, 112.58 1.14 M69.41 62.99 C82.92 48.06, 95.77 30.47, 123.21 1.1 M69.41 62.99 C80.59 50.99, 91.18 37.15, 123.21 1.1 M80.04 62.95 C98.95 39.53, 118.7 16.78, 133.18 1.82 M80.04 62.95 C93.55 47.65, 105.34 32.52, 133.18 1.82 M90.67 62.92 C106.98 41.58, 123.76 23.92, 139.22 7.07 M90.67 62.92 C109.64 40.65, 128.93 20.4, 139.22 7.07 M101.3 62.88 C115 45.63, 131.5 27.55, 141.32 16.85 M101.3 62.88 C112.67 49.73, 124.52 36.94, 141.32 16.85 M111.93 62.85 C117.75 53.9, 126.35 48.94, 140.8 29.64 M111.93 62.85 C120.35 53.08, 129.67 41.04, 140.8 29.64 M121.9 63.57 C125.67 60.32, 130.13 55.06, 140.93 41.68 M121.9 63.57 C129.64 54.17, 137.04 45.69, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C40.13 1.52, 65.14 -0.14, 125.25 0 M15.75 0 C51.2 -0.26, 88.91 -0.19, 125.25 0 M125.25 0 C134.12 1.12, 141.25 6.89, 141 15.75 M125.25 0 C136.26 -1.69, 141.69 6.93, 141 15.75 M141 15.75 C141 24.88, 139.44 36.47, 141 47.25 M141 15.75 C140.22 25.6, 141.14 35.76, 141 47.25 M141 47.25 C142.45 59.18, 137.36 63.96, 125.25 63 M141 47.25 C139.34 56.32, 137.54 64.9, 125.25 63 M125.25 63 C82.11 61.53, 40.73 61.62, 15.75 63 M125.25 63 C101.15 63.47, 78.44 62.95, 15.75 63 M15.75 63 C5.15 64.94, -1.3 56.42, 0 47.25 M15.75 63 C3.46 63.9, 1.37 59.07, 0 47.25 M0 47.25 C-2 39.12, -1.7 35.33, 0 15.75 M0 47.25 C-0.07 38.88, 0.08 28.64, 0 15.75 M0 15.75 C1.46 3.59, 5.22 0.03, 15.75 0 M0 15.75 C1.95 6.47, 3.61 1.42, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(37.68000030517578 83.8233642578125) rotate(0 42.81999969482422 25)"><text x="42.81999969482422" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Sistema </text><text x="42.81999969482422" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Vendas</text></g><g stroke-linecap="round"><g transform="translate(162.0726611011055 111.73381683192997) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M-0.23 0.37 C20.03 0.44, 101.88 1.42, 122.34 1.57 M-1.82 -0.48 C18.74 0.29, 103.37 2.58, 124.49 3.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(162.0726611011055 111.73381683192997) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M100.79 11.01 C109.94 6.4, 117.77 3.75, 124.49 3.05 M100.79 11.01 C106.2 9.52, 113.43 7.67, 124.49 3.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(162.0726611011055 111.73381683192997) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M101.22 -6.09 C110.37 -4.49, 118.04 -0.92, 124.49 3.05 M101.22 -6.09 C106.65 -3.04, 113.76 -0.36, 124.49 3.05" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(581 178.79488118489587) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C53.98 -0.56, 83.93 2.46, 132.33 0 M23.67 0 C53.3 0.21, 82.91 -1.34, 132.33 0 M132.33 0 C148.71 1.46, 156.79 7.99, 156 23.67 M132.33 0 C149.99 -0.02, 154.21 6.07, 156 23.67 M156 23.67 C154.25 34.94, 155.81 49.39, 156 71 M156 23.67 C157.15 38.56, 155.62 51.87, 156 71 M156 71 C157.56 88.43, 149.82 93.77, 132.33 94.67 M156 71 C153.74 85.72, 147.66 94.58, 132.33 94.67 M132.33 94.67 C102.1 93.83, 70.56 94.75, 23.67 94.67 M132.33 94.67 C103.98 95.06, 76.14 96.11, 23.67 94.67 M23.67 94.67 C9.08 95.81, -1.82 88.68, 0 71 M23.67 94.67 C7.81 92.71, -1.79 85.16, 0 71 M0 71 C0.24 54.22, 1.98 37.4, 0 23.67 M0 71 C-0.36 56.32, 0.5 42.15, 0 23.67 M0 23.67 C-1.43 9.12, 7 0.22, 23.67 0 M0 23.67 C-1.72 9.54, 8.73 -0.07, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(595 188.3948811848959) rotate(0 64.66665649414062 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(580.5 81.9615478515625) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C8.82 13.11, 10.92 5.58, 16.91 1.45 M4.44 15.79 C7.37 13.72, 9.68 9.12, 16.91 1.45 M4.57 27.83 C11.42 20.91, 16.18 18.75, 27.54 1.41 M4.57 27.83 C9.4 20.88, 15.15 15.19, 27.54 1.41 M4.71 39.87 C11.97 30.96, 23.72 19.19, 38.17 1.38 M4.71 39.87 C13.48 31.05, 20.76 21.54, 38.17 1.38 M3.53 53.42 C14.52 41.91, 22.65 29.24, 48.8 1.34 M3.53 53.42 C18.84 35.42, 33.58 18.31, 48.8 1.34 M6.94 61.68 C22.49 44.24, 37.63 27.01, 59.43 1.31 M6.94 61.68 C19.89 47.64, 31.98 31.8, 59.43 1.31 M13.64 66.18 C33.49 40.37, 56.88 17.08, 70.06 1.27 M13.64 66.18 C30.43 46.85, 45.72 28.22, 70.06 1.27 M24.27 66.14 C44.98 45.25, 62.94 20.7, 80.03 1.99 M24.27 66.14 C43.41 43.61, 63.68 21.61, 80.03 1.99 M34.9 66.11 C47.43 54.93, 57.35 38.38, 90.66 1.96 M34.9 66.11 C46.75 51.8, 58.26 38.52, 90.66 1.96 M45.53 66.08 C63.65 46.32, 79.59 25.6, 101.29 1.92 M45.53 66.08 C60.93 48.01, 76.17 30.42, 101.29 1.92 M55.5 66.8 C69.11 47.61, 87.59 31.4, 111.92 1.89 M55.5 66.8 C72.47 45.12, 91.8 25.57, 111.92 1.89 M66.13 66.76 C84.33 46, 104.79 20.19, 122.55 1.86 M66.13 66.76 C80.32 50.15, 94.04 34.37, 122.55 1.86 M76.76 66.73 C93.07 48.22, 113.07 26.14, 133.18 1.82 M76.76 66.73 C91.65 52.29, 105.46 34.61, 133.18 1.82 M87.39 66.69 C106.98 46.81, 125.61 25.28, 139.22 7.07 M87.39 66.69 C100.97 49.62, 115.74 34.04, 139.22 7.07 M98.02 66.66 C111.63 49.33, 125.39 36.84, 143.29 14.58 M98.02 66.66 C111.51 51.73, 124.47 37.82, 143.29 14.58 M108.65 66.62 C115.76 54.4, 124.89 45.03, 143.42 26.62 M108.65 66.62 C118.57 56.05, 129.39 43.24, 143.42 26.62 M118.62 67.34 C125.96 59.6, 134.93 46.7, 142.9 39.42 M118.62 67.34 C127.23 57, 135.69 47.69, 142.9 39.42" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C39.71 -2.45, 65.92 -0.57, 125.25 0 M15.75 0 C57.86 1.54, 99.77 0.85, 125.25 0 M125.25 0 C137.38 -0.02, 139.44 3.67, 141 15.75 M125.25 0 C134.98 0.39, 140.27 3.15, 141 15.75 M141 15.75 C141.05 24.52, 142.39 32.01, 141 47.25 M141 15.75 C141.55 27.54, 141.63 37.24, 141 47.25 M141 47.25 C139.04 56.83, 135.36 62.93, 125.25 63 M141 47.25 C140.65 55.72, 137.91 63.84, 125.25 63 M125.25 63 C101.66 62.39, 81.82 61.19, 15.75 63 M125.25 63 C100.27 64, 73.19 64.38, 15.75 63 M15.75 63 C5.18 61.3, -1.56 56.34, 0 47.25 M15.75 63 C6.67 62.93, 1.76 58.01, 0 47.25 M0 47.25 C-0.1 38.71, -1.97 27.22, 0 15.75 M0 47.25 C-0.98 38.37, -0.71 28.37, 0 15.75 M0 15.75 C-1.5 6.68, 5.98 -0.06, 15.75 0 M0 15.75 C0.83 3.29, 3.49 -1.8, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(597.4599990844727 100.9615478515625) rotate(0 53.540000915527344 12.5)"><text x="53.540000915527344" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Mensageria</text></g><g stroke-linecap="round"><g transform="translate(448.0000000000002 114.38565787327343) rotate(0 61.64009241943421 0.7880712501122389)"><path d="M0.15 0.98 C20.9 0.99, 102.96 0.45, 123.55 0.69 M-1.23 0.45 C19.47 0.53, 102.02 1.75, 122.74 1.77" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(448.0000000000002 114.38565787327343) rotate(0 61.64009241943421 0.7880712501122389)"><path d="M99.17 10.11 C105.92 8.7, 107.96 7.04, 122.74 1.77 M99.17 10.11 C108.75 6.98, 115.94 3.09, 122.74 1.77" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(448.0000000000002 114.38565787327343) rotate(0 61.64009241943421 0.7880712501122389)"><path d="M99.32 -6.99 C106.13 -4.66, 108.14 -2.58, 122.74 1.77 M99.32 -6.99 C108.91 -3.74, 116.05 -1.25, 122.74 1.77" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(337 10) rotate(0 24.12200164794922 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Fila</text></g><g stroke-linecap="round" transform="translate(295.75 83.99017333984375) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C6.68 15.19, 7.58 9.01, 17.56 0.69 M0.5 20.31 C4.12 16.16, 10.07 9.68, 17.56 0.69 M0.64 32.35 C6.68 23.46, 15.29 13.66, 28.19 0.66 M0.64 32.35 C10.39 22.64, 18.85 10.67, 28.19 0.66 M0.77 44.4 C11.45 31.99, 20.9 21.1, 38.82 0.62 M0.77 44.4 C9.66 33.32, 18.1 23.62, 38.82 0.62 M2.22 54.93 C16.74 35.77, 33.39 20.17, 49.45 0.59 M2.22 54.93 C16.13 38.9, 30.18 23.06, 49.45 0.59 M7.6 60.93 C28.32 37.96, 45.49 18.05, 60.08 0.55 M7.6 60.93 C22.89 43.51, 39.22 25.64, 60.08 0.55 M16.92 62.41 C29.67 44.69, 43.92 30.82, 70.06 1.27 M16.92 62.41 C33.55 43.7, 48.36 25.65, 70.06 1.27 M27.55 62.37 C41.75 45.86, 57.37 28.54, 80.69 1.24 M27.55 62.37 C45.86 42.14, 61.33 21.97, 80.69 1.24 M38.18 62.34 C56.42 39.96, 76.33 19.19, 91.32 1.2 M38.18 62.34 C53.38 44.26, 68.21 27.52, 91.32 1.2 M48.81 62.3 C58.98 48.68, 70.05 37.21, 101.95 1.17 M48.81 62.3 C68.42 40.3, 89.33 17.5, 101.95 1.17 M58.78 63.02 C74.77 43.85, 95 23.21, 112.58 1.14 M58.78 63.02 C80.12 39.75, 98.93 15.39, 112.58 1.14 M69.41 62.99 C82.23 45.81, 97.02 32.13, 123.21 1.1 M69.41 62.99 C84.06 48.33, 96.6 31.97, 123.21 1.1 M80.04 62.95 C95.16 49.35, 104.29 34.21, 133.18 1.82 M80.04 62.95 C90.88 51.15, 101.21 37.74, 133.18 1.82 M90.67 62.92 C103.38 46.9, 118.73 32.14, 139.22 7.07 M90.67 62.92 C104.14 46.32, 120.25 29, 139.22 7.07 M101.3 62.88 C110.08 53.67, 120.49 41.27, 141.32 16.85 M101.3 62.88 C115.91 44.65, 131.91 27.36, 141.32 16.85 M111.93 62.85 C124.08 52.99, 131.7 39.99, 140.8 29.64 M111.93 62.85 C119.17 55.42, 123.9 48.9, 140.8 29.64 M121.9 63.57 C128.05 55.14, 136.21 47.15, 140.93 41.68 M121.9 63.57 C125.13 58.89, 130.09 54.1, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C52.49 0.46, 86.41 1.3, 125.25 0 M15.75 0 C48.17 1.03, 80.94 -0.05, 125.25 0 M125.25 0 C134.4 -0.27, 141.48 5.34, 141 15.75 M125.25 0 C137.42 1.65, 142.85 6.36, 141 15.75 M141 15.75 C142.46 24.64, 139.91 28.14, 141 47.25 M141 15.75 C140.79 22.87, 141.88 32.13, 141 47.25 M141 47.25 C139.14 57.9, 134.82 62.1, 125.25 63 M141 47.25 C140.89 59.98, 134.25 61.47, 125.25 63 M125.25 63 C102.07 61.83, 78.5 63.59, 15.75 63 M125.25 63 C102.35 62.2, 78.98 62.34, 15.75 63 M15.75 63 C5.6 64.97, 0.65 57.96, 0 47.25 M15.75 63 C6.93 61.09, -0.04 57.78, 0 47.25 M0 47.25 C1.07 34.25, 0.05 21.52, 0 15.75 M0 47.25 C0.25 34.69, -0.88 23.05, 0 15.75 M0 15.75 C1.14 6.81, 6.46 -0.03, 15.75 0 M0 15.75 C-1.61 6.41, 3.84 -1.3, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(320.11000061035156 90.49017333984375) rotate(0 46.13999938964844 25)"><text x="46.13999938964844" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Message </text><text x="46.13999938964844" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Brocker</text></g><g stroke-linecap="round" transform="translate(374.25 179.15684000651038) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C60.54 -2.45, 95.86 -1.55, 132.33 0 M23.67 0 C57.43 0.86, 92.79 1.04, 132.33 0 M132.33 0 C149.56 1.43, 157.61 8.85, 156 23.67 M132.33 0 C146.45 -1.43, 157.79 9.79, 156 23.67 M156 23.67 C154.87 41.53, 155.71 59.87, 156 71 M156 23.67 C155.52 33.76, 155.98 43.25, 156 71 M156 71 C155.9 88.72, 146.81 93.34, 132.33 94.67 M156 71 C154.21 87.68, 149.48 95.99, 132.33 94.67 M132.33 94.67 C107.94 93.07, 85.56 95.87, 23.67 94.67 M132.33 94.67 C100.24 95.77, 68.12 94.89, 23.67 94.67 M23.67 94.67 C9.35 93.01, -0.03 86.81, 0 71 M23.67 94.67 C9.84 95.89, -1.64 88.2, 0 71 M0 71 C1.38 59.62, -0.11 47.51, 0 23.67 M0 71 C0.93 61.61, 0.97 50.65, 0 23.67 M0 23.67 C-1.4 8.9, 6.66 -1.13, 23.67 0 M0 23.67 C-1.28 6.18, 10.08 -0.55, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(388.25 188.7568400065104) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 3}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(208.25 180.15684000651038) rotate(0 78 47.333333333333314)"><path d="M23.67 0 C48.13 -0.27, 70.09 -0.99, 132.33 0 M23.67 0 C55.55 0.67, 86.78 0.43, 132.33 0 M132.33 0 C146.67 -1.24, 157.56 9.54, 156 23.67 M132.33 0 C150.08 -1.03, 153.74 6.83, 156 23.67 M156 23.67 C154.21 38.82, 156.71 48.36, 156 71 M156 23.67 C155.73 39.19, 155.13 52.11, 156 71 M156 71 C154.44 87.56, 149.3 95.81, 132.33 94.67 M156 71 C153.9 88.96, 148.04 92.71, 132.33 94.67 M132.33 94.67 C108.13 96.87, 84.28 95.69, 23.67 94.67 M132.33 94.67 C89.76 93.95, 45.71 94.79, 23.67 94.67 M23.67 94.67 C9.58 95.73, -1.43 88.01, 0 71 M23.67 94.67 C6.87 94.92, -1.72 88.43, 0 71 M0 71 C-1.69 53.54, -1.55 40.29, 0 23.67 M0 71 C-0.99 53.62, -0.9 34.67, 0 23.67 M0 23.67 C-1.11 6.4, 9.79 -0.48, 23.67 0 M0 23.67 C-1.47 7.88, 5.92 0.44, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(222.25 189.75684000651052) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "352j-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round"><g transform="translate(296.25 141.49017333984375) rotate(0 -51 12)"><path d="M-0.81 -0.16 C-17.7 4.03, -84.99 19.88, -101.71 24.05 M0.96 -1.29 C-16.02 3.21, -85.45 21.58, -102.51 25.62" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(438.25 140.49017333984375) rotate(0 48.5 13)"><path d="M0.29 0.05 C16.73 4.56, 81.59 22.44, 97.87 26.86 M-1.02 -0.96 C15.41 3.22, 80.92 20.94, 97.38 25.19" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(192.25 164.49017333984375) rotate(0 176.5 59)"><path d="M29.5 0 C125.76 -0.39, 220.6 -2.66, 323.5 0 M29.5 0 C100.1 -2.14, 168.7 -2.15, 323.5 0 M323.5 0 C342.78 -0.07, 352.7 8.07, 353 29.5 M323.5 0 C345.32 0.84, 350.86 10.01, 353 29.5 M353 29.5 C354.68 43.15, 351.41 57.85, 353 88.5 M353 29.5 C352.44 50.45, 353.32 68.66, 353 88.5 M353 88.5 C351.44 106.76, 344.4 117.94, 323.5 118 M353 88.5 C354.76 108.42, 343.57 120.27, 323.5 118 M323.5 118 C224.5 119.57, 127.98 120.85, 29.5 118 M323.5 118 C219.34 119.43, 115.29 119.88, 29.5 118 M29.5 118 C10.56 117.94, 0.72 106.46, 0 88.5 M29.5 118 C8.07 116.2, 1.31 109.96, 0 88.5 M0 88.5 C1.02 66, -1.12 44.83, 0 29.5 M0 88.5 C-1.2 75.18, -1.42 62.07, 0 29.5 M0 29.5 C1.75 9.25, 10.39 1.96, 29.5 0 M0 29.5 C-1.63 8.51, 7.84 -1.34, 29.5 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></svg>

<p>O problema é que esse padrão não suporta múltiplos consumidores, é necessário replicar e alimentar a fila para cada consumidor adicional. Não é uma solução escalável, já que a quantidade de mensagens cresce em função do número de consumidores.</p>

<p>A alternativa, para lidar com um cenário de múltiplos consumidores, é utilizar a abordagem PubSub adotada pelo Kafka. Nesse modelo, o broker guarda o conteúdo por um tempo determinado (<em>e.g.</em> 7 dias), fica a cargo da aplicação consumidora controlar o <em>offset</em> para saber o que foi lido.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 730.75 436.1158744154561" width="700" height="400">
  <!-- svg-source:excalidraw -->
  
  <defs>
    <style class="style-fonts">
      @font-face {
        font-family: "Virgil";
        src: url("https://excalidraw.com/Virgil.woff2");
      }
      @font-face {
        font-family: "Cascadia";
        src: url("https://excalidraw.com/Cascadia.woff2");
      }
      @font-face {
        font-family: "Assistant";
        src: url("https://excalidraw.com/Assistant-Regular.woff2");
      }
    </style>
    
  </defs>
  <g transform="translate(312 25.64324951171875) rotate(0 47.89399719238281 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">PubSub</text></g><g stroke-linecap="round" transform="translate(18.75 143.36181640625) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C8.54 12.28, 11.23 6.31, 16.91 1.45 M4.44 15.79 C9.17 9.86, 14.25 4.52, 16.91 1.45 M4.57 27.83 C13.71 18.12, 21.56 9.69, 27.54 1.41 M4.57 27.83 C14.02 17.07, 22.97 7.72, 27.54 1.41 M4.71 39.87 C15 24.24, 29.79 11.02, 38.17 1.38 M4.71 39.87 C14.4 27.7, 25.64 16.27, 38.17 1.38 M3.53 53.42 C14.14 41.15, 21.94 31.71, 48.8 1.34 M3.53 53.42 C20.79 34.11, 37.87 14.55, 48.8 1.34 M6.94 61.68 C19.83 46.69, 33.21 28.82, 59.43 1.31 M6.94 61.68 C23.42 42.63, 38.26 24.09, 59.43 1.31 M13.64 66.18 C32.78 41.53, 51.07 19.29, 70.06 1.27 M13.64 66.18 C34 43.1, 53.43 20.38, 70.06 1.27 M24.27 66.14 C46.98 39.14, 67.95 14.91, 80.03 1.99 M24.27 66.14 C40.14 47.63, 55.04 28.76, 80.03 1.99 M34.9 66.11 C47.1 51.31, 61.53 37.8, 90.66 1.96 M34.9 66.11 C55.1 44.63, 73.33 20.86, 90.66 1.96 M45.53 66.08 C63.14 47.62, 79.05 24.48, 101.29 1.92 M45.53 66.08 C66.15 40.97, 88.46 15.53, 101.29 1.92 M55.5 66.8 C72.06 46.11, 88.67 26.61, 111.92 1.89 M55.5 66.8 C78.28 42.06, 98.87 17.25, 111.92 1.89 M66.13 66.76 C81.76 52.25, 94.85 33.06, 122.55 1.86 M66.13 66.76 C78 52.55, 91.51 38.81, 122.55 1.86 M76.76 66.73 C91.58 51.58, 106.62 32.54, 133.18 1.82 M76.76 66.73 C100.56 41.67, 122.55 14.62, 133.18 1.82 M87.39 66.69 C107.12 44.9, 124.16 23.53, 139.22 7.07 M87.39 66.69 C98.48 54.69, 109.2 40.9, 139.22 7.07 M98.02 66.66 C114.06 47.9, 130.55 27.72, 143.29 14.58 M98.02 66.66 C110.09 51.48, 123.75 36.86, 143.29 14.58 M108.65 66.62 C117 55.74, 128.22 42.24, 143.42 26.62 M108.65 66.62 C117.84 54.77, 128.49 43.54, 143.42 26.62 M118.62 67.34 C124.4 58.75, 131.49 52.53, 142.9 39.42 M118.62 67.34 C126.07 60.3, 132.57 51.55, 142.9 39.42" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C49.77 -1.97, 89.43 0.48, 125.25 0 M15.75 0 C41.85 -1.51, 65.5 -0.67, 125.25 0 M125.25 0 C137.5 -0.35, 139.27 4.9, 141 15.75 M125.25 0 C136.72 -2.2, 141.04 3.73, 141 15.75 M141 15.75 C140.01 21.68, 140.85 29.25, 141 47.25 M141 15.75 C140.4 26.78, 140.26 37.62, 141 47.25 M141 47.25 C142.34 57.62, 134.39 63.99, 125.25 63 M141 47.25 C141.88 56.73, 135.65 64.32, 125.25 63 M125.25 63 C91.28 62.25, 58.55 63.74, 15.75 63 M125.25 63 C83.53 63.65, 41.73 63.41, 15.75 63 M15.75 63 C4.13 62.24, -1.26 58.97, 0 47.25 M15.75 63 C7.45 60.71, -0.74 58.4, 0 47.25 M0 47.25 C-1.04 35.6, 0.31 24.16, 0 15.75 M0 47.25 C0.2 40.18, 0.32 32.72, 0 15.75 M0 15.75 C-0.08 5.89, 4.12 -1.2, 15.75 0 M0 15.75 C-0.36 3.45, 6.7 -0.7, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(46.43000030517578 149.86181640625) rotate(0 42.81999969482422 25)"><text x="42.81999969482422" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Sistema </text><text x="42.81999969482422" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Vendas</text></g><g stroke-linecap="round"><g transform="translate(170.8226611011055 177.77226898036747) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M0.5 -0.85 C21.29 -0.58, 103.08 2.37, 123.75 2.67 M-0.7 1.32 C20.05 1.79, 102.14 0.98, 122.99 1.07" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(170.8226611011055 177.77226898036747) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M99.53 9.7 C103.17 8.31, 111.38 7.79, 122.99 1.07 M99.53 9.7 C106.09 7.39, 110 5.31, 122.99 1.07" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(170.8226611011055 177.77226898036747) rotate(0 61.69805502204872 0.8341756103390026)"><path d="M99.48 -7.4 C103.28 -5.05, 111.49 -1.84, 122.99 1.07 M99.48 -7.4 C105.97 -5.6, 109.89 -3.56, 122.99 1.07" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(201.75 248.83333333333348) rotate(0 78 47.33333333333337)"><path d="M23.67 0 C62.43 1.63, 96.67 -1.32, 132.33 0 M23.67 0 C58.04 0.02, 94.56 -0.77, 132.33 0 M132.33 0 C148.15 -1.32, 154.85 7.05, 156 23.67 M132.33 0 C146.79 -0.99, 154.04 7.87, 156 23.67 M156 23.67 C157.89 32.27, 157.17 41.99, 156 71 M156 23.67 C155.64 42.23, 156.7 59.35, 156 71 M156 71 C155.91 87.93, 148.23 93.28, 132.33 94.67 M156 71 C155.25 86.25, 147.66 95.05, 132.33 94.67 M132.33 94.67 C94.04 95.12, 56.21 95.23, 23.67 94.67 M132.33 94.67 C96.26 94.86, 62.4 93.26, 23.67 94.67 M23.67 94.67 C7.25 95.23, 1.26 85.83, 0 71 M23.67 94.67 C6.85 93.3, -0.25 86.97, 0 71 M0 71 C-0.4 57.47, -0.17 44.02, 0 23.67 M0 71 C-0.55 60.73, -0.14 50.87, 0 23.67 M0 23.67 C1.26 7.28, 8.91 -0.59, 23.67 0 M0 23.67 C1.27 10.15, 6.09 0.22, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(215.75 258.4333333333334) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(459.25 10) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C6.97 12, 10.51 8.74, 16.91 1.45 M4.44 15.79 C8.86 11.33, 14.08 5.29, 16.91 1.45 M4.57 27.83 C14.03 19.47, 22.46 9.54, 27.54 1.41 M4.57 27.83 C9.35 22.49, 14.26 16.59, 27.54 1.41 M4.71 39.87 C13.01 27.92, 23.05 19.82, 38.17 1.38 M4.71 39.87 C17.47 26.21, 30.01 11.16, 38.17 1.38 M3.53 53.42 C18.3 37.65, 28.63 24.65, 48.8 1.34 M3.53 53.42 C13.56 41.69, 25.12 28.88, 48.8 1.34 M6.94 61.68 C25.6 40.69, 47.49 17.99, 59.43 1.31 M6.94 61.68 C22 45.24, 37.37 26.68, 59.43 1.31 M13.64 66.18 C31.71 46.27, 53.62 21.18, 70.06 1.27 M13.64 66.18 C33.69 43.44, 54.21 20.99, 70.06 1.27 M24.27 66.14 C41.58 43.55, 62.38 22.02, 80.03 1.99 M24.27 66.14 C40.24 48.6, 55.3 29.9, 80.03 1.99 M34.9 66.11 C53.97 43.06, 76.2 19.11, 90.66 1.96 M34.9 66.11 C51.48 46.76, 70.85 26.19, 90.66 1.96 M45.53 66.08 C58.61 50.24, 74.04 32.34, 101.29 1.92 M45.53 66.08 C60.42 50.47, 74.41 32.19, 101.29 1.92 M55.5 66.8 C75.77 42.76, 95.72 20.65, 111.92 1.89 M55.5 66.8 C75.16 41.91, 95.63 19.77, 111.92 1.89 M66.13 66.76 C78.9 52.4, 90.77 37.99, 122.55 1.86 M66.13 66.76 C83.49 46.85, 100.82 25.38, 122.55 1.86 M76.76 66.73 C96.97 42.86, 119.8 15.49, 133.18 1.82 M76.76 66.73 C97.18 42.33, 118.55 18.59, 133.18 1.82 M87.39 66.69 C102.43 49.56, 117.85 29.11, 139.22 7.07 M87.39 66.69 C100.26 51.17, 114.55 35.21, 139.22 7.07 M98.02 66.66 C107.65 55.2, 118.1 42.62, 143.29 14.58 M98.02 66.66 C107.09 55.05, 118.72 42.92, 143.29 14.58 M108.65 66.62 C116.8 55.39, 128.15 45.29, 143.42 26.62 M108.65 66.62 C118 56.6, 126.57 45.8, 143.42 26.62 M118.62 67.34 C127.36 59.6, 133.84 47.2, 142.9 39.42 M118.62 67.34 C125.1 59.28, 132.1 51.24, 142.9 39.42" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C59.81 2.29, 102.56 -1.18, 125.25 0 M15.75 0 C46.7 0.39, 77.04 0.63, 125.25 0 M125.25 0 C134.6 -0.86, 139.29 5.23, 141 15.75 M125.25 0 C133.71 0.79, 141.74 7.24, 141 15.75 M141 15.75 C142.79 24.33, 141.03 28.87, 141 47.25 M141 15.75 C141.08 27.33, 141.18 37.06, 141 47.25 M141 47.25 C140.35 57.29, 135.35 63.33, 125.25 63 M141 47.25 C142.26 59.64, 136.14 63.32, 125.25 63 M125.25 63 C91.47 62.09, 55.88 61.95, 15.75 63 M125.25 63 C85.41 62.19, 46.84 61.44, 15.75 63 M15.75 63 C4.35 61.82, -0.22 57.92, 0 47.25 M15.75 63 C5 61.06, -0.86 56.83, 0 47.25 M0 47.25 C-1.82 37.47, 0.73 26.71, 0 15.75 M0 47.25 C0.68 39.38, 0.56 31.83, 0 15.75 M0 15.75 C1.1 7.22, 3.68 0.19, 15.75 0 M0 15.75 C-1.29 6.06, 4.51 -0.58, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(476.20999908447266 29) rotate(0 53.540000915527344 12.5)"><text x="53.540000915527344" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Mensageria</text></g><g stroke-linecap="round"><g transform="translate(410.64465824041633 145.77862548828102) rotate(0 39.64458462645297 -31.779405163574836)"><path d="M-1.04 -0.21 C12.09 -10.96, 66.4 -54.02, 79.8 -64.71 M0.62 -1.37 C13.54 -11.99, 66.12 -53.43, 79.11 -63.78" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(410.64465824041633 145.77862548828102) rotate(0 39.64458462645297 -31.779405163574836)"><path d="M66.01 -42.49 C70.77 -49.77, 74.28 -58.83, 79.11 -63.78 M66.01 -42.49 C68.62 -47.84, 71.41 -52.39, 79.11 -63.78" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(410.64465824041633 145.77862548828102) rotate(0 39.64458462645297 -31.779405163574836)"><path d="M55.39 -55.89 C64.24 -57.89, 71.86 -61.76, 79.11 -63.78 M55.39 -55.89 C60.41 -58.06, 65.68 -59.48, 79.11 -63.78" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(304.5 150.02862548828125) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M0.5 20.31 C8.51 12.38, 15.49 6.16, 17.56 0.69 M0.5 20.31 C6.18 14.94, 11.4 9.04, 17.56 0.69 M0.64 32.35 C11.19 21.15, 20.73 10.35, 28.19 0.66 M0.64 32.35 C8.25 24.33, 13.63 17.12, 28.19 0.66 M0.77 44.4 C11.64 31.54, 24.51 18.55, 38.82 0.62 M0.77 44.4 C12.85 30.93, 25.09 15.33, 38.82 0.62 M2.22 54.93 C18.41 33.79, 38.09 15.33, 49.45 0.59 M2.22 54.93 C12.99 41.07, 25.97 28.7, 49.45 0.59 M7.6 60.93 C23.44 40.07, 43.69 20.39, 60.08 0.55 M7.6 60.93 C19.74 46.42, 33.03 33.68, 60.08 0.55 M16.92 62.41 C28.96 47.91, 41.54 35.05, 70.06 1.27 M16.92 62.41 C32.29 43.06, 48.47 25.41, 70.06 1.27 M27.55 62.37 C41.09 49.21, 53.25 34.36, 80.69 1.24 M27.55 62.37 C40 45.91, 54.28 31.2, 80.69 1.24 M38.18 62.34 C52.77 42.19, 71.46 27.04, 91.32 1.2 M38.18 62.34 C59.02 38.46, 79.33 15.81, 91.32 1.2 M48.81 62.3 C58.76 49.19, 73.54 34.62, 101.95 1.17 M48.81 62.3 C61.09 47.24, 74.19 31.99, 101.95 1.17 M58.78 63.02 C75.37 42.21, 90.01 22.35, 112.58 1.14 M58.78 63.02 C81.08 38.93, 101.69 14.47, 112.58 1.14 M69.41 62.99 C81.43 48.86, 94.51 34.06, 123.21 1.1 M69.41 62.99 C86.77 42.02, 104.43 22.56, 123.21 1.1 M80.04 62.95 C91.66 48.28, 102.52 35.8, 133.18 1.82 M80.04 62.95 C90.55 49.65, 102.57 36.64, 133.18 1.82 M90.67 62.92 C103.27 46.55, 116.74 33.05, 139.22 7.07 M90.67 62.92 C102.98 49.17, 114.97 35.71, 139.22 7.07 M101.3 62.88 C110.2 50.92, 119.06 43.96, 141.32 16.85 M101.3 62.88 C110.77 52.33, 118.91 43.2, 141.32 16.85 M111.93 62.85 C120.35 53.56, 129.87 42.86, 140.8 29.64 M111.93 62.85 C117.93 55.66, 125.31 47.48, 140.8 29.64 M121.9 63.57 C128.21 56.8, 132.34 50.79, 140.93 41.68 M121.9 63.57 C129.92 55.09, 136.66 47.43, 140.93 41.68" stroke="#a5d8ff" stroke-width="1" fill="none"></path><path d="M15.75 0 C37.23 -0.43, 61.21 -1.61, 125.25 0 M15.75 0 C43.14 0.46, 71.35 0.81, 125.25 0 M125.25 0 C136.4 1.73, 139.6 6.26, 141 15.75 M125.25 0 C133.81 1.88, 142.54 5.1, 141 15.75 M141 15.75 C140.27 22.97, 142.3 30.49, 141 47.25 M141 15.75 C140.7 23.18, 141.45 31.11, 141 47.25 M141 47.25 C141.34 58.02, 136.02 64.01, 125.25 63 M141 47.25 C141.44 58.35, 134.46 62.13, 125.25 63 M125.25 63 C97.29 63.4, 73.86 65.31, 15.75 63 M125.25 63 C97.46 62.43, 69.79 61.51, 15.75 63 M15.75 63 C4.51 62.2, 0.16 57.18, 0 47.25 M15.75 63 C5.71 60.8, -0.09 58.48, 0 47.25 M0 47.25 C-1.38 40.84, -0.42 32.94, 0 15.75 M0 47.25 C-0.96 38.83, -0.74 30.57, 0 15.75 M0 15.75 C-0.65 4.74, 7.23 1.82, 15.75 0 M0 15.75 C0.18 3.03, 6.12 -0.43, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(328.86000061035156 156.52862548828125) rotate(0 46.13999938964844 25)"><text x="46.13999938964844" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Message </text><text x="46.13999938964844" y="42.519999999999996" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Brocker</text></g><g stroke-linecap="round" transform="translate(369 247.19529215494777) rotate(0 78 47.33333333333337)"><path d="M23.67 0 C55.62 -0.52, 88.39 -1.08, 132.33 0 M23.67 0 C56.5 0.49, 87.99 0.13, 132.33 0 M132.33 0 C146.42 1.63, 157.34 7.76, 156 23.67 M132.33 0 C146.55 1.13, 156.88 6.87, 156 23.67 M156 23.67 C154.89 37.01, 155.81 51.26, 156 71 M156 23.67 C156.43 38.78, 156.39 54.13, 156 71 M156 71 C156.38 87.3, 146.99 93.91, 132.33 94.67 M156 71 C154.56 88.18, 150.31 92.38, 132.33 94.67 M132.33 94.67 C102.28 94.45, 72.93 95.14, 23.67 94.67 M132.33 94.67 C98.46 93.35, 65.41 93.46, 23.67 94.67 M23.67 94.67 C8.29 92.75, -0.08 87.41, 0 71 M23.67 94.67 C6.59 93.29, -0.36 84.98, 0 71 M0 71 C-0.45 54.93, 2.11 35.08, 0 23.67 M0 71 C-0.06 56.09, 1.25 42.6, 0 23.67 M0 23.67 C0.16 5.96, 8.64 -0.37, 23.67 0 M0 23.67 C-2.11 6.46, 8.45 0.55, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(383 256.7952921549479) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 3}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round" transform="translate(538 246.19529215494777) rotate(0 78 47.33333333333337)"><path d="M23.67 0 C50.02 -0.85, 77.09 1.57, 132.33 0 M23.67 0 C64.83 0.27, 108.35 -0.61, 132.33 0 M132.33 0 C146.75 0.99, 156.76 7, 156 23.67 M132.33 0 C148.01 1.32, 156.14 6.3, 156 23.67 M156 23.67 C156.22 37.42, 157.54 49.31, 156 71 M156 23.67 C156.25 38.78, 155.5 52.98, 156 71 M156 71 C154.74 88, 150.02 92.68, 132.33 94.67 M156 71 C155.26 87.42, 149.56 93.58, 132.33 94.67 M132.33 94.67 C104.79 94.33, 75.22 93.8, 23.67 94.67 M132.33 94.67 C104.26 93.4, 75.75 94.68, 23.67 94.67 M23.67 94.67 C6.76 93.47, -0.31 85.21, 0 71 M23.67 94.67 C9.34 93.97, 1.18 86.1, 0 71 M0 71 C-0.27 53.06, 0.23 36.73, 0 23.67 M0 71 C-0.39 57.04, -0.09 44.82, 0 23.67 M0 23.67 C-1.84 6.65, 8.38 0.48, 23.67 0 M0 23.67 C-0.43 6.09, 9.5 -1.39, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(552 255.79529215494813) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "352j-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g stroke-linecap="round"><g transform="translate(305 207.52862548828125) rotate(0 -139.5 15.5)"><path d="M0.39 1.04 C-46.08 6.24, -233.11 26.45, -279.84 31.6 M-0.87 0.54 C-46.86 5.35, -231.44 24.86, -277.57 29.8" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(447 206.52862548828125) rotate(0 119 14.5)"><path d="M-0.84 0.6 C38.59 5.43, 197.05 25.26, 236.99 29.98 M0.92 -0.13 C40.68 4.9, 199.58 23.32, 239.17 28.37" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(10 235.52862548828125) rotate(0 351 59)"><path d="M29.5 0 C177.94 2.7, 325.03 1.63, 672.5 0 M29.5 0 C254.09 1.37, 478.74 0.86, 672.5 0 M672.5 0 C691.51 -0.46, 701.6 10.16, 702 29.5 M672.5 0 C693.43 1.89, 702.39 10.15, 702 29.5 M702 29.5 C702.82 46.86, 701.54 65.21, 702 88.5 M702 29.5 C702.24 51.08, 703.19 71.62, 702 88.5 M702 88.5 C701.1 106.98, 691.95 118.17, 672.5 118 M702 88.5 C701.75 106.22, 691.31 117.08, 672.5 118 M672.5 118 C473.66 118.48, 276.6 118.06, 29.5 118 M672.5 118 C518.38 115.87, 363.95 115.87, 29.5 118 M29.5 118 C10.94 119.97, -1.57 108.36, 0 88.5 M29.5 118 C8.54 118.81, -0.74 107.58, 0 88.5 M0 88.5 C-2.47 65.73, -0.91 39.61, 0 29.5 M0 88.5 C-0.33 73.69, 0.56 59.83, 0 29.5 M0 29.5 C-0.28 9.9, 8.48 1.05, 29.5 0 M0 29.5 C-1.63 10.59, 7.91 2.08, 29.5 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g stroke-linecap="round" transform="translate(563.015380859375 112.93130493164062) rotate(0 70.5 31.5)"><path d="M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M2.34 6.01 C2.34 6.01, 2.34 6.01, 2.34 6.01 M4.44 15.79 C10.18 11.82, 12.21 4.06, 16.91 1.45 M4.44 15.79 C8.72 12.93, 10.43 8.76, 16.91 1.45 M4.57 27.83 C8.45 22.17, 14.33 16.06, 27.54 1.41 M4.57 27.83 C11.77 18.55, 21.34 9.67, 27.54 1.41 M4.71 39.87 C14.1 30.84, 24.1 21.65, 38.17 1.38 M4.71 39.87 C11.2 30.16, 18.86 22.06, 38.17 1.38 M3.53 53.42 C20.86 32.97, 35.56 16.43, 48.8 1.34 M3.53 53.42 C16.9 37.91, 28.52 24.54, 48.8 1.34 M6.94 61.68 C26.06 40.02, 46.95 18.98, 59.43 1.31 M6.94 61.68 C25.59 39.44, 44.12 17.51, 59.43 1.31 M13.64 66.18 C31.81 42.83, 50.06 23.2, 70.06 1.27 M13.64 66.18 C24.13 52.93, 35.77 40.09, 70.06 1.27 M24.27 66.14 C39.18 50.56, 51.8 35.72, 80.03 1.99 M24.27 66.14 C38.13 50.29, 51.03 33.79, 80.03 1.99 M34.9 66.11 C53.23 44.59, 67.73 26.17, 90.66 1.96 M34.9 66.11 C54.86 43.95, 75.7 19.31, 90.66 1.96 M45.53 66.08 C64.15 41.8, 86.2 21.7, 101.29 1.92 M45.53 66.08 C58.42 50.05, 72.64 33.77, 101.29 1.92 M55.5 66.8 C74.93 42.57, 96.88 17.11, 111.92 1.89 M55.5 66.8 C73.01 45.63, 92.42 24.2, 111.92 1.89 M66.13 66.76 C77.24 52.17, 91.06 38.22, 122.55 1.86 M66.13 66.76 C85.49 44.44, 104.36 23.09, 122.55 1.86 M76.76 66.73 C98.38 41.42, 120.02 16.5, 133.18 1.82 M76.76 66.73 C94.74 46.15, 112.44 24.86, 133.18 1.82 M87.39 66.69 C104.52 45.93, 122.31 22.85, 139.22 7.07 M87.39 66.69 C102.69 50.6, 116.71 32.13, 139.22 7.07 M98.02 66.66 C112.04 50.17, 126.1 32.98, 143.29 14.58 M98.02 66.66 C110.12 51.1, 124.16 35.69, 143.29 14.58 M108.65 66.62 C123.08 53.24, 134.48 35.71, 143.42 26.62 M108.65 66.62 C119.07 56.09, 127.57 44.64, 143.42 26.62 M118.62 67.34 C126.85 58.55, 135.97 48.66, 142.9 39.42 M118.62 67.34 C126.61 59.46, 133.19 51.3, 142.9 39.42" stroke="#b2f2bb" stroke-width="1" fill="none"></path><path d="M15.75 0 C46.28 -0.52, 79.88 1.02, 125.25 0 M15.75 0 C59.54 -0.38, 102.63 0.49, 125.25 0 M125.25 0 C134.17 1.81, 142.75 4.22, 141 15.75 M125.25 0 C135.78 0.27, 141.41 6.32, 141 15.75 M141 15.75 C141.81 24.9, 140.58 31.93, 141 47.25 M141 15.75 C141.82 27.14, 141.31 38.47, 141 47.25 M141 47.25 C139.44 58.12, 136.65 62.98, 125.25 63 M141 47.25 C142.74 55.5, 133.95 64.17, 125.25 63 M125.25 63 C102.1 62.14, 81.88 61.07, 15.75 63 M125.25 63 C100.11 62.04, 75.75 63.88, 15.75 63 M15.75 63 C7.03 62.55, -0.84 58.99, 0 47.25 M15.75 63 C4.98 62.95, 1.89 55.93, 0 47.25 M0 47.25 C-1.84 33.2, -0.35 24.44, 0 15.75 M0 47.25 C-0.69 35.18, -1.17 22.07, 0 15.75 M0 15.75 C0.17 6.08, 5.97 0.49, 15.75 0 M0 15.75 C1.17 5.63, 5.16 0.53, 15.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(590.0053825378418 131.93130493164062) rotate(0 43.5099983215332 12.5)"><text x="43.5099983215332" y="17.52" font-family="Virgil, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Analytics</text></g><g stroke-linecap="round"><g transform="translate(449.515380859375 169.2697681584525) rotate(0 54 -4.803119436582733)"><path d="M0.6 -0.24 C18.41 -1.92, 89.8 -8.68, 107.57 -10.16 M-0.54 -1.42 C17.02 -2.92, 88.03 -7.27, 106.39 -8.92" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(449.515380859375 169.2697681584525) rotate(0 54 -4.803119436582733)"><path d="M83.59 1.34 C91.36 -3.17, 99 -7.48, 106.39 -8.92 M83.59 1.34 C91.07 -2.82, 98.37 -5.16, 106.39 -8.92" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(449.515380859375 169.2697681584525) rotate(0 54 -4.803119436582733)"><path d="M82.33 -15.71 C90.53 -14.12, 98.62 -12.33, 106.39 -8.92 M82.33 -15.71 C90.39 -14.14, 98.12 -10.73, 106.39 -8.92" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(181.515380859375 404.8078161104661) rotate(0 98.84110260009766 10.654029152495013)"><text x="0" y="17.146328167296705" font-family="Cascadia, Segoe UI Emoji" font-size="17.7567152541584px" fill="#1971c2" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">offset:mensageria:2</text></g><g stroke-linecap="round" transform="translate(26.515380859375 248.72248331705714) rotate(0 78 47.33333333333337)"><path d="M23.67 0 C53.41 0.57, 85.11 -1.62, 132.33 0 M23.67 0 C63.28 -0.32, 100.62 0.95, 132.33 0 M132.33 0 C149.81 1.68, 154.72 6.59, 156 23.67 M132.33 0 C147.19 0.63, 157.2 8.81, 156 23.67 M156 23.67 C157.52 35.15, 157.69 44.59, 156 71 M156 23.67 C155.63 38.58, 157.16 52.25, 156 71 M156 71 C156.96 87.25, 146.45 95.21, 132.33 94.67 M156 71 C155.01 87.6, 148.04 94.76, 132.33 94.67 M132.33 94.67 C97.47 92.47, 63.04 95.62, 23.67 94.67 M132.33 94.67 C97.19 95.89, 62.46 94.29, 23.67 94.67 M23.67 94.67 C8.86 95.97, -1.75 87.66, 0 71 M23.67 94.67 C8.63 92.56, 0.31 85.52, 0 71 M0 71 C1.98 60.38, -1.73 47.41, 0 23.67 M0 71 C-0.46 60.58, -1.01 51.25, 0 23.67 M0 23.67 C-0.12 7.35, 7.56 -1.4, 23.67 0 M0 23.67 C1.87 7.64, 5.91 0.58, 23.67 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(40.515380859375 258.32248331705705) rotate(0 64.6666488647461 38.627555555555546)"><text x="0" y="8.880888888888883" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">{message_id: "fsd3-..."</text><text x="0" y="19.917333333333318" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> client_id:: "a324-...",</text><text x="0" y="30.953777777777756" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> products: [</text><text x="0" y="41.990222222222194" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    {sku: "231",</text><text x="0" y="53.02666666666663" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">     quantity: 1}</text><text x="0" y="64.06311111111106" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">    ...]</text><text x="0" y="75.0995555555555" font-family="Cascadia, Segoe UI Emoji" font-size="9.197037037037031px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic"> address:}</text></g><g transform="translate(533.4721221923828 404.4017874978956) rotate(0 93.6389389038086 10.654029152495013)"><text x="0" y="17.146328167296705" font-family="Cascadia, Segoe UI Emoji" font-size="17.7567152541584px" fill="#2f9e44" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">offset:analytics:4</text></g><g stroke-linecap="round"><g transform="translate(619.9688361510687 341.52862548828125) rotate(0 -0.1594167865938516 25.763595581054688)"><path d="M1.09 0.02 C0.77 8.91, -0.85 44, -1 52.59" stroke="#2f9e44" stroke-width="2.5" fill="none" stroke-dasharray="1.5 8"></path></g><g transform="translate(619.9688361510687 341.52862548828125) rotate(0 -0.1594167865938516 25.763595581054688)"><path d="M-8.75 28.82 C-5.97 36.53, -1.32 47.44, -1 52.59" stroke="#2f9e44" stroke-width="2.5" fill="none" stroke-dasharray="1.5 6"></path></g><g transform="translate(619.9688361510687 341.52862548828125) rotate(0 -0.1594167865938516 25.763595581054688)"><path d="M8.34 29.4 C4.72 37.04, 2.96 47.74, -1 52.59" stroke="#2f9e44" stroke-width="2.5" fill="none" stroke-dasharray="1.5 6"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(276.515380859375 344.0558166503906) rotate(0 0.5 28.5)"><path d="M0.45 0.77 C0.37 10.42, 0.15 48.27, 0.04 57.84" stroke="#1971c2" stroke-width="2.5" fill="none" stroke-dasharray="1.5 8"></path></g><g transform="translate(276.515380859375 344.0558166503906) rotate(0 0.5 28.5)"><path d="M-8.32 34.28 C-4.17 40.72, -1.21 48.45, 0.04 57.84" stroke="#1971c2" stroke-width="2.5" fill="none" stroke-dasharray="1.5 6"></path></g><g transform="translate(276.515380859375 344.0558166503906) rotate(0 0.5 28.5)"><path d="M8.78 34.41 C7.07 40.88, 4.18 48.55, 0.04 57.84" stroke="#1971c2" stroke-width="2.5" fill="none" stroke-dasharray="1.5 6"></path></g></g><mask></mask></svg>

<p>No cenário acima, temos dois consumidores, cada um com um <em>offset</em> diferente. Como mandar e-mails é um processo mais demorado que salvar no ambiente analítico, então é normal o <em>offset</em> da mensageria ficar “atrasado” em relação ao processo analítico.</p>

<p>Essa abordagem de PubSub é mais flexível, pois facilita o cenário de múltiplos consumidores, que é uma demanda bastante comum. Por que utilizar filas então, se existe essa limitação de um único consumidor? A simplicidade é o principal motivo, já que o paradgima PubSub tem vários detalhes que precisam ser tratados.</p>

<p>Ao trabalhar com <em>offsets</em>, existem diferentes formas de lidar com a atualização a depender das semânticas de consumo: <em>at-most-once</em>, <em>exactly-once</em> e <em>at-least-one</em>. Usando filas, basta dar um “ack” para garantir um consumo <em>exactly-once</em>.</p>

<h1 id="arquitetos-e-programadores">Arquitetos e programadores</h1>

<p>Em geral, os <em>trade-offs</em> de síncrono e assíncrono se repetem, independente do problema de negócio e do tipo de broker utilizado. Da perspectiva de arquitetura, quase sempre é melhor ser assíncrono e no modelo PubSub, mas programadores ficam ressabiados com os desafios de implementação.</p>

<p>Arquitetos se preocupam muito com escalabilidade e custos, pontos fortes da comunicação assíncrona. Para os desenvolvedores, por outro lado, é necessário mais esforço para sustentação e os desafios de desenvolvimento são maiores.</p>

<p>O mundo seria mais simples para programadores, se os sistemas fossem sempre integrados usandos APIs REST síncronas, mas a realidade é que precisamos lidar com tópicos e o paradigma PubSub.</p>

<h2 id="semânticas-de-consumo">Semânticas de consumo</h2>

<p>A mensageria é um problema que se encaixa bem com a ideia de eventos, mas é necessário cuidado ao usar tópicos no lugar de filas. A forma de lidar com o consumo depende da natureza da comunicação, uma decisão que precisa ser tomada com o negócio em vista.</p>

<p>Podemos consumir um tópico seguindo uma dessas semânticas:</p>

<ul>
  <li><em>at-most-once</em> significa que a mensagem será consumida no máximo uma vez, mas pode ser perdida;</li>
  <li><em>at-least-once</em> significa que todas as mensagens serão consumidas uma vez, mas podem  ser consumidas mais de uma vez;</li>
  <li><em>exactly-once</em> significa que todas as mensagens serão consumidas uma única vez.</li>
</ul>

<p>O ideal seria tudo ser <em>exactly-once</em>, mas não é algo que vem de graça. Para garantir essa semântica, é necessário um controle externo de <em>offsets</em> e há limites para a escalabilidade do consumidor, enquanto as outras abordagens são mais simples e escaláveis.</p>

<p>Considerando o cenário de e-mails para confirmação de compra, é interessante pensar na abordagem <em>exactly-once</em>, mesmo com as dificuldades:</p>

<ul>
  <li>
    <p>mensagens enviadas por e-mails, normalmente não demandam urgência e são pouco volumosas, o que reduz os problema de escalabilidade;</p>
  </li>
  <li>
    <p>por outro lado, enviar e-mails repetidos é algo ruim para experiência e pode ser classificado como spam pelos serviços;</p>
  </li>
  <li>
    <p>no cenário de um fluxo de compras, perder mensagens com comprovantes pode ser problemático para o consumidor e o negócio.</p>
  </li>
</ul>

<p>Essa discussão seria muito diferente, se pensarmos em mensageria de notificações para uma rede social por exemplo, que normalmente é um cenário de grande volume e menor criticidade. As vantagens de escalabilidade de uma abordagem <em>at-most-once</em> ou <em>at-least-once</em> podem ser indispensáveis, mesmo que exista o risco de perder ou repetir notificações.</p>

<h2 id="garantindo-exactly-once">Garantindo <em>exactly-once</em></h2>

<p>Começando pelo cenário <em>exactly-once</em>, podemos nos basear na solução do <a href="https://docs.confluent.io/kafka-clients/python/current/overview.htm">tutorial da Confluent</a>, que é a implementação mais simples possível de um consumidor.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>

    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>

        <span class="n">msg</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">poll</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">msg</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">continue</span>

        <span class="nf">process_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">())</span>

<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Nessa solução, cada mensagem consumida é processada logo em seguida, mas não há controle explícito dos <em>offsets</em>. Como o objetivo é trabalhar com <em>exactly-once</em>, o ideal é desativar o <code class="language-plaintext highlighter-rouge">commit</code>automático. O consumidor deve controlar a atualização manualmente e de forma síncrona programaticamente.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>

    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>    
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>

    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>

        <span class="n">msg</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">poll</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">msg</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">continue</span>

        <span class="nf">process_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">())</span>

        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span> <span class="c1"># Commit explícito para cada mensagem
</span>
<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Nesse código, aparentemente é garantido o consumo <em>exactly-once</em>, mas e se houver um erro ao executar <code class="language-plaintext highlighter-rouge">process_message</code>? Nesse cenário, se faz necessário controle externo – fila morta, banco de dados, cache – para guardar as mensagens que deram erro.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>

    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>    
    <span class="n">start_time</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>

    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>

        <span class="n">msg</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">poll</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">msg</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">continue</span>

        <span class="n">ok</span> <span class="o">=</span> <span class="nf">process_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">())</span>

        <span class="k">if</span> <span class="ow">not</span> <span class="n">ok</span><span class="p">:</span>
          <span class="nf">dlq</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="c1"># Salvando mensagens em uma dlq (dead letter queue)
</span>
        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

        <span class="k">if</span> <span class="n">count</span> <span class="o">==</span> <span class="n">num_records</span><span class="p">:</span> <span class="k">break</span>

<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Sem um controle externo para salvar as mensagens com erro, é necessário parar o processamento do tópico: não é possível atualizar o <em>offset</em> para a mensagem \(x_{i}\), se existe uma mensagem \(x_{j}\) \(\forall j &lt; i\) não processada.</p>

<p>Para esse cenário <em>exactly-once</em>, as filas costumam funcionar melhor, já que as mensagens são controladas individualmente e não por offsets. Soluções de fila, como <a href="https://aws.amazon.com/what-is/dead-letter-queue/">SQS da Amazon</a> e <a href="https://www.rabbitmq.com/docs/dlx">RabbitMQ</a> por exemplo, têm o recurso de fila morta para facilitar a gestão de mensagens problemáticas.</p>

<p>No caso do tópico Kafka, o usuário precisa estar ciente desse problema e implementar uma solução por fora. Ou seja, é possível implementar a semântica <em>exactly-once</em> em tópicos, mas é necessário cuidado com <em>offsets</em> e talvez demande ferramentas extras para o tratamento adequado de erros.</p>

<p>Apesar de não ser a melhor estratatégia para consumir um tópico, recomendo começar pela semântica <em>exactly-once</em> até que necessidades de escalabilidade apareçam. As outras semânticas podem trazer ganhos expressivos, mas criam outras dificuldades e perde-se garantias.</p>

<p>Um equívoco comum, quando surge a necessidade de escalar um consumidor Kafka, é misturar a leitura do tópico com o processamento da mensagem. É importante entender essa diferença, porque a solução dos problemas vão em direção oposta: aumentar o <em>throughput</em> de leitura, quando o gargalo é de processamento, poder causar <em>overflow</em> no consumidor.</p>

<p>A ideia é discutir os gargalos de processamento das mensagens, não do consumo propriamente dito, pois vejo poucas discussões sobre o assunto e muitas dificuldades por parte dos desenvolvedores. É uma questão que depende muito do problema a ser resolvido – aplicar um modelo de <em>machine learning</em>, salvar as mensagens em um banco de dados e agregar informações são tarefas muito diferentes – mas existem estratégias que podem ser aplicadas otogonalmente à natureza do problema.</p>

<h2 id="um-problema-io-bound">Um problema <em>“I/O bound”</em></h2>

<p>Voltando ao nosso cenário de mensageria, imagine que essas mensagens precisem ser salvas em um banco de dados, para uso analítico e acompanhamento da operação. A solução é consumir continuamente as mensagens postadas no tópico e salvá-las em uma tabela do PostgreSQL.</p>

<p>Para ilustrar o problema, a estratégia foi produzir mensagens aleatórias serializadas como <code class="language-plaintext highlighter-rouge">json</code> em um tópico. Abaixo, um exemplo de mensagem gerada.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"message_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2268086d-5d3f-40e2-80ce-fc9f4e3008dc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8592b337-168e-4304-82d3-0d36b556e58b"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"products"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"sku"</span><span class="p">:</span><span class="w"> </span><span class="s2">"d33b284b-c62e-47d7-b8e8-2048fe2d9da5"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
            </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"sku"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aebbb957-e16e-43d2-b742-198c12ef96a8"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mi">16387</span><span class="p">,</span><span class="w">
            </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"sku"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9f8c0bfe-5e9b-4822-84b7-7b8e86a6469d"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mi">6750</span><span class="p">,</span><span class="w">
            </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"sku"</span><span class="p">:</span><span class="w"> </span><span class="s2">"79c5824b-ac16-4b59-8bda-31328a0204ec"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
            </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Para geração dessas mensagens, o script abaixo foi utilizado.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_sample</span><span class="p">():</span>
    
    <span class="n">σ_price</span> <span class="o">=</span> <span class="mi">2_000</span>
    <span class="n">μ_price</span> <span class="o">=</span> <span class="mi">10_000</span>

    <span class="n">quantity_range</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">10</span><span class="p">)</span>
    <span class="n">products_range</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">5</span><span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">message_id</span><span class="sh">'</span><span class="p">:</span> <span class="nf">str</span><span class="p">(</span><span class="nf">uuid4</span><span class="p">()),</span>
        <span class="sh">'</span><span class="s">client_id</span><span class="sh">'</span><span class="p">:</span> <span class="nf">str</span><span class="p">(</span><span class="nf">uuid4</span><span class="p">()),</span>
        <span class="sh">'</span><span class="s">products</span><span class="sh">'</span><span class="p">:</span> <span class="p">[{</span><span class="sh">"</span><span class="s">sku</span><span class="sh">"</span><span class="p">:</span> <span class="nf">str</span><span class="p">(</span><span class="nf">uuid4</span><span class="p">()),</span>
                      <span class="sh">"</span><span class="s">price</span><span class="sh">"</span><span class="p">:</span> <span class="nf">max</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span>
                                   <span class="nf">int</span><span class="p">(</span><span class="n">σ_price</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randn</span><span class="p">()</span> <span class="o">*</span> <span class="n">μ_price</span><span class="p">)),</span>
                      <span class="sh">"</span><span class="s">quantity</span><span class="sh">"</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randint</span><span class="p">(</span><span class="o">*</span><span class="n">quantity_range</span><span class="p">)}</span>
                     <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randint</span><span class="p">(</span><span class="o">*</span><span class="n">products_range</span><span class="p">))]</span>
    <span class="p">}</span> 
</code></pre></div></div>

<p>As mensagens serão salvas em uma tabela, com dois campos (<code class="language-plaintext highlighter-rouge">message_id</code> e <code class="language-plaintext highlighter-rouge">client_id</code>) que armazenam dados extraídos da mensagem e uma coluna de <code class="language-plaintext highlighter-rouge">content</code> para guardar o conteúdo original do tópico.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">table</span> <span class="n">messages</span><span class="p">(</span><span class="n">message_id</span> <span class="nb">char</span><span class="p">(</span><span class="mi">36</span><span class="p">)</span> <span class="k">primary</span> <span class="k">key</span><span class="p">,</span> 
                      <span class="n">client_id</span> <span class="nb">char</span><span class="p">(</span><span class="mi">36</span><span class="p">),</span>
                      <span class="nb">datetime</span> <span class="nb">timestamp</span><span class="p">,</span>
                      <span class="n">content</span> <span class="nb">text</span><span class="p">)</span>
</code></pre></div></div>

<p>Para salvar a mensagem, será utilizada essa função, que faz um <em>parse</em> do <code class="language-plaintext highlighter-rouge">json</code> e insere na tabela que foi descrita.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msg</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">callable</span><span class="p">:</span>

    <span class="n">content</span> <span class="o">=</span>  <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>

    <span class="n">values</span> <span class="o">=</span> <span class="p">(</span>
        <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">message_id</span><span class="sh">'</span><span class="p">],</span>
        <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">client_id</span><span class="sh">'</span><span class="p">],</span>
        <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(),</span>
        <span class="n">msg</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="n">insert</span> <span class="o">=</span> <span class="sh">"</span><span class="s">insert into messages VALUES (%s, %s, %s, %s)</span><span class="sh">"</span>

    <span class="k">with</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
        <span class="n">cur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">insert</span><span class="p">,</span> <span class="n">values</span><span class="p">)</span>

    <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
</code></pre></div></div>

<p>O problema proposto é <em><a href="https://en.wikipedia.org/wiki/I/O_bound">I/O bound</a></em> e <a href="https://en.wikipedia.org/wiki/Data_parallelism">facilmente paralelizável</a>, um cenário comum em sistemas de informação e que podemos ter ganhos expressivos de performance com um bom desenho de solução.</p>

<h2 id="os-limites-do-exactly-once">Os limites do <em>exactly-once</em></h2>

<p>A primeira opção para resolver esse problema, é utilizar a solução do consumidor <em>exactly-once</em>, substituindo a função <code class="language-plaintext highlighter-rouge">process_message</code> pela <code class="language-plaintext highlighter-rouge">save_message</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">while</span> <span class="n">count</span> <span class="o">&lt;</span> <span class="n">num_records</span><span class="p">:</span>
        
        <span class="n">msg</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">poll</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">msg</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">continue</span>

        <span class="nf">save_message</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">())</span>

        <span class="n">count</span><span class="o">+=</span><span class="mi">1</span>
        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
<span class="k">finally</span><span class="p">:</span>
    <span class="n">conn</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Para inserir 1.000.000 de registros, esse script demorou 47 minutos. O gargalo desse processo é escrever no banco de dados. Removendo a etapa de inserção – mas mantendo o <em>pull</em> das mensagens, <em>parse</em> e formatação para o modelo de dados – o processo demorou 16 segundos.</p>

<p>Fiz esse experimento, para ilustar que esse é um cenário de problemas com o processamento da mensagem. Otimizações no consumo e/ou brokers não fazem sentido, já que essa parte está ocupando uma pequena fração do tempo total de processamento.</p>

<h2 id="asyncio-para-remover-o-gargalo">Asyncio para remover o gargalo</h2>

<p>A lentidão dessa solução é devido a ausência de concorrência, executar esse tipo de trabalho de forma sequencial é um desperdício em dois sentidos:</p>

<ul>
  <li>
    <p>o processo do consumidor fica pausado, enquanto espera salvar no banco de dados;</p>
  </li>
  <li>
    <p>os bancos de dados normalmente são desenhados para lidar bem com escritas concorrentes, escrever vários registros sequencialmente não tira proveito disso.</p>
  </li>
</ul>

<p>Existem várias alternativas para implementar concorrência em Python, uma delas 
é o uso de <code class="language-plaintext highlighter-rouge">asyncio</code> para trabalhar com tarefas em segundo plano. Essa estratégia depende do suporte das bibliotecas utilizadas, então nem sempre é possível utilizá-la.</p>

<p>A biblioteca <a href="https://www.psycopg.org/psycopg3/docs/advanced/async.html">psycopg</a>, utilizada para conexão com o banco de dados, tem suporte a <code class="language-plaintext highlighter-rouge">asyncio</code>. A função <code class="language-plaintext highlighter-rouge">save_message</code> foi re-escrita de forma assíncrona como <code class="language-plaintext highlighter-rouge">asave_message</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">asave_message</span><span class="p">(</span><span class="n">aconn</span><span class="p">,</span> <span class="n">msg</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    
    <span class="k">async</span> <span class="k">with</span> <span class="n">aconn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">acur</span><span class="p">:</span>

        <span class="n">content</span> <span class="o">=</span>  <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>

        <span class="n">values</span> <span class="o">=</span> <span class="p">(</span>
            <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">message_id</span><span class="sh">'</span><span class="p">],</span>
            <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">client_id</span><span class="sh">'</span><span class="p">],</span>
            <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(),</span>
            <span class="n">msg</span><span class="p">,</span>
        <span class="p">)</span>

        <span class="n">insert</span> <span class="o">=</span> <span class="sh">"</span><span class="s">insert into messages VALUES (%s, %s, %s, %s)</span><span class="sh">"</span>

        <span class="k">await</span> <span class="n">acur</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">insert</span><span class="p">,</span> <span class="n">values</span><span class="p">)</span>
</code></pre></div></div>

<p>A programação assíncrona com <code class="language-plaintext highlighter-rouge">asyncio</code> tem vários conceitos como tarefas, corotinas, etc – eu mesmo tenho uma compreensão superficial – então recomendo a <a href="https://docs.python.org/3/library/asyncio-task.html#coroutine">documentação</a> e <a href="https://realpython.com/python-async-features/">outros materiais</a>, caso o leitor queira entender melhor a implementação. Não é necessário compreender a implementação para a leitura do post, mas é interessante entender a ideia do que está acontecendo após essa mudança.</p>

<p>Na versão assíncrona, o processo não espera o término da inserção pra iniciar as demais. Aproveita-se melhor o tempo de CPU e o tempo total de execução é bem reduzido, considerando que o banco de dados lida bem com escritas concorrentes.</p>

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 838.8346802883782 598.2265625" width="700" height="500">
  <!-- svg-source:excalidraw -->
  
  <defs>
    <style class="style-fonts">
      @font-face {
        font-family: "Virgil";
        src: url("https://excalidraw.com/Virgil.woff2");
      }
      @font-face {
        font-family: "Cascadia";
        src: url("https://excalidraw.com/Cascadia.woff2");
      }
      @font-face {
        font-family: "Assistant";
        src: url("https://excalidraw.com/Assistant-Regular.woff2");
      }
    </style>
    
  </defs> 
  <g transform="translate(16.393386314902443 10) rotate(0 54.544002532958984 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Síncrono</text></g><g stroke-linecap="round"><g transform="translate(12.287383507285256 196.9765625) rotate(0 402 -0.5)"><path d="M-0.85 -0.23 C133.18 -0.39, 670.86 -2.03, 804.83 -1.96 M0.91 -1.4 C134.8 -1.36, 670.24 -0.9, 804.31 -0.93" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(12.287383507285256 196.9765625) rotate(0 402 -0.5)"><path d="M780.82 7.62 C788.79 6.14, 795.88 0.01, 804.31 -0.93 M780.82 7.62 C791.16 3.82, 800.17 0.88, 804.31 -0.93" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(12.287383507285256 196.9765625) rotate(0 402 -0.5)"><path d="M780.82 -9.49 C788.65 -4.92, 795.74 -5.01, 804.31 -0.93 M780.82 -9.49 C791.14 -6.45, 800.14 -2.57, 804.31 -0.93" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask><g transform="translate(331.28738350728526 213.4765625) rotate(0 24.248001098632812 10)"><text x="0" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Tempo</text></g><g stroke-linecap="round" transform="translate(14.287383507285256 134.9765625) rotate(0 21.5 22.5)"><path d="M10.75 0 C19.4 0.43, 20 -1.64, 32.25 0 C37.89 3.4, 40.38 4.14, 43 10.75 C42.58 20.73, 44.5 22.01, 43 34.25 C45.49 43.66, 40.6 44.42, 32.25 45 C24.53 43.07, 17.47 44.78, 10.75 45 C6.07 43.93, -1.82 43.09, 0 34.25 C-3.54 30.01, -2.21 16.98, 0 10.75 C-1.46 7.02, 3.84 -1.29, 10.75 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M10.75 0 C18.01 -0.65, 23.7 2.05, 32.25 0 M10.75 0 C17.36 0.91, 25.29 -0.88, 32.25 0 M32.25 0 C41.31 -0.13, 44.2 3.8, 43 10.75 M32.25 0 C38.97 -1.26, 42.98 3.93, 43 10.75 M43 10.75 C42.01 15.73, 42.64 24.09, 43 34.25 M43 10.75 C43.35 17.2, 42.08 25.21, 43 34.25 M43 34.25 C43.43 41.4, 41 45.58, 32.25 45 M43 34.25 C43.96 42.24, 41.3 44.93, 32.25 45 M32.25 45 C27.27 43.84, 23.31 43.03, 10.75 45 M32.25 45 C25.18 44.81, 17.15 45.06, 10.75 45 M10.75 45 C3.5 43.77, -0.63 41.68, 0 34.25 M10.75 45 C3.63 46.12, -1.97 41.78, 0 34.25 M0 34.25 C1.29 27.28, -0.77 17.76, 0 10.75 M0 34.25 C-0.4 28.5, -0.25 22.54, 0 10.75 M0 10.75 C-0.67 4.67, 5.58 -0.8, 10.75 0 M0 10.75 C-1.67 3.87, 4.24 -2.08, 10.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(19.635383080039162 147.4765625) rotate(0 16.152000427246094 10)"><text x="16.152000427246094" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CPU</text></g><g stroke-linecap="round" transform="translate(61.287383507285256 84.4765625) rotate(0 96 22.5)"><path d="M11.25 0 C75.89 2.96, 136.17 1.97, 180.75 0 C186.68 -2.61, 193.06 7.03, 192 11.25 C188.68 16.73, 188.82 31.03, 192 33.75 C190.2 44.15, 189.8 46.36, 180.75 45 C134.08 43.33, 89.87 42.89, 11.25 45 C6.94 42.75, -2.91 37.66, 0 33.75 C0.32 29.17, -1.52 25.34, 0 11.25 C-0.71 0.16, 7.3 -1.25, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C72.66 -2.98, 135.62 0.51, 180.75 0 M11.25 0 C63.73 -1.29, 115.44 -1.9, 180.75 0 M180.75 0 C188.42 0.95, 193.84 2.28, 192 11.25 M180.75 0 C190.2 0.68, 192.51 5.75, 192 11.25 M192 11.25 C192.97 20.75, 192.11 25.46, 192 33.75 M192 11.25 C191.21 17.95, 192.32 23.88, 192 33.75 M192 33.75 C191.53 42.68, 187.42 44.63, 180.75 45 M192 33.75 C190.73 40.17, 188.7 43.9, 180.75 45 M180.75 45 C134.85 44.02, 85.05 44.36, 11.25 45 M180.75 45 C124.63 43.21, 70.19 44.39, 11.25 45 M11.25 45 C5.47 43.12, 1.3 40.73, 0 33.75 M11.25 45 C2.91 45.36, 1.15 42.36, 0 33.75 M0 33.75 C-0.19 25.82, -2.05 19.45, 0 11.25 M0 33.75 C0.7 26.79, 1.17 21.57, 0 11.25 M0 11.25 C-1.37 4.26, 2.25 -0.85, 11.25 0 M0 11.25 C-1.11 1.95, 6.03 -2.23, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(147.11938328603281 96.9765625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g transform="translate(344.2992290575989 568.2265625) rotate(0 24.248001098632812 10)"><text x="0" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Tempo</text></g><g transform="translate(16.74338097432627 244.4765625) rotate(0 70.41999816894531 17.5)"><text x="0" y="24.528" font-family="Virgil, Segoe UI Emoji" font-size="28px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Assíncrono</text></g><g stroke-linecap="round" transform="translate(261.78738350728526 136.4765625) rotate(0 21.5 22.5)"><path d="M10.75 0 C20.75 1.15, 29.75 -2.98, 32.25 0 C42.2 1.3, 40.7 6.8, 43 10.75 C44.55 14.62, 45.99 27.56, 43 34.25 C44.89 41.89, 35.86 43.65, 32.25 45 C25.19 46.26, 23.88 43.79, 10.75 45 C2.39 41.63, -2.48 42.56, 0 34.25 C-1.19 27.62, 0.85 20.7, 0 10.75 C2.54 3.32, 1.39 0.23, 10.75 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M10.75 0 C17.93 1.86, 27.91 -0.47, 32.25 0 M10.75 0 C16.67 -0.13, 22.24 -0.27, 32.25 0 M32.25 0 C39.45 -1.61, 41.43 3.72, 43 10.75 M32.25 0 C38.25 1.38, 41.56 4.41, 43 10.75 M43 10.75 C41.55 18.48, 41.63 29.88, 43 34.25 M43 10.75 C42.99 17.23, 43.54 25.57, 43 34.25 M43 34.25 C41.58 39.78, 38.83 44.74, 32.25 45 M43 34.25 C43 41.93, 39.34 44.76, 32.25 45 M32.25 45 C25.21 46.99, 19.71 47, 10.75 45 M32.25 45 C25.61 44.5, 18.49 45.63, 10.75 45 M10.75 45 C1.97 43.56, 0.27 42.54, 0 34.25 M10.75 45 C2.54 45.38, 1.97 43.66, 0 34.25 M0 34.25 C1.77 26.89, -1.64 20, 0 10.75 M0 34.25 C0.57 24.62, -0.83 17.07, 0 10.75 M0 10.75 C-0.2 4.83, 4.38 -1.95, 10.75 0 M0 10.75 C1.25 5.38, 5.26 0.39, 10.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(267.13538308003916 148.9765625) rotate(0 16.152000427246094 10)"><text x="16.152000427246094" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CPU</text></g><g stroke-linecap="round" transform="translate(130.28738350728526 363.4765625) rotate(0 96 22.5)"><path d="M11.25 0 C73.07 -2.73, 130.5 -0.76, 180.75 0 C184.72 -0.18, 190.52 5.78, 192 11.25 C194.73 17.11, 191.19 23.2, 192 33.75 C191.89 43.97, 187.68 48.28, 180.75 45 C145.12 43.94, 101.9 43.47, 11.25 45 C0.17 42.05, 3 42.11, 0 33.75 C2.95 26.13, 1.74 20.63, 0 11.25 C-2.86 0.92, 2.55 2.58, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C72.31 2.12, 134.38 2.12, 180.75 0 M11.25 0 C66.97 0.29, 124.65 0.85, 180.75 0 M180.75 0 C189.48 0.51, 192.61 3.49, 192 11.25 M180.75 0 C189.82 -1.2, 191.51 6.01, 192 11.25 M192 11.25 C191.69 15.17, 189.95 24.15, 192 33.75 M192 11.25 C191.4 20.37, 192.12 28.36, 192 33.75 M192 33.75 C193.83 42.02, 187.01 43.73, 180.75 45 M192 33.75 C192.83 42.89, 187.61 47.06, 180.75 45 M180.75 45 C117.57 43.69, 58.24 42.14, 11.25 45 M180.75 45 C128.2 44.14, 77.74 43.47, 11.25 45 M11.25 45 C5.23 43.61, -1.94 40.96, 0 33.75 M11.25 45 C2.74 45.12, -1.86 41.84, 0 33.75 M0 33.75 C0.42 24.84, -0.33 17.98, 0 11.25 M0 33.75 C-0.65 26.77, 0.75 18.47, 0 11.25 M0 11.25 C1.97 4.32, 3.05 -1.26, 11.25 0 M0 11.25 C-0.2 3.87, 3.19 2.17, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(216.11938328603281 375.9765625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g stroke-linecap="round" transform="translate(507.78738350728526 136.4765625) rotate(0 21.5 22.5)"><path d="M10.75 0 C17.12 0.48, 23.82 -2.22, 32.25 0 C39.65 -1.48, 44.92 6.68, 43 10.75 C40.32 15.6, 44.08 26.87, 43 34.25 C43.37 39.34, 40.84 47.51, 32.25 45 C27.36 44.07, 18.65 41.85, 10.75 45 C0.9 43.11, -3.12 40.52, 0 34.25 C-2.91 25.82, 3.7 20.62, 0 10.75 C0.3 3.42, 1.73 2.42, 10.75 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M10.75 0 C16.86 0.38, 25.61 0.72, 32.25 0 M10.75 0 C17.76 -0.84, 23.96 1.01, 32.25 0 M32.25 0 C39.01 -0.56, 43.04 1.96, 43 10.75 M32.25 0 C41.21 1.94, 40.88 4.22, 43 10.75 M43 10.75 C44.15 20.7, 42.17 28.98, 43 34.25 M43 10.75 C43.52 18.07, 43.83 23.35, 43 34.25 M43 34.25 C43.81 42.2, 40.69 44.09, 32.25 45 M43 34.25 C41.02 40.44, 38.96 46.34, 32.25 45 M32.25 45 C29.35 46.47, 21.92 45.75, 10.75 45 M32.25 45 C24.16 44.07, 18.1 45.11, 10.75 45 M10.75 45 C3.91 45.76, 1.67 40.31, 0 34.25 M10.75 45 C1.51 45.68, 2.04 43.4, 0 34.25 M0 34.25 C-0.05 27.3, 1.2 24.5, 0 10.75 M0 34.25 C-0.39 25.95, 0.42 18.05, 0 10.75 M0 10.75 C-0.02 2.34, 2.8 -0.43, 10.75 0 M0 10.75 C-1.28 5.57, 2.76 1.69, 10.75 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(513.1353830800392 148.9765625) rotate(0 16.152000427246094 10)"><text x="16.152000427246094" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CPU</text></g><g stroke-linecap="round" transform="translate(568.2873835072853 80.4765625) rotate(0 96 22.5)"><path d="M11.25 0 C44.07 -0.17, 83.15 -5.51, 180.75 0 C184.89 -1, 191.45 5.82, 192 11.25 C189.05 18.32, 193.02 24.47, 192 33.75 C193.03 39.62, 187.6 45.48, 180.75 45 C134.94 44.55, 86.34 40.19, 11.25 45 C6.71 45.98, 1.37 39.21, 0 33.75 C-2.96 28.24, 1.63 19.55, 0 11.25 C-2.26 1.75, 3.74 -0.44, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C49 0.5, 90.21 0.1, 180.75 0 M11.25 0 C66.35 2.29, 121.82 0.43, 180.75 0 M180.75 0 C190.15 1.93, 193.37 5.36, 192 11.25 M180.75 0 C189.02 -0.82, 193.23 4.97, 192 11.25 M192 11.25 C192.69 21.03, 190.3 27.86, 192 33.75 M192 11.25 C191.21 15.57, 191.07 20.58, 192 33.75 M192 33.75 C191.47 41.93, 187.16 43.85, 180.75 45 M192 33.75 C192.17 42.74, 190.48 43.22, 180.75 45 M180.75 45 C118.43 46.6, 59.36 48.19, 11.25 45 M180.75 45 C136.47 46.74, 92.44 46.93, 11.25 45 M11.25 45 C5.07 45.29, -1.98 40.72, 0 33.75 M11.25 45 C4.02 45.06, 0.57 42.95, 0 33.75 M0 33.75 C-1.87 27.82, -1.33 21.29, 0 11.25 M0 33.75 C-0.37 25.06, -0.98 17.7, 0 11.25 M0 11.25 C0.88 2.51, 2.2 0.65, 11.25 0 M0 11.25 C0.5 5.48, 3.11 0.49, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(654.1193832860329 92.9765625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g stroke-linecap="round" transform="translate(100.28738350728526 422.4765625) rotate(0 96 22.5)"><path d="M11.25 0 C57.36 -4.43, 98.71 -4.88, 180.75 0 C187.24 -0.55, 194.59 6.39, 192 11.25 C188.51 15.31, 195.07 21.04, 192 33.75 C191.04 39.38, 185.66 42.78, 180.75 45 C134.5 48.79, 93.93 48.32, 11.25 45 C1.73 42.53, 0.25 44.24, 0 33.75 C3.36 28.91, -0.19 24.6, 0 11.25 C-0.01 5.13, 4.89 -0.33, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C72.75 -0.7, 130.69 -1.26, 180.75 0 M11.25 0 C62.93 0.41, 114.73 0.48, 180.75 0 M180.75 0 C189.85 1.87, 190.14 3.18, 192 11.25 M180.75 0 C186.98 -0.26, 192.47 1.76, 192 11.25 M192 11.25 C189.98 16.52, 191.66 25.16, 192 33.75 M192 11.25 C192.93 16.23, 192.26 20.67, 192 33.75 M192 33.75 C193.71 41.87, 189.62 43.62, 180.75 45 M192 33.75 C189.96 43, 186.07 42.93, 180.75 45 M180.75 45 C145.47 41.9, 106.86 45.01, 11.25 45 M180.75 45 C116.03 45.4, 51.65 46.05, 11.25 45 M11.25 45 C2.91 45.68, -0.02 40.41, 0 33.75 M11.25 45 C4.51 43.36, -1.6 41.24, 0 33.75 M0 33.75 C-1.17 27.58, -1.7 20.04, 0 11.25 M0 33.75 C-0.67 29.56, -0.26 23.59, 0 11.25 M0 11.25 C-1.8 3.03, 5.62 1.16, 11.25 0 M0 11.25 C1.34 5.47, 5.37 -2.04, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(186.11938328603281 434.9765625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g stroke-linecap="round" transform="translate(163.28738350728526 301.476806640625) rotate(0 96 22.5)"><path d="M11.25 0 C75.53 -2.57, 145.24 -4.91, 180.75 0 C187.78 -1.14, 193.96 5.79, 192 11.25 C189.56 17.05, 193.22 21.39, 192 33.75 C191.46 43.2, 191.04 47.37, 180.75 45 C122.26 48.1, 67.4 48.39, 11.25 45 C5.93 44.52, 1.33 43.9, 0 33.75 C2.08 25.05, -3.52 24.45, 0 11.25 C-1.64 6.17, 0.28 2.86, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C76.69 1.21, 145.24 -0.12, 180.75 0 M11.25 0 C76.99 1.39, 144.09 1.2, 180.75 0 M180.75 0 C189.71 -1.19, 193.78 3.08, 192 11.25 M180.75 0 C188.21 0.69, 190.19 1.57, 192 11.25 M192 11.25 C191.81 18.86, 191.54 22.67, 192 33.75 M192 11.25 C193.09 18.4, 191.31 27.8, 192 33.75 M192 33.75 C192.68 40.11, 190.11 44.25, 180.75 45 M192 33.75 C192.68 41.79, 190.28 46.48, 180.75 45 M180.75 45 C136.34 46.5, 90.34 44.64, 11.25 45 M180.75 45 C133.06 44.33, 86.74 44.3, 11.25 45 M11.25 45 C5.19 44.95, -1.33 40.57, 0 33.75 M11.25 45 C5.91 44.35, -1.44 40.17, 0 33.75 M0 33.75 C-1.34 25.47, -0.5 16.87, 0 11.25 M0 33.75 C-0.03 26.27, 0.53 21.27, 0 11.25 M0 11.25 C-0.88 5.34, 2.67 1.25, 11.25 0 M0 11.25 C-0.79 2.57, 4.98 0.27, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(249.11938328603281 313.976806640625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g stroke-linecap="round" transform="translate(312.28738350728526 81.476806640625) rotate(0 96 22.5)"><path d="M11.25 0 C47.81 0.75, 84.55 5.3, 180.75 0 C184.85 1.81, 195.37 1.15, 192 11.25 C189.58 16.52, 195.27 31.31, 192 33.75 C191.48 43.43, 187.17 42.62, 180.75 45 C133.08 43.82, 82.39 47.57, 11.25 45 C5.1 42.83, -2.85 42.77, 0 33.75 C-2.07 27.54, -2.67 19.87, 0 11.25 C1.66 3.37, 4.56 -0.22, 11.25 0" stroke="none" stroke-width="0" fill="#ffc9c9"></path><path d="M11.25 0 C70.38 -1.09, 131 -1.45, 180.75 0 M11.25 0 C76.55 -0.35, 142.42 -0.99, 180.75 0 M180.75 0 C188.02 -1.56, 190.52 5.66, 192 11.25 M180.75 0 C190.55 0.49, 193.28 1.59, 192 11.25 M192 11.25 C191.62 18.69, 190.49 22.57, 192 33.75 M192 11.25 C191.14 19.64, 192.24 26.83, 192 33.75 M192 33.75 C190.61 39.44, 188.09 44.18, 180.75 45 M192 33.75 C191.11 40.4, 186.21 43.61, 180.75 45 M180.75 45 C116.72 44.77, 54.16 44.72, 11.25 45 M180.75 45 C120.7 45.27, 63.32 44.61, 11.25 45 M11.25 45 C2.95 43.16, -1.24 42.27, 0 33.75 M11.25 45 C5.29 44.03, 1.43 41.13, 0 33.75 M0 33.75 C1.11 24.07, 1.64 17.11, 0 11.25 M0 33.75 C0.26 27.43, -0.29 22.28, 0 11.25 M0 11.25 C-1.94 2.44, 2.44 -0.61, 11.25 0 M0 11.25 C-2.21 2.95, 3.4 1.57, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(398.1193832860328 93.976806640625) rotate(0 10.168000221252441 10)"><text x="10.168000221252441" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">IO</text></g><g stroke-linecap="round" transform="translate(55.787383507285256 478.47705078125) rotate(0 52.5 22.5)"><path d="M11.25 0 C36.06 -0.64, 55.75 -3.13, 93.75 0 C103.4 -2.36, 106.95 0.21, 105 11.25 C103.01 19.93, 105.48 26.35, 105 33.75 C103.56 43.44, 102.09 48.27, 93.75 45 C67.68 43.48, 37.45 47.69, 11.25 45 C5.4 47.72, -1.21 38.43, 0 33.75 C0.77 25.74, 1.8 14.56, 0 11.25 C-1.04 6.82, 1.8 -2.39, 11.25 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M11.25 0 C38.36 2.03, 65.19 -0.7, 93.75 0 M11.25 0 C29.94 0.13, 47.61 0.28, 93.75 0 M93.75 0 C102.01 -1.68, 106.52 2.56, 105 11.25 M93.75 0 C102.06 -1.41, 103.2 2.78, 105 11.25 M105 11.25 C106.53 18.07, 105.3 19.67, 105 33.75 M105 11.25 C105.79 18.53, 105.39 26.77, 105 33.75 M105 33.75 C103.09 43, 101.43 45.33, 93.75 45 M105 33.75 C105.26 40.58, 99.51 44.48, 93.75 45 M93.75 45 C73.81 44.27, 54.9 44.48, 11.25 45 M93.75 45 C64.88 45.18, 34.6 45.65, 11.25 45 M11.25 45 C5.34 44.59, 1.02 41.31, 0 33.75 M11.25 45 C4.59 44.06, -0.23 42.87, 0 33.75 M0 33.75 C1.02 29.25, 1.45 20.52, 0 11.25 M0 33.75 C-0.06 29.04, -0.67 22.34, 0 11.25 M0 11.25 C0.31 2.33, 5.35 0.21, 11.25 0 M0 11.25 C-0.17 5.11, 5.64 0.22, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(92.13538308003916 490.97705078125) rotate(0 16.152000427246094 10)"><text x="16.152000427246094" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CPU</text></g><g stroke-linecap="round" transform="translate(277.78738350728526 482.47705078125) rotate(0 47 22.5)"><path d="M11.25 0 C33.79 -0.15, 57.56 -0.92, 82.75 0 C88.19 -1.85, 91.08 0.52, 94 11.25 C91.65 19.83, 93.91 19.89, 94 33.75 C91.65 43.06, 89.23 41.54, 82.75 45 C65.34 41.42, 50.07 47.82, 11.25 45 C2.05 44.35, -1.5 44.71, 0 33.75 C1.51 29.14, -3.12 21.49, 0 11.25 C-0.34 7.15, 2.35 2.94, 11.25 0" stroke="none" stroke-width="0" fill="#a5d8ff"></path><path d="M11.25 0 C35.82 -0.44, 57.74 -1.8, 82.75 0 M11.25 0 C30.16 -1.17, 47.43 0.41, 82.75 0 M82.75 0 C90.7 -1.78, 92.41 2.09, 94 11.25 M82.75 0 C92.29 1.19, 93.95 2.11, 94 11.25 M94 11.25 C92.41 20.87, 92.68 29.73, 94 33.75 M94 11.25 C95.09 20.42, 94.09 28.1, 94 33.75 M94 33.75 C95.52 40.92, 88.39 45.65, 82.75 45 M94 33.75 C92.42 39.56, 89.25 45.58, 82.75 45 M82.75 45 C63.14 43.88, 45.75 45.81, 11.25 45 M82.75 45 C59.98 45.84, 39.6 44.77, 11.25 45 M11.25 45 C4.83 45.81, 0.85 39.31, 0 33.75 M11.25 45 C2.15 45.85, -0.06 40.5, 0 33.75 M0 33.75 C0.7 27.52, 1.61 19.31, 0 11.25 M0 33.75 C-0.43 28.15, 0.65 23.39, 0 11.25 M0 11.25 C1.53 5.51, 2.81 -1.5, 11.25 0 M0 11.25 C-0.06 4.77, 3.6 1.17, 11.25 0" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(308.63538308003916 494.97705078125) rotate(0 16.152000427246094 10)"><text x="16.152000427246094" y="14.016" font-family="Virgil, Segoe UI Emoji" font-size="16px" fill="#1e1e1e" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CPU</text></g><g stroke-linecap="round"><g transform="translate(11.299229057598836 547.9227290332371) rotate(0 408.5 -1)"><path d="M0.1 0.04 C136.31 -0.12, 681.42 -0.51, 817.54 -0.83 M-1.3 -0.99 C134.77 -1.45, 680.66 -2.41, 816.87 -2.34" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(11.299229057598836 547.9227290332371) rotate(0 408.5 -1)"><path d="M793.38 6.23 C801.07 4.32, 810.57 0.32, 816.87 -2.34 M793.38 6.23 C800.67 3.26, 808.12 0.72, 816.87 -2.34" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g><g transform="translate(11.299229057598836 547.9227290332371) rotate(0 408.5 -1)"><path d="M793.37 -10.87 C801.07 -7.23, 810.58 -5.67, 816.87 -2.34 M793.37 -10.87 C800.52 -8.61, 807.97 -5.91, 816.87 -2.34" stroke="#1e1e1e" stroke-width="2" fill="none"></path></g></g><mask></mask></svg>

<p>Usando execução assíncrona, é preciso tomar cuidado com o <em>overflow</em> de mensagens. Se iniciarmos uma <em>task</em> para cada mensagem que chega, o consumidor pode ter problemas ao lidar com rajadas e cenários de dados represados e/ou reprocessamento.</p>

<p>A estratégia de <em>micro-batches</em> é um jeito fácil de lidar com esse problema, que consiste em gerar pequenos lotes a partir do fluxo contínuo. No código abaixo, a função <code class="language-plaintext highlighter-rouge">consume</code> puxa até <code class="language-plaintext highlighter-rouge">BACTH_SIZE</code> registros do tópico a cada iteração. A variável <code class="language-plaintext highlighter-rouge">timeout</code> é utilizada para configurar a latência máxima: se não houver <code class="language-plaintext highlighter-rouge">BATCH_SIZE</code> registros a serem puxados em 1 segundo, um batch menor será criado e executado.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>

    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">while</span> <span class="n">count</span> <span class="o">&lt;</span> <span class="n">num_records</span><span class="p">:</span>
    
        <span class="n">msgs</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">consume</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">msgs</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="k">continue</span>
        
        <span class="n">msgs_content</span> <span class="o">=</span> <span class="nf">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span> <span class="p">:</span> <span class="n">x</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">(),</span> <span class="n">msgs</span><span class="p">)</span>

        <span class="k">await</span> <span class="nf">asave_messages</span><span class="p">(</span><span class="n">msgs_content</span><span class="p">)</span>

        <span class="n">count</span><span class="o">+=</span><span class="nf">len</span><span class="p">(</span><span class="n">msgs</span><span class="p">)</span>
        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>
<p>A função <code class="language-plaintext highlighter-rouge">asave_messages</code> é responsável por lançar as <code class="language-plaintext highlighter-rouge">tasks</code> de <code class="language-plaintext highlighter-rouge">asave_message</code>, o <code class="language-plaintext highlighter-rouge">await</code> indica que o loop principal precisa esperar todas as mensagens serem processadas antes de buscar outro lote de mensagens.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">asave_messages</span><span class="p">(</span><span class="n">msgs</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]):</span>
    
    <span class="n">aconn</span> <span class="o">=</span> <span class="k">await</span> <span class="n">psycopg</span><span class="p">.</span><span class="n">AsyncConnection</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>

    <span class="k">async</span> <span class="k">with</span> <span class="n">aconn</span><span class="p">:</span>
        <span class="k">async</span> <span class="k">with</span> <span class="n">asyncio</span><span class="p">.</span><span class="nc">TaskGroup</span><span class="p">()</span> <span class="k">as</span> <span class="n">tg</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">msgs</span><span class="p">:</span>
                <span class="n">tg</span><span class="p">.</span><span class="nf">create_task</span><span class="p">(</span><span class="nf">asave_message</span><span class="p">(</span><span class="n">aconn</span><span class="p">,</span> <span class="n">msg</span><span class="p">))</span>

        <span class="k">await</span> <span class="n">aconn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
</code></pre></div></div>

<p>O <em>micro-batch</em> é uma estratégia para ter previsibilidade no consumidor, que pode ser configurado e dimensionado de acordo com uma taxa fixa e latência esperada, apenas o broker que precisar lidar com picos de volumetria e eventuais anomalias. Perde-se um pouco em latência, mas é o <em>trade-off</em> esperado em uma arquitetura de eventos.</p>

<p>Nesse cenário, não existe mais uma garantia de <em>exactly-once</em>. Se houver algum erro durante o processamento do batch, o usuário deve optar por não atualizar os <em>offsets</em> e adotar uma semântica <em>at-least-once</em> ou atualizá-los e adotar uma semântica <em>at-most-once</em>. No cenário proposto, faz sentido adotar <em>at-least-once</em>, pois o banco de dados garante <a href="https://pt.wikipedia.org/wiki/Idempotência">idempotência</a> da operação de <code class="language-plaintext highlighter-rouge">insert</code>.</p>

<p>Usando <code class="language-plaintext highlighter-rouge">BATCH_SIZE=10_000</code>, <strong>o tempo total foi de 47 minutos para cerca de 3 minutos</strong></p>

<h2 id="a-alternativa-multiprocess">A alternativa multiprocess</h2>

<p>O <code class="language-plaintext highlighter-rouge">asyncio</code> é a solução desenhada para esse cenário de muito I/O concorrente, mas existem bons motivos para usar outros métodos de implementar concorrência, como <code class="language-plaintext highlighter-rouge">multiprocess</code> e <code class="language-plaintext highlighter-rouge">threading</code>.</p>

<p>O principal motivo é que a implementação assíncrona depende que as bibliotecas utilizadas a suportem, o que não é tão prevalente no ecossistema Python. Além disso, existem muitos conceitos complexos em programação assíncrona e isso reflete na dificuldade de implementação.</p>

<p>Processamento paralelo em Python é uma questão conteciosa pela existência do <a href="https://wiki.python.org/moin/GlobalInterpreterLock">GIL</a>, a decisão entre <em>threads</em> e processos normalmente é definida pela natureza do processamento. Sendo um processo <em>I/O bound</em>, eu poderia optar por <em>threads</em> que são mais leves e fáceis de se comunicar, mas optei por paralelizar com processos pelos seguintes motivos:</p>

<ul>
  <li>
    <p>é um cenário clássico de <em>data paralelism</em>, as dificuldades de comunicação entre processos não são releventes nesse contexto;</p>
  </li>
  <li>
    <p>usando <a href="https://en.wikipedia.org/wiki/Thread_pool"><em>pool</em> de processos</a>, o problema do custo extra de criar múltiplos processos é mitigado;</p>
  </li>
  <li>
    <p>prefiro evitar pensar sobre questões de <a href="https://en.wikipedia.org/wiki/Thread_safety">thread safety</a>, vou usar um <em>hack</em> para criar objetos separados por processo e ignorar esse problema;</p>
  </li>
  <li>
    <p>a solução pode ser aplicada em cenários <em>CPU bound</em>, se quiséssemos aplicar modelos de <em>machine learning</em> por exemplo.</p>
  </li>
</ul>

<p>A estratégia de usar vários processos, é aproveitar a interrupção do sistema operacional para operar de forma concorrente: quando um processo chega na etapa de I/O, ele é interrompido e outro entra em execução. No caso do <code class="language-plaintext highlighter-rouge">asyncio</code>, estamos usando um único processo Python que implementa concorrência usando <code class="language-plaintext highlighter-rouge">Tasks</code>.</p>

<p>A maior complicação de usar <em>multiprocess</em> nesse problema, é lidar com a conexão com o banco de dados. Não é possível compartilhar esse tipo de objeto entre processos, mas é contraproducente ficar recriando a conexão a cada iteração.</p>

<p>A solução é criar um objeto por processo, que será utilizado até o fim do <em>batch</em>. Não é a forma mais elegante, mas um jeito é criar uma variável vazia <code class="language-plaintext highlighter-rouge">conn</code> e inicializar como global usando a função <code class="language-plaintext highlighter-rouge">set_global_conn</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">conn</span> <span class="o">=</span> <span class="bp">None</span>

<span class="k">def</span> <span class="nf">set_global_conn</span><span class="p">():</span>
    <span class="k">global</span> <span class="n">conn</span>
    <span class="n">conn</span> <span class="o">=</span> <span class="n">psycopg</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">db</span><span class="p">)</span>
</code></pre></div></div>

<p>Abaixo, a implementação do proceso usando <code class="language-plaintext highlighter-rouge">set_global_conn</code> na criação do <em>pool</em> de processos.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_messages_conn</span><span class="p">(</span><span class="n">msgs</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]):</span> <span class="nf">save_message_batch</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msgs</span><span class="p">)</span>

<span class="k">try</span><span class="p">:</span>

    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">while</span> <span class="n">count</span> <span class="o">&lt;</span> <span class="n">num_records</span><span class="p">:</span>

        <span class="n">msgs</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">consume</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
    
        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">msgs</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="k">continue</span>
        
        <span class="n">msgs_content</span> <span class="o">=</span> <span class="nf">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span> <span class="p">:</span> <span class="n">x</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">(),</span> <span class="n">msgs</span><span class="p">)</span>

        <span class="k">with</span> <span class="nc">Pool</span><span class="p">(</span><span class="n">processes</span><span class="o">=</span><span class="mi">48</span><span class="p">,</span>
                  <span class="n">initializer</span><span class="o">=</span><span class="n">set_global_conn</span><span class="p">)</span> <span class="k">as</span> <span class="n">pool</span><span class="p">:</span>

            <span class="n">pool</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">save_message_conn</span><span class="p">,</span> <span class="n">msgs_content</span><span class="p">)</span>

        <span class="n">count</span><span class="o">+=</span><span class="nf">len</span><span class="p">(</span><span class="n">msgs</span><span class="p">)</span>
        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Em termos de desempenho, essa solução ficou parecida com a implementada utilizando <code class="language-plaintext highlighter-rouge">asyncio</code>. Ambos ficaram perto dos 2 minutos, mas o desempenho do <code class="language-plaintext highlighter-rouge">multiprocess</code> demanda mais processamento e está condicionada a configuração de núcleos da máquina.</p>

<p>As estratégias aplicadas até o momento, foram para agilizar as operações de I/O, mas podemos reduzir a quantidade de operações também.</p>

<h2 id="reduzindo-o-custo-fixo">Reduzindo o custo fixo</h2>

<p>As operações de I/O têm um custo fixo – não importa a quantidade de dados transmitido, sempre é necessário interromper o processamento e lidar com o <em>overhead</em> do protocolo – faz sentido agrupar as operações e dissolver esse custo.</p>

<p>Quando se faz aplicações em lote para trabalhar com banco de dados, é sempre recomendado aplicar estratégias que tirem proveito dessa ideia. Desde ações simples, como não executar o <code class="language-plaintext highlighter-rouge">commit</code> para toda linha modificada, seja ações mais agressivas como remover índices durante o processamento e recriá-los posteriormente.</p>

<p>Nesse caso, a ideia é simplesmente agrupar os <code class="language-plaintext highlighter-rouge">inserts</code> em pequenos grupos e não chamar <code class="language-plaintext highlighter-rouge">commit</code> a cada linha inserida. É bem simples fazer isso com <code class="language-plaintext highlighter-rouge">psycopg</code>, basta criar uma lista de valores e usar o comando <code class="language-plaintext highlighter-rouge">executemany</code> no cursor.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">save_message_batch</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">msgs</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]):</span>

    <span class="n">values</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">msgs</span><span class="p">:</span>
    
        <span class="n">content</span> <span class="o">=</span>  <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>

        <span class="n">values</span><span class="p">.</span><span class="nf">append</span><span class="p">((</span>
            <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">message_id</span><span class="sh">'</span><span class="p">],</span>
            <span class="n">content</span><span class="p">[</span><span class="sh">'</span><span class="s">client_id</span><span class="sh">'</span><span class="p">],</span>
            <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(),</span>
            <span class="n">msg</span><span class="p">,</span>
        <span class="p">))</span>

    <span class="n">insert</span> <span class="o">=</span> <span class="sh">"</span><span class="s">insert into messages VALUES (%s, %s, %s, %s)</span><span class="sh">"</span>

    <span class="k">with</span> <span class="n">conn</span><span class="p">.</span><span class="nf">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
        <span class="n">cur</span><span class="p">.</span><span class="nf">executemany</span><span class="p">(</span><span class="n">insert</span><span class="p">,</span> <span class="n">values</span><span class="p">)</span>
        
    <span class="n">conn</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>
</code></pre></div></div>

<p>No consumidor, é necessário criar esses grupos de mensagens a serem inseridas. Como as mensagens são independentes entre si, os grupos podem ser criados de forma arbitrária aplicando cortes no vetor.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">num_msgs</span> <span class="o">&lt;=</span> <span class="n">BATCH_INSERT</span><span class="p">:</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[</span><span class="n">msgs_content</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[</span>
        <span class="n">msgs_content</span><span class="p">[</span>
            <span class="n">i</span><span class="o">*</span><span class="n">BATCH_INSERT</span><span class="p">:</span>
            <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="n">BATCH_INSERT</span><span class="p">)</span><span class="o">+</span><span class="n">BATCH_INSERT</span>
        <span class="p">]</span>
        <span class="k">for</span> <span class="n">i</span>
        <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_msgs</span> <span class="o">//</span> <span class="n">BATCH_INSERT</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)]</span>
</code></pre></div></div>

<p>Exceto pela criação dos <code class="language-plaintext highlighter-rouge">chunks</code>, a estrutura do consumidor fica praticamente igual às demais implementações.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>

    <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">while</span> <span class="n">count</span> <span class="o">&lt;</span> <span class="n">num_records</span><span class="p">:</span>
    
        <span class="n">msgs</span> <span class="o">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">consume</span><span class="p">(</span><span class="n">BATCH_SIZE</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mf">1.0</span><span class="p">)</span>
        <span class="n">num_msgs</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">msgs</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">num_msgs</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="k">continue</span>
    
        <span class="n">msgs_content</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="nf">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span> <span class="p">:</span> <span class="n">x</span><span class="p">.</span><span class="nf">value</span><span class="p">().</span><span class="nf">decode</span><span class="p">(),</span> <span class="n">msgs</span><span class="p">))</span>

        <span class="k">if</span> <span class="n">num_msgs</span> <span class="o">&lt;=</span> <span class="n">BATCH_INSERT</span><span class="p">:</span>
            <span class="n">chunks</span> <span class="o">=</span> <span class="p">[</span><span class="n">msgs_content</span><span class="p">]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">chunks</span> <span class="o">=</span> <span class="p">[</span>
                <span class="n">msgs_content</span><span class="p">[</span>
                    <span class="n">i</span><span class="o">*</span><span class="n">BATCH_INSERT</span><span class="p">:</span>
                    <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="n">BATCH_INSERT</span><span class="p">)</span><span class="o">+</span><span class="n">BATCH_INSERT</span>
                <span class="p">]</span>
                <span class="k">for</span> <span class="n">i</span>
                <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">num_msgs</span> <span class="o">//</span> <span class="n">BATCH_INSERT</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)]</span>

        <span class="k">with</span> <span class="nc">Pool</span><span class="p">(</span><span class="n">processes</span><span class="o">=</span><span class="mi">48</span><span class="p">,</span>
                  <span class="n">initializer</span><span class="o">=</span><span class="n">set_global_conn</span><span class="p">)</span> <span class="k">as</span> <span class="n">pool</span><span class="p">:</span>
            <span class="n">pool</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="n">save_message_batch_conn</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>

        <span class="n">count</span><span class="o">+=</span><span class="n">num_msgs</span>
        <span class="n">consumer</span><span class="p">.</span><span class="nf">commit</span><span class="p">()</span>

<span class="k">finally</span><span class="p">:</span>
    <span class="n">consumer</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>

<p>Essa implementação com operações agrupadas, ficou ainda mais performática – usando <code class="language-plaintext highlighter-rouge">BATCH_INSERT = 35</code>, <code class="language-plaintext highlighter-rouge">BATCH_SIZE = 10_000</code> e 48 processos – o processo todo demorou <strong>40 segundos para inserir 1.000.000 de registros, o que antes demorava 47 minutos</strong>.</p>

<h2 id="capcioso">Capcioso</h2>

<p>Eu não esperava que essas mudanças trouxesse ganhos tão expressivos, mas naturalmente esses resultados dependem de uma miríade de fatores. Para quem quiser executar os experimentos ou adaptar a solução para outro cenário, os códigos estão <a href="https://github.com/gdarruda/kafka-consumer-experiments">nesse repositório</a>.</p>

<p>A ideia do post não era discutir esse problema em específico, mas as tomadas de decisão ao construir um consumidor Kafka. Os códigos desenvolvidos não são trabalhosos, nem mesmo complexos. Capciosos, talvez?</p>

<p>São códigos curtos, mas que demandam entendimento de concorrência e paralelismo, conceitos considerados avançados. O truque, de usar uma variável <code class="language-plaintext highlighter-rouge">global</code> não inicializada para criar um objeto por processo, é algo simples de implementar. Só que não é óbvio entender o porquê não se pode serializar uma conexão com banco de dados, nem o porquê isso é necessário quando se trabalha com múltiplos processos.</p>

<p>Problemas de engenharia de dados têm essa característica, mas até pela natureza de lidar com grandes volumes, é comum esses gargalos óbvios sejam discutidos em materias introdutórios. No caso do Kafka, essa discussão existe, mas muita mais sobre a infra do broker (<em>e.g</em> partições, réplicas, ZooKeeper, storage).</p>

<p>Imagino que um dos motivos, para não existir tantas discussões sobre a arquitetura do consumo, seja a grande variedade de aplicações. As discussões mudariam completamente se, por exemplo, o problema consistisse de eventos dependentes entre si. Mesmo assim, enxergo que existem padrões a serem seguidos em praticamente qualquer cenário, como a ideia de <em>micro-batch</em> para não ter problemas de <em>overflow</em> e abrir possibilidades de otimização.</p>

<p>Espero ter conseguido passar a ideia, dos pontos de atenção a serem considerados ao desenhar um consumidor.  É um post que acabou maior que o esperado, mas o diabo está nos detalhes quando se fala de <del>Goethe</del> Kafka, então achei importante expandir alguns tópicos para além do código e resultados.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Lendo tópicos Kafka de forma prática]]></summary></entry><entry><title type="html">O que é um arquivo parquet?</title><link href="/2024/02/25/parquet.html" rel="alternate" type="text/html" title="O que é um arquivo parquet?" /><published>2024-02-25T00:00:00+00:00</published><updated>2024-02-25T00:00:00+00:00</updated><id>/2024/02/25/parquet</id><content type="html" xml:base="/2024/02/25/parquet.html"><![CDATA[<p>No post sobre <a href="/2022/08/21/data-mesh.html">Data Mesh</a>, comentei sobre os trade-offs de usar uma abordagem “Unix like” para ferramentas de dados. Por um lado, é muito interessante pensando em arquitetura e estratégia, mas pode ser um problema para os usuários menos técnicos. Um exemplo desse desafio, é lidar com diferentes formatos e estratégias de <a href="https://en.wikipedia.org/wiki/Serialization">serialização</a>.</p>

<p>Imagino ser um cenário comum – encontrar cientistas, analistas e afins – sofrendo desnecessariamente com tabelas enormes armazenadas em formato texto (e.g. csv, json). São formatos práticos e acessíveis, ótimos para pequenos projetos, mas que não escalam bem para grandes volumes.</p>

<p>Uma das alternativas mais comuns para dados tabulares, é utilizar <a href="https://parquet.apache.org">arquivos parquet</a>. Mas o que são esses arquivos?  Após anos usando parquet, tendo uma vaga noção do que se trata, surgiu a curiosidade de entender mais profundamente os detalhes de implementação.</p>

<h2 id="a-proposta-do-parquet">A proposta do Parquet</h2>

<p>O formato parquet foi proposto pelo Twitter, em parceria com a Cloudera, como um formato otimizado para uso no ecossistema Hadoop. As ideias de design por trás do formato são discutidas <a href="https://www.youtube.com/watch?v=Qfp6Uv1UrA0">nessa apresentação</a>, entre as mais importantes delas:</p>

<ul>
  <li>encoding e compressão a nível de coluna, dando flexibilidade para otimizações;</li>
  <li>formato colunar, ideal para <em>workloads</em> analíticos;</li>
  <li>especificação aberta e agnóstica de plataforma.</li>
</ul>

<p>Na especificação do formato, tem um diagrama que apresenta a estrutura geral do arquivo (Figura 1).</p>

<figure>
  <img src="https://parquet.apache.org/images/FileLayout.gif" style="margin: auto" />
  <figcaption>Figura 1 – Especificação Parquet</figcaption>
</figure>

<p>Olhando somente o diagrama, não consegui entender muito bem o formato, então optei pela estratégia de ler o código de alguma implementação. Uma ideia que acabou se mostrando bem mais complicada que o esperado, mas após muito debug e leitura de especificações, consegui entender o formato e gerar um arquivo parquet “na unha”.</p>

<h2 id="fastparquet-como-referência">Fastparquet como referência</h2>

<p>Sendo um formato aberto, existem diversas implementações diferentes para leitura e escrita de arquivos parquet, como <a href="https://arrow.apache.org/docs/python/parquet.html">Arrow</a> e <a href="https://spark.apache.org/docs/latest/sql-data-sources-parquet.html">Spark</a> por exemplo. Optei pelo <a href="https://fastparquet.readthedocs.io/en/latest/">fastparquet</a>, que é uma implementação puramente em Python e me permitiria debugar passo-a-passo se necessário.</p>

<p>Para analisar o código, fui executando linha-a-linha o comando <code class="language-plaintext highlighter-rouge">write('outfile.parquet', df, append=False)</code>, usando a tabela abaixo.</p>

<table>
  <thead>
    <tr>
      <th>Key</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>chave_1</td>
      <td>valor_1</td>
    </tr>
    <tr>
      <td>chave_2</td>
      <td>valor_2</td>
    </tr>
    <tr>
      <td>chave_3</td>
      <td>valor_3</td>
    </tr>
  </tbody>
</table>

<p>Após executar várias vezes, consegui formar uma noção melhor da especificação. Agora, a ideia é fazer uma implementação “minimalista” em Go: transformar essa tabela em um arquivo parquet, da forma mais simples possível.</p>

<h2 id="sobre-os-metadados">Sobre os metadados</h2>

<p>Os metadados de um arquivo parquet são escritos usando o <a href="https://github.com/apache/thrift/blob/master/doc/specs/thrift-compact-protocol.md">Thrift Compact Protocol</a>, um tipo de serialização utilizado pelo protocolo RPC <a href="https://thrift.
apache.org">Thrift</a>. É similar a outros protocolos de serialização, como <a href="https://protobuf.dev">Protobuf</a> e <a href="https://avro.apache.org/docs/">Avro</a> por exemplo.</p>

<p>Para quem não está familiarizado com esses protocolos, são especificações de serialização binária, otimizadas para processamento/armazenamento em comparação a formatos texto. Em contrapartida, não são amigáveis para leitura do usuário, como um <code class="language-plaintext highlighter-rouge">json</code> ou <code class="language-plaintext highlighter-rouge">yaml</code>.</p>

<p>Para ler e escrever em Thrift Compact Protocol, precisamos de uma especificação, um schema com a estrutura da informação. <a href="https://github.com/dask/fastparquet/blob/main/fastparquet/parquet.thrift">Esse arquivo</a> é a especificação dos metadados de um arquivo parquet, uma representação gráfica da estrutura pode ser vista na figura abaixo.</p>

<figure>
  <img src="https://parquet.apache.org/images/FileFormat.gif" style="margin: auto" />
  <figcaption>Figura 2 – Metadados Parquet</figcaption>
</figure>

<h2 id="criando-uma-página">Criando uma página</h2>

<p>A primeira parte de um arquivo parquet são as páginas de dados. Cada página contém dados de uma única coluna, uma coluna pode estar separada em várias páginas. Em nosso caso, será apenas uma página por coluna, na qual o dado será armazenado sem compactação e com representação <code class="language-plaintext highlighter-rouge">PLAIN</code>.</p>

<p>Cada página de dados tem um cabeçalho, representado pelo objetos <code class="language-plaintext highlighter-rouge">PageHeader</code> e <code class="language-plaintext highlighter-rouge">DataPageHeader</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pageHeader</span> <span class="o">:=</span> <span class="n">PageHeader</span><span class="p">{</span>
		<span class="n">pageType</span><span class="o">:</span>             <span class="m">0</span> <span class="c">/* DATA_PAGE */</span><span class="p">,</span>
		<span class="n">uncompressedPageSize</span><span class="o">:</span> <span class="kt">int32</span><span class="p">(</span><span class="n">content</span><span class="o">.</span><span class="n">Len</span><span class="p">()),</span>
		<span class="n">compressedPageSize</span><span class="o">:</span>   <span class="kt">int32</span><span class="p">(</span><span class="n">content</span><span class="o">.</span><span class="n">Len</span><span class="p">()),</span>
		<span class="n">dataPageHeader</span><span class="o">:</span> <span class="n">DataPageHeader</span><span class="p">{</span>
			<span class="n">numValues</span><span class="o">:</span> <span class="kt">int32</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">column</span><span class="p">)),</span>
			<span class="n">encoding</span><span class="o">:</span>  <span class="m">0</span> <span class="c">/* PLAIN */</span><span class="p">}}</span>
</code></pre></div></div>

<p>Para criar esse cabeçalho, é necessário saber o tamanho da página e a quantidade de registros. Portanto, antes de escrever o cabeçalho de uma página, é necessário lidar com o conteúdo dela.</p>

<h3 id="escrevendo-a-coluna-de-strings-em-plain-text">Escrevendo a coluna de strings em PLAIN text</h3>

<p>As colunas da tabela serão tratadas como strings binárias, que podem ser representadas como <code class="language-plaintext highlighter-rouge">BYTE_ARRAY</code> em um arquivo parquet. Usando a representação <code class="language-plaintext highlighter-rouge">PLAIN</code>, basta informar o tamanho do array de bytes e escrever o conteúdo em seguida.</p>

<blockquote>
  <p>BYTE_ARRAY: length in 4 bytes little endian followed by the bytes contained in the array</p>
</blockquote>

<p>Abaixo, o código utilizado para escrever uma coluna. Para cada item da coluna, o tamanho é calculado e representado como um inteiro em 4 bytes. O conteúdo é escrito logo após, sem nenhuma transformação.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">Parquet</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">writer</span>    <span class="n">thrift</span><span class="o">.</span><span class="n">TProtocol</span>
	<span class="n">file</span>      <span class="n">io</span><span class="o">.</span><span class="n">ReadWriter</span>
	<span class="n">ctx</span>       <span class="n">context</span><span class="o">.</span><span class="n">Context</span>
	<span class="n">transport</span> <span class="o">*</span><span class="n">thrift</span><span class="o">.</span><span class="n">StreamTransport</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">Parquet</span><span class="p">)</span> <span class="n">writeColumn</span><span class="p">(</span><span class="n">records</span> <span class="p">[][]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">{</span>

	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">record</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">records</span> <span class="p">{</span>

		<span class="n">valueSize</span> <span class="o">:=</span> <span class="kt">uint32</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">record</span><span class="p">))</span>

		<span class="n">size</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">4</span><span class="p">)</span>
		<span class="n">binary</span><span class="o">.</span><span class="n">LittleEndian</span><span class="o">.</span><span class="n">PutUint32</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">valueSize</span><span class="p">)</span>
		<span class="n">t</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">size</span><span class="p">)</span>
		<span class="n">t</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="n">t</span><span class="o">.</span><span class="n">transport</span><span class="o">.</span><span class="n">Flush</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">)</span>

<span class="p">}</span>
</code></pre></div></div>
<h3 id="juntando-no-arquivo">Juntando no arquivo</h3>

<p>Como é necessário saber o tamanho do conteúdo para criar o cabeçalho da página, foram usados dois buffers apartados: <code class="language-plaintext highlighter-rouge">header</code> e <code class="language-plaintext highlighter-rouge">content</code>. O <code class="language-plaintext highlighter-rouge">content</code> é criado antes do <code class="language-plaintext highlighter-rouge">header</code>, mas no arquivo é escrito na ordem inversa.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">WritePage</span><span class="p">(</span><span class="n">column</span> <span class="p">[][]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">f</span> <span class="o">*</span><span class="n">os</span><span class="o">.</span><span class="n">File</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span>

	<span class="n">header</span> <span class="o">:=</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewBuffer</span><span class="p">([]</span><span class="kt">byte</span><span class="p">{})</span>
	<span class="n">content</span> <span class="o">:=</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewBuffer</span><span class="p">([]</span><span class="kt">byte</span><span class="p">{})</span>

	<span class="n">parquetHeader</span> <span class="o">:=</span> <span class="n">TableParquet</span><span class="p">(</span><span class="n">header</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">parquetHeader</span><span class="o">.</span><span class="n">transport</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">parquetContent</span> <span class="o">:=</span> <span class="n">TableParquet</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
	<span class="k">defer</span> <span class="n">parquetContent</span><span class="o">.</span><span class="n">transport</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

	<span class="n">parquetContent</span><span class="o">.</span><span class="n">writeColumn</span><span class="p">(</span><span class="n">column</span><span class="p">)</span>

	<span class="n">pageHeader</span> <span class="o">:=</span> <span class="n">PageHeader</span><span class="p">{</span>
		<span class="n">pageType</span><span class="o">:</span>             <span class="n">DATA_PAGE</span><span class="p">,</span>
		<span class="n">uncompressedPageSize</span><span class="o">:</span> <span class="kt">int32</span><span class="p">(</span><span class="n">content</span><span class="o">.</span><span class="n">Len</span><span class="p">()),</span>
		<span class="n">compressedPageSize</span><span class="o">:</span>   <span class="kt">int32</span><span class="p">(</span><span class="n">content</span><span class="o">.</span><span class="n">Len</span><span class="p">()),</span>
		<span class="n">dataPageHeader</span><span class="o">:</span> <span class="n">DataPageHeader</span><span class="p">{</span>
			<span class="n">numValues</span><span class="o">:</span> <span class="kt">int32</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">column</span><span class="p">)),</span>
			<span class="n">encoding</span><span class="o">:</span>  <span class="m">0</span> <span class="c">/* PLAIN */</span><span class="p">}}</span>

	<span class="n">parquetHeader</span><span class="o">.</span><span class="n">writePageHeader</span><span class="p">(</span><span class="n">pageHeader</span><span class="p">)</span>
	<span class="n">pageSize</span> <span class="o">:=</span> <span class="n">content</span><span class="o">.</span><span class="n">Len</span><span class="p">()</span> <span class="o">+</span> <span class="n">header</span><span class="o">.</span><span class="n">Len</span><span class="p">()</span>

	<span class="n">bufferToFile</span><span class="p">(</span><span class="n">header</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
	<span class="n">bufferToFile</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>

	<span class="k">return</span> <span class="n">pageSize</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Utilizando a <a href="https://pkg.go.dev/github.com/apache/thrift@v0.19.0">biblioteca em Go</a>, a função <code class="language-plaintext highlighter-rouge">writePageHeader</code> escreve o objeto thrift <code class="language-plaintext highlighter-rouge">PageHeader</code>.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">Parquet</span><span class="p">)</span> <span class="n">writeI32</span><span class="p">(</span><span class="n">name</span> <span class="kt">string</span><span class="p">,</span> <span class="n">id</span> <span class="kt">int16</span><span class="p">,</span> <span class="n">value</span> <span class="kt">int32</span><span class="p">)</span> <span class="p">{</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteFieldBegin</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">thrift</span><span class="o">.</span><span class="n">I32</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteI32</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteFieldEnd</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">Parquet</span><span class="p">)</span> <span class="n">writeDataPageHeader</span><span class="p">(</span><span class="n">dataPageHeader</span> <span class="n">DataPageHeader</span><span class="p">)</span> <span class="p">{</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteStructBegin</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"DataPageHeader"</span><span class="p">)</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"num_values"</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="n">dataPageHeader</span><span class="o">.</span><span class="n">numValues</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"encoding"</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="n">dataPageHeader</span><span class="o">.</span><span class="n">encoding</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"definition_level_encoding"</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="n">dataPageHeader</span><span class="o">.</span><span class="n">definitionLevelEncoding</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"repetition_level_encoding"</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="n">dataPageHeader</span><span class="o">.</span><span class="n">repetitionLevelEncoding</span><span class="p">)</span>

	<span class="n">t</span><span class="o">.</span><span class="n">finishStruct</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">Parquet</span><span class="p">)</span> <span class="n">writePageHeader</span><span class="p">(</span><span class="n">pageHeader</span> <span class="n">PageHeader</span><span class="p">)</span> <span class="p">{</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteStructBegin</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"PageHeader"</span><span class="p">)</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"type"</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="kt">int32</span><span class="p">(</span><span class="n">pageHeader</span><span class="o">.</span><span class="n">pageType</span><span class="p">))</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"uncompressed_page_size"</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="n">pageHeader</span><span class="o">.</span><span class="n">uncompressedPageSize</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeI32</span><span class="p">(</span><span class="s">"compressed_page_size"</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="n">pageHeader</span><span class="o">.</span><span class="n">compressedPageSize</span><span class="p">)</span>

	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteFieldBegin</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"data_page_header"</span><span class="p">,</span> <span class="n">thrift</span><span class="o">.</span><span class="n">STRUCT</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writeDataPageHeader</span><span class="p">(</span><span class="n">pageHeader</span><span class="o">.</span><span class="n">dataPageHeader</span><span class="p">)</span>
	<span class="n">t</span><span class="o">.</span><span class="n">writer</span><span class="o">.</span><span class="n">WriteFieldEnd</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">ctx</span><span class="p">)</span>

	<span class="n">t</span><span class="o">.</span><span class="n">finishStruct</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ao final, a página da primeira coluna é representada da seguinte forma no arquivo.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00000000  50 41 52 31 15 00 15 42  15 42 2c 15 06 15 00 15  |PAR1...B.B,.....|
00000010  00 15 00 00 00 07 00 00  00 63 68 61 76 65 5f 31  |.........chave_1|
00000020  07 00 00 00 63 68 61 76  65 5f 32 07 00 00 00 63  |....chave_2....c|
00000030  68 61 76 65 5f 33                                 |have_3|
00000036
</code></pre></div></div>

<h2 id="escrevendo-o-rodapé">Escrevendo o rodapé</h2>

<p>Diferente da maioria dos formatos, para ler um parquet começamos pelo rodapé. As colunas e seus tipos são especificados pelo <code class="language-plaintext highlighter-rouge">schema</code>, enquanto os detalhes das páginas geradas ficam nos <code class="language-plaintext highlighter-rouge">row_groups</code>.</p>

<p>Abaixo o código para escrita completa do arquivo, primeiro as páginas são geradas, para depois criar a estrutura do <code class="language-plaintext highlighter-rouge">footer</code> indicando a posição delas no arquivo.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">Create</span><span class="p">(</span><span class="s">"sample.parquet"</span><span class="p">)</span>
<span class="k">defer</span> <span class="n">f</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

<span class="n">footer</span> <span class="o">:=</span> <span class="n">bytes</span><span class="o">.</span><span class="n">NewBuffer</span><span class="p">([]</span><span class="kt">byte</span><span class="p">{})</span>
<span class="n">parquetFooter</span> <span class="o">:=</span> <span class="n">TableParquet</span><span class="p">(</span><span class="n">footer</span><span class="p">)</span>
<span class="k">defer</span> <span class="n">parquetFooter</span><span class="o">.</span><span class="n">transport</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

<span class="n">magicBytes</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>

<span class="n">sizeKeys</span> <span class="o">:=</span> <span class="n">WritePage</span><span class="p">(</span><span class="n">keys</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
<span class="n">sizeValues</span> <span class="o">:=</span> <span class="n">WritePage</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
<span class="n">numRows</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">records</span><span class="p">)</span>

<span class="n">magicBytesOffset</span> <span class="o">:=</span> <span class="m">4</span>

<span class="n">schema</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">SchemaElement</span><span class="p">{</span>
    <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"schema"</span><span class="p">,</span> <span class="n">numChildren</span><span class="o">:</span> <span class="m">2</span><span class="p">},</span>
    <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"key"</span><span class="p">,</span> <span class="n">colType</span><span class="o">:</span> <span class="m">6</span> <span class="c">/*BINARY ARRAY*/</span><span class="p">},</span> 
    <span class="p">{</span><span class="n">name</span><span class="o">:</span> <span class="s">"values"</span><span class="p">,</span> <span class="n">colType</span><span class="o">:</span> <span class="m">6</span> <span class="c">/*BINARY ARRAY*/</span><span class="p">}}</span>

<span class="n">columns</span> <span class="o">:=</span> <span class="p">[]</span><span class="n">ColumnChunk</span><span class="p">{</span>
    <span class="p">{</span><span class="n">fileOffset</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">magicBytesOffset</span><span class="p">),</span>
        <span class="n">metaData</span><span class="o">:</span> <span class="n">ColumnMetadata</span><span class="p">{</span>
            <span class="n">columnType</span><span class="o">:</span>            <span class="m">6</span> <span class="c">/*BINARY ARRAY*/</span><span class="p">,</span>
            <span class="n">encodings</span><span class="o">:</span>             <span class="p">[]</span><span class="kt">int32</span><span class="p">{</span><span class="m">0</span><span class="p">},</span>
            <span class="n">pathInSchema</span><span class="o">:</span>          <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"key"</span><span class="p">},</span>
            <span class="n">codec</span><span class="o">:</span>                 <span class="m">0</span><span class="p">,</span>
            <span class="n">numValues</span><span class="o">:</span>             <span class="kt">int64</span><span class="p">(</span><span class="n">numRows</span><span class="p">),</span>
            <span class="n">totalUncompressedSize</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">sizeKeys</span><span class="p">),</span>
            <span class="n">totalCompressedSize</span><span class="o">:</span>   <span class="kt">int64</span><span class="p">(</span><span class="n">sizeKeys</span><span class="p">),</span>
            <span class="n">dataPageOffset</span><span class="o">:</span>        <span class="kt">int64</span><span class="p">(</span><span class="n">magicBytesOffset</span><span class="p">),</span>
            <span class="n">statistics</span><span class="o">:</span>            <span class="n">Statistics</span><span class="p">{</span><span class="n">nullCount</span><span class="o">:</span> <span class="m">0</span><span class="p">},</span>
        <span class="p">},</span>
    <span class="p">},</span>
    <span class="p">{</span><span class="n">fileOffset</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">magicBytesOffset</span> <span class="o">+</span> <span class="n">sizeKeys</span><span class="p">),</span>
        <span class="n">metaData</span><span class="o">:</span> <span class="n">ColumnMetadata</span><span class="p">{</span>
            <span class="n">columnType</span><span class="o">:</span>            <span class="m">6</span> <span class="c">/*BINARY ARRAY*/</span><span class="p">,</span>
            <span class="n">encodings</span><span class="o">:</span>             <span class="p">[]</span><span class="kt">int32</span><span class="p">{</span><span class="m">0</span><span class="p">},</span>
            <span class="n">pathInSchema</span><span class="o">:</span>          <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"values"</span><span class="p">},</span>
            <span class="n">codec</span><span class="o">:</span>                 <span class="m">0</span><span class="p">,</span>
            <span class="n">numValues</span><span class="o">:</span>             <span class="kt">int64</span><span class="p">(</span><span class="n">numRows</span><span class="p">),</span>
            <span class="n">totalUncompressedSize</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">sizeValues</span><span class="p">),</span>
            <span class="n">totalCompressedSize</span><span class="o">:</span>   <span class="kt">int64</span><span class="p">(</span><span class="n">sizeValues</span><span class="p">),</span>
            <span class="n">dataPageOffset</span><span class="o">:</span>        <span class="kt">int64</span><span class="p">(</span><span class="n">magicBytesOffset</span> <span class="o">+</span> <span class="n">sizeKeys</span><span class="p">),</span>
            <span class="n">statistics</span><span class="o">:</span>            <span class="n">Statistics</span><span class="p">{</span><span class="n">nullCount</span><span class="o">:</span> <span class="m">0</span><span class="p">},</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">}</span>

<span class="n">parquetFooter</span><span class="o">.</span><span class="n">writeFileMetadata</span><span class="p">(</span>
    <span class="n">FileMetaData</span><span class="p">{</span>
        <span class="n">version</span><span class="o">:</span>  <span class="m">1</span><span class="p">,</span>
        <span class="n">schema</span><span class="o">:</span>   <span class="n">schema</span><span class="p">,</span>
        <span class="n">num_rows</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">numRows</span><span class="p">),</span>
        <span class="n">row_groups</span><span class="o">:</span> <span class="p">[]</span><span class="n">RowGroup</span><span class="p">{</span>
            <span class="p">{</span><span class="n">columns</span><span class="o">:</span> <span class="n">columns</span><span class="p">,</span>
                <span class="n">totalByteSize</span><span class="o">:</span> <span class="kt">int64</span><span class="p">(</span><span class="n">sizeKeys</span> <span class="o">+</span> <span class="n">sizeValues</span><span class="p">),</span>
                <span class="n">numRows</span><span class="o">:</span>       <span class="kt">int64</span><span class="p">(</span><span class="n">numRows</span><span class="p">)},</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Após escrever o <code class="language-plaintext highlighter-rouge">footer</code> no arquivo como um objeto Thrift, é necessário escrever o seu tamanho como um inteiro de 4 bytes. Isso é importante, porque o consumidor utiliza esse número para identificar onde o rodapé começa.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bs</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">4</span><span class="p">)</span>
<span class="n">binary</span><span class="o">.</span><span class="n">LittleEndian</span><span class="o">.</span><span class="n">PutUint32</span><span class="p">(</span><span class="n">bs</span><span class="p">,</span> <span class="kt">uint32</span><span class="p">(</span><span class="n">footer</span><span class="o">.</span><span class="n">Len</span><span class="p">()))</span>
<span class="n">parquetFooter</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">Write</span><span class="p">(</span><span class="n">bs</span><span class="p">)</span>
</code></pre></div></div>

<p>No final, o arquivo completo completo, com as duas colunas e o rodapé ficou da seguinte forma.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00000000  50 41 52 31 15 00 15 42  15 42 2c 15 06 15 00 15  |PAR1...B.B,.....|
00000010  00 15 00 00 00 07 00 00  00 63 68 61 76 65 5f 31  |.........chave_1|
00000020  07 00 00 00 63 68 61 76  65 5f 32 07 00 00 00 63  |....chave_2....c|
00000030  68 61 76 65 5f 33 15 00  15 42 15 42 2c 15 06 15  |have_3...B.B,...|
00000040  00 15 00 15 00 00 00 07  00 00 00 76 61 6c 6f 72  |...........valor|
00000050  5f 31 07 00 00 00 76 61  6c 6f 72 5f 32 07 00 00  |_1....valor_2...|
00000060  00 76 61 6c 6f 72 5f 33  15 02 19 3c 15 00 38 06  |.valor_3...&lt;..8.|
00000070  73 63 68 65 6d 61 15 04  00 15 0c 38 03 6b 65 79  |schema.....8.key|
00000080  15 00 00 15 0c 38 06 76  61 6c 75 65 73 15 00 00  |.....8.values...|
00000090  16 06 19 1c 19 2c 26 08  1c 15 0c 19 17 00 19 18  |.....,&amp;.........|
000000a0  03 6b 65 79 15 00 16 06  16 64 16 64 26 08 3c 36  |.key.....d.d&amp;.&lt;6|
000000b0  00 00 00 00 26 6c 1c 15  0c 19 17 00 19 18 06 76  |....&amp;l.........v|
000000c0  61 6c 75 65 73 15 00 16  06 16 64 16 64 26 6c 3c  |alues.....d.d&amp;l&lt;|
000000d0  36 00 00 00 00 16 c8 01  16 06 00 00 74 00 00 00  |6...........t...|
000000e0  50 41 52 31                                       |PAR1|
000000e4
</code></pre></div></div>
<p>No começo e no final do arquivo, temos os “magic bytes”,  a constante <code class="language-plaintext highlighter-rouge">PAR1</code> que identifica o arquivo como sendo um parquet.</p>

<p>O arquivo é mais simples que o gerado pelo fastparquet, mas não não está totalmente funcional: consegui ler com a implementação padrão em Java e Arrow, mas não usando Spark e nem o próprio fastparquet.</p>

<p>A implementação completa desse script está <a href="https://github.com/gdarruda/parquet-go/blob/Main/main.go">nesse arquivo</a>.</p>

<h2 id="é-isso-mais-ou-menos">É isso? Mais ou menos</h2>

<p>Imagino que para resolver o problema de compatibilidade, seja necessário preencher mais metadados, que algumas implementações tratam como obrigatória. Meu objetivo era entender melhor o formato, então essa implementação capenga já atendeu seu objetivo, não animei de ir atrás de resolver essa questão.</p>

<p>No futuro, mais interessante que fazer uma implementação completa, seria entender os impactos de diferentes estratégias para gerar um arquivo. Um arquivo parquet – contendo os mesmos dados – pode ser gerado de diferentes formas:</p>

<ul>
  <li>
    <p>Eu usei o <a href="https://parquet.apache.org/docs/file-format/data-pages/encodings/">encoding básico</a>, mas estratégias de <em>dictionary encoding</em> e <em>delta encoding</em> podem fazer uma enorme diferença, além da compactação pura e simples.</p>
  </li>
  <li>
    <p>Como funcionam o <a href="https://parquet.apache.org/docs/file-format/data-pages/encodings/">Page Index</a>? É possível ter uma boa performance de look-up para acessos baseados em linha?</p>
  </li>
  <li>
    <p>Qual o tamanho ideal de páginas e grupos de linha? Discute-se muito o problema de “small files” em Big Data, mas nunca vi essa discussão para a estrutura interna dos arquivos.</p>
  </li>
  <li>
    <p>Qual as diferenças entre as várias implementações? São relevantes?</p>
  </li>
</ul>

<p>Talvez, em um post futuro, eu explore essa questão das diferentes implementações e estratégias para gerar arquivos parquet.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Explicando os detalhes de um arquivo parquet]]></summary></entry><entry><title type="html">Low-code e complexidade acidental</title><link href="/2024/01/20/low-code-dilema.html" rel="alternate" type="text/html" title="Low-code e complexidade acidental" /><published>2024-01-20T00:00:00+00:00</published><updated>2024-01-20T00:00:00+00:00</updated><id>/2024/01/20/low-code-dilema</id><content type="html" xml:base="/2024/01/20/low-code-dilema.html"><![CDATA[<p>A proposta das soluções (low|no)-code é irresistível para os executivos. Quem antes dependia da área tecnologia para tudo – com o uso ferramentas low-code – consegue resolver problemas com mais autonomia, menor custo e maior agilidade.</p>

<p>Por outro lado, programadores costumam tratar com desdém a ideia de low-code. Muitas vezes por simples elitismo e protecionismo, mas também por experiências terríves: soluções desnecessariamente complexas, “lock-in” em plataformas proprietárias, dificuldades de operação e manutenção.</p>

<p>Entre os fatores para o sucesso ou fracasso, entendo que passe muito pela <a href="https://en.wikipedia.org/wiki/No_Silver_Bullet">complexidade acidental e essencial</a> do problema. Low-code funciona muita bem para problemas com baixa complexidade essencial, mas pode virar um caos para problemas de negócio complexos.</p>

<h1 id="complexidade-essencial-e-acidental">Complexidade essencial e acidental</h1>

<p>A complexidade acidental são questões “técnicas”, aspectos do problema que os engenheiros devem saber lidar e são inerentes ao desenvolvimento da solução. Por exemplo, como escolher o banco de dados ideal, que se adequa ao problema e atenda aos requisitos funcionais e não-funcionais.</p>

<p>A complexidade essencial se refere ao problema de negócio. Se precisamos implementar um sistema de tributação, não podemos mexer nos requisitos para simplificar o código.</p>

<p>Um boa solução de software lida com a complexidade essencial, adicionando o mínimo possível de complexidade acidental.</p>

<p>Não é uma tarefa fácil, mesmo com bons engenheiros e ferramentas, é comum acabar com mais complexidade que o necessário. Quando a solução fica a cargo de alguém não especializado, usando ferramentas limitadas, a situação tende a ficar muito pior.</p>

<p>Por isso, o quanto uma solução low-code faz sentido, depende muito da complexidade essencial. Não é uma relação linear, a complexidade acidental tende a crescer muito mais em função da complexidade essencial.</p>

<h1 id="o-cenário-do-caos">O cenário do caos</h1>

<p>Por natureza da especialidade, engenheiros são treinados para focar em complexidade acidental: é interessante discutir como montar uma linha do tempo em baixa latências para milhões de usuários, mas nem tanto em entender todas as regras de cobrança do ICMS.</p>

<p>Suponha que existe um processo para estimar valor dos impostos, consumindo arquivos tabulares e gerando outro, para relatório e acompanhamento. Esse tipo de cenário, normalmente é visto como o ideal para low-code: os contadores conseguem implementar e validar de forma autônonoma, as demandas de latência são mínimas e não é operacional.</p>

<p>Depois de algum tempo, temos o seguinte cenário:</p>

<ul>
  <li>
    <p>A ferramenta low-code depende de várias outras, que também não são adequadas mas resolvem outras limitações, como planilhas Excel e/ou scripts Python locais.</p>
  </li>
  <li>
    <p>Poucas pessoas conseguem entender a solução, porque é uma ferramenta proprietária e a lógica é representada como um diagrama incompreensível.</p>
  </li>
  <li>
    <p>O custo de migrar a solução para um sistema é muito alto (e ninguém quer fazer), é preciso pagar e sustentar indefinidamente a solução.</p>
  </li>
  <li>
    <p>O relatório também está sendo usado de forma operacional, porque é a melhor fonte dessa informação.</p>
  </li>
</ul>

<h1 id="a-natureza-do-problema">A natureza do problema</h1>

<p>O problema desse cenário hipotético, foi desconsiderar a complexidade essencial do problema. Um fluxo com arquitetura de solução simular, mas uma lógica de negócios simples, seria um bom caso de uso para low-code.</p>

<p>Em um cenário de lógica complexa, o ideal seria um programador competente, para desenvolver um código sustentável. Apesar de ser uma arquitetura simples, as regras podem demandar muito código e abstrações sofisticadas, cenário em que ferramentas low-code não são o ideal.</p>

<p>Muito dos esforços na engenharia de software, estão relacionados a lidar esse tipo de problema, por exemplo:</p>

<ul>
  <li>
    <p>A consolidação da programação orientada a objetos e mantras como <a href="https://en.wikipedia.org/wiki/SOLID">SOLID</a> e <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>, foi pelo anseio de melhor organização e abstração;</p>
  </li>
  <li>
    <p>o uso de ferramentas e estratégias de versionamento é fundamental em cenários de alta complexidade e colaboração;</p>
  </li>
  <li>
    <p>um dos atrativos da arquitetura de micro-serviços, é conseguir distribuir a complexidade essencial por múltiplos sistemas.</p>
  </li>
</ul>

<p>O melhor jeito de atuar usando as melhores práticas, é usando linguagens de programação e plataformas modernas.</p>

<h1 id="usando-bad-code-no-lugar">Usando “bad-code” no lugar</h1>

<p>Nem sempre é viável ter especialistas, para fazer desenhar e contruir a solução usandos as melhores ferramentas. Uma alternativa que não resolve, mas pode mitigar alguns problemas, é considerar o uso de soluções em código desenvolvida por quem não é especialista.</p>

<p>Posso estar errado – realmente não sou o melhor para opinar, dado que programo há muito tempo – mas entendo que o problema do usuário de low-code não é sobre aprender a programar.</p>

<p>Na área de dados, é muito comum ter pessoas que sabem programar bem, mas que não estão familiarizadas com processos de tecnologia e boas práticas. Inclusive, a ideia de usar soluções low-code, muitas vezes é para substituir códigos ruins de VBA ou em linguagens como PL/SQL e Transact-SQL.</p>

<p>Fazer um notebook linguição usando Python não é necessariamente melhor, reproduz os mesmos erros em outra tecnologia. A questão é facilitar uma possível evolução, para algo mais estruturado e sustentável.</p>

<p>Por exemplo, pode-se criar um ambiente restrito para implantar essas soluções, usar uma imagem com Python instalado e as bibliotecas básicas. Mesmo que seja necessário uma re-escrita completa do código no futuro, esse processo costuma ser bem menos doloroso usando a mesma plataforma, que migrando de outra.</p>

<h1 id="acertando-o-problema">Acertando o problema</h1>

<p>Não acho low-code uma ideia terrível, mas não muito boa também: resolve uma classe de problemas, mas as promessas são irreais. A ideia  de abolir código não é nova, há 20 anos tinha-se a ideia de <a href="https://en.wikipedia.org/wiki/Rational_Software_Modeler">gerar código a partir de UML</a> e ferramentas <a href="https://en.wikipedia.org/wiki/WYSIWYG">WYSIWYG</a> para construir sites. Por que elas não se tornaram o padrão?</p>

<p>Grandes empresas – as consumidoras dessas ferramentas – são cheias de complexidade essencial, que são difíceis de implementar em código e pior ainda em ferramentas low-code. É importante identificar, se o problema a ser resolvido tem esse tipo complexidade, pois há o risco da solução virar mais um problema.</p>

<p>Entendo que muito disso, passa pelos profissionais de tecnologia, especialmente na etapa de arquitetura de solução. Somos ensinados a lidar com complexidade acidental, <a href="https://blog.bradfieldcs.com/you-are-not-google-84912cf44afb?gi=7cf9084614af">pecando mais por excesso</a> que falta de precaução. Por outro lado, a complexidade essencial acaba aparecendo apenas no final ou no meio da execução do projeto, porque é normal abstrair o problema nessa etapa.</p>

<p>Em resumo: uma arquitetura simples não significa um código simples, nenhuma ferramenta resolverá complexidade essencial, subestimá-la é um erro recorrente que precisamos nos atentar.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Soluções (low|no)-code não resolvem complexidade inerente]]></summary></entry></feed>