<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Backend | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/backend/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/backend/index.xml" rel="self" type="application/rss+xml"/><item><title>Eu Fiz um Sistema de Data Mining pra Minha Namorada Influencer — Dicas e Truques</title><link>https://akitaonrails.github.io/2026/03/04/eu-fiz-um-sistema-de-data-mining-pra-minha-namorada-influencer-dicas-e-truques/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/04/eu-fiz-um-sistema-de-data-mining-pra-minha-namorada-influencer-dicas-e-truques/</guid><pubDate>Wed, 04 Mar 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Minha namorada é influencer de games, anime, cosplay e cultura pop. Faz entrevistas, cobre convenções, produz conteúdo pra múltiplas plataformas, negocia com patrocinadores. É uma operação profissional de uma pessoa com uma equipe enxuta. E como toda operação profissional, ela precisa de dados.&lt;/p&gt;
&lt;p&gt;O problema é que coletar dados de redes sociais é trabalho braçal. Abrir Instagram, YouTube, X, olhar números, comparar com concorrentes, checar calendário de eventos, monitorar patrocinadores, ler comentários, calcular engajamento, decidir preço de publi. Tudo manual, tudo repetitivo, tudo consumindo tempo que deveria ir pra criação de conteúdo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Minha namorada é influencer de games, anime, cosplay e cultura pop. Faz entrevistas, cobre convenções, produz conteúdo pra múltiplas plataformas, negocia com patrocinadores. É uma operação profissional de uma pessoa com uma equipe enxuta. E como toda operação profissional, ela precisa de dados.</p>
<p>O problema é que coletar dados de redes sociais é trabalho braçal. Abrir Instagram, YouTube, X, olhar números, comparar com concorrentes, checar calendário de eventos, monitorar patrocinadores, ler comentários, calcular engajamento, decidir preço de publi. Tudo manual, tudo repetitivo, tudo consumindo tempo que deveria ir pra criação de conteúdo.</p>
<p>Fiz o que qualquer programador namorado faria: construí um sistema de data mining completo, com coleta automatizada, análise via LLM, e um chatbot no Discord onde ela conversa com os dados em linguagem natural.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-15-10.jpg" alt="mila-bot"  loading="lazy" /></p>
<p>Este artigo não é sobre o código em si (o projeto não será open source). É sobre o processo de construção, as decisões que só aparecem depois que você começa, os truques técnicos que salvaram o projeto, e por que um documento de desejos da sua usuária vale mais que qualquer spec funcional.</p>
<h2>Comece Pelo Desejo, Não Pela Arquitetura<span class="hx:absolute hx:-mt-20" id="comece-pelo-desejo-não-pela-arquitetura"></span>
    <a href="#comece-pelo-desejo-n%c3%a3o-pela-arquitetura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A primeira coisa que fiz foi sentar com ela e pedir que falasse livremente o que queria. Sem linguagem técnica, sem formulário, sem user stories. As próprias palavras dela. Gravei num documento IDEA.md que virou o norte do projeto inteiro.</p>
<p>O que saiu dali:</p>
<blockquote>
  <p>&ldquo;Maior dificuldade hoje é saber qual tipo de conteúdo criar que pode dar muito certo e por que deram certo, o que fez dar o resultado que deu. Tem que ler comentários dos vídeos pra entender motivos gerais.&rdquo;</p>

</blockquote>
<blockquote>
  <p>&ldquo;Quanto deveria/poderia cobrar por uma publicidade, baseado na pesquisa de engajamento, concorrência, patrocinadores, eventos.&rdquo;</p>

</blockquote>
<blockquote>
  <p>&ldquo;Como fazer uma publicidade que não parece publicidade, conteúdo pago que traz engajamento sem parecer só propaganda de venda.&rdquo;</p>

</blockquote>
<p>Ela não pediu um dashboard. Não pediu gráficos. Não pediu relatórios em PDF. Pediu respostas pra problemas concretos do dia a dia dela. Isso mudou completamente a forma como eu abordei a arquitetura.</p>
<p>Se eu tivesse começado pela spec técnica, teria construído uma plataforma de analytics com charts bonitos e exports em CSV. O sistema que ela realmente precisava era algo mais parecido com uma assistente que conhece os dados e responde perguntas em português.</p>
<p>O IDEA.md também listava concorrentes e patrocinadores iniciais. Marcas com as quais ela poderia trabalhar (restaurantes temáticos, lojas de figures, Crunchyroll, Piticas, Fanlab). Perfis de referência no Instagram. Tudo isso virou dados de seed. O documento não era um spec — era uma conversa que virou requisito orgânico.</p>
<h2>58 Commits em 3 Dias<span class="hx:absolute hx:-mt-20" id="58-commits-em-3-dias"></span>
    <a href="#58-commits-em-3-dias" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto foi construído em 3 dias. Não 3 semanas, não 3 meses. 58 commits, 3 fases distintas:</p>
<p><strong>Dia 1 — O Motor de Dados.</strong> 12 commits. Scaffold Rails 8, modelos, coletores pra YouTube/Instagram/X, integração com LLM. Pipeline de discovery pra encontrar novos perfis automaticamente. Scoring de performance por post. Análise de sentimento de comentários via Claude. No final do dia, o sistema coletava dados das 3 plataformas e classificava cada post como viral, acima da média, na média, abaixo ou flop.</p>
<p><strong>Dia 2 — O Cérebro e a Voz.</strong> 35 commits. O dia mais intenso. Dois sistemas grandes apareceram: o &ldquo;Oracle&rdquo; (rastreamento de eventos, convenções, lançamentos de jogos/filmes/anime, notícias) e o chatbot Discord com tool calling via RubyLLM. Também hardening pra produção, Docker, guia de deploy. E uma pivotada grande: troquei o relatório semanal por email por 5 digests diários via Discord.</p>
<p><strong>Dia 3 — Entretenimento e Resiliência.</strong> 11 commits. Steam games (Store API + SteamSpy), AniList pra tracking de animes da temporada, analytics de crescimento, geração de imagens via Gemini, sistema de auto-healing pra URLs quebradas.</p>
<p>Os números finais:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Arquivos</td>
          <td>430</td>
      </tr>
      <tr>
          <td>Linhas totais</td>
          <td>37.088</td>
      </tr>
      <tr>
          <td>Linhas de Ruby</td>
          <td>25.562</td>
      </tr>
      <tr>
          <td>Testes</td>
          <td>916 (0 falhas)</td>
      </tr>
      <tr>
          <td>Modelos</td>
          <td>17</td>
      </tr>
      <tr>
          <td>Jobs agendados</td>
          <td>25+</td>
      </tr>
      <tr>
          <td>Tools do bot</td>
          <td>40+</td>
      </tr>
      <tr>
          <td>Prompts YAML</td>
          <td>23</td>
      </tr>
      <tr>
          <td>Integrações externas</td>
          <td>12+ APIs</td>
      </tr>
  </tbody>
</table>
<p>Esses números não são mérito meu. Claude fez boa parte do código. Mas a direção, as decisões arquiteturais, e principalmente a validação de cada step foram humanas. Claude não sabe o que uma influencer brasileira de cosplay precisa. Eu também não sabia — mas ela me disse.</p>
<h2>A Arquitetura que Emergiu<span class="hx:absolute hx:-mt-20" id="a-arquitetura-que-emergiu"></span>
    <a href="#a-arquitetura-que-emergiu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema final é um Rails 8 headless. Zero interface web. Sem views, sem controllers de verdade (só o <code>/up</code> pra health check). Toda funcionalidade é entregue via background jobs, rake tasks, e o chatbot Discord.</p>
<p>A stack:</p>
<ul>
<li><strong>Rails 8.1</strong> com Solid Queue (jobs) e Solid Cache</li>
<li><strong>SQLite3</strong> em produção (WAL mode, bind mount entre containers)</li>
<li><strong>RubyLLM</strong> pra integração com Claude Sonnet via OpenRouter e Grok</li>
<li><strong>Ferrum</strong> pra scraping com headless Chrome</li>
<li><strong>Discordrb</strong> pro chatbot</li>
<li><strong>Docker Compose</strong> com 4 serviços</li>
</ul>
<p>O <code>docker-compose.yml</code> ficou enxuto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">x-app</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./data/storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;127.0.0.1:3000:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;curl&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-f&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:80/up&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">512M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">bundle exec rake solid_queue:start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1G</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">discord</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">bundle exec rake discord:start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">chrome</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">chromedp/headless-shell:stable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1G</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>4 containers: app (Puma, basicamente só o health check), jobs (Solid Queue rodando 25+ jobs scheduled), discord (o bot), e chrome (headless browser pra scraping). Todo o estado fica em SQLite via bind mount no host. Sem Redis, sem Postgres, sem infraestrutura extra.</p>
<h2>Idempotência Acima de Tudo<span class="hx:absolute hx:-mt-20" id="idempotência-acima-de-tudo"></span>
    <a href="#idempot%c3%aancia-acima-de-tudo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu tivesse que escolher um conceito pra resumir o projeto, seria <strong>idempotência</strong>. Todo job pode rodar duas vezes seguidas sem criar duplicatas, sem corromper dados, sem efeitos colaterais.</p>
<p>O pattern base:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Collection</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">BaseCollectorJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">    <span class="no">SNAPSHOT_DEDUP_WINDOW</span> <span class="o">=</span> <span class="mi">1</span><span class="o">.</span><span class="n">hour</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">retry_on</span> <span class="no">StandardError</span><span class="p">,</span> <span class="ss">wait</span><span class="p">:</span> <span class="ss">:polynomially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="ss">profile_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">scope</span> <span class="o">=</span> <span class="n">profile_id</span> <span class="p">?</span> <span class="no">SocialProfile</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="n">profile_id</span><span class="p">)</span> <span class="p">:</span> <span class="n">profiles_to_collect</span>
</span></span><span class="line"><span class="cl">      <span class="n">scope</span><span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">profile</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="n">log</span> <span class="o">=</span> <span class="n">find_or_create_log</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">begin</span>
</span></span><span class="line"><span class="cl">          <span class="n">items</span> <span class="o">=</span> <span class="n">collect_for_profile</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">profile</span><span class="o">.</span><span class="n">touch</span><span class="p">(</span><span class="ss">:last_collected_at</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">log</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:completed</span><span class="p">,</span> <span class="ss">items_collected</span><span class="p">:</span> <span class="n">items</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">          <span class="n">log</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:failed</span><span class="p">,</span> <span class="ss">error_message</span><span class="p">:</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">class</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">raise</span> <span class="k">if</span> <span class="n">should_raise?</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">upsert_post</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="n">attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span> <span class="o">=</span> <span class="n">profile</span><span class="o">.</span><span class="n">social_posts</span><span class="o">.</span><span class="n">find_or_initialize_by</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="ss">platform_post_id</span><span class="p">:</span> <span class="n">attrs</span><span class="o">[</span><span class="ss">:platform_post_id</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span><span class="o">.</span><span class="n">assign_attributes</span><span class="p">(</span><span class="n">attrs</span><span class="o">.</span><span class="n">except</span><span class="p">(</span><span class="ss">:platform_post_id</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span><span class="o">.</span><span class="n">save!</span>
</span></span><span class="line"><span class="cl">      <span class="n">post</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">record_snapshot</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="n">metrics</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">if</span> <span class="n">profile</span><span class="o">.</span><span class="n">profile_snapshots</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;captured_at &gt; ?&#34;</span><span class="p">,</span> <span class="no">SNAPSHOT_DEDUP_WINDOW</span><span class="o">.</span><span class="n">ago</span><span class="p">)</span><span class="o">.</span><span class="n">exists?</span>
</span></span><span class="line"><span class="cl">      <span class="n">profile</span><span class="o">.</span><span class="n">profile_snapshots</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span><span class="ss">captured_at</span><span class="p">:</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">,</span> <span class="o">**</span><span class="n">metrics</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Dois mecanismos de proteção: <code>find_or_initialize_by(platform_post_id)</code> pra posts (upsert, não insert), e <code>SNAPSHOT_DEDUP_WINDOW</code> pra snapshots (ignora se já coletou na última hora). Se o job crashear no meio e o Solid Queue reenfileirar, nada duplica.</p>
<p>Erros de scraping (<code>BlockedError</code>, <code>RateLimitError</code>) são engolidos silenciosamente — o retry não ajuda, o rate limit precisa esfriar, e ser bloqueado é esperado. Erros de verdade (banco, rede, bugs) sobem pro retry com backoff polinomial.</p>
<p>Essa decisão de &ldquo;engolir certos erros&rdquo; parece errada até você rodar o sistema por uma semana e ver que o Instagram bloqueia uma em cada 5 coletas. Se cada bloqueio gerasse 3 retries com backoff exponencial, o sistema ficaria perpetuamente atrasado.</p>
<h2>nil vs Zero<span class="hx:absolute hx:-mt-20" id="nil-vs-zero"></span>
    <a href="#nil-vs-zero" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é um daqueles bugs conceituais que você só descobre com dados reais. Quando o Instagram não retorna o número de compartilhamentos de um post (porque a API simplesmente não fornece esse dado), o valor deveria ser <code>nil</code> ou <code>0</code>?</p>
<p>A diferença importa. <code>nil</code> significa <em>&ldquo;não temos esse dado&rdquo;</em>. Zero significa <em>&ldquo;temos o dado e ele é zero&rdquo;</em>. Se você trata <code>nil</code> como zero, a análise de LLM conclui que ninguém compartilha posts no Instagram — o que é falso. A API simplesmente não expõe essa métrica.</p>
<p>Criei um snippet de prompt reutilizável só pra isso:</p>
<blockquote>
  <p>Quando um campo é null: Não diga &ldquo;0 likes&rdquo; — diga &ldquo;dado não disponível para esta plataforma&rdquo;. Quando comparar plataformas, avise que certas métricas não são comparáveis. Quando é zero: Reporte normalmente — o dado é real e confirmado pela API.</p>

</blockquote>
<p>Esse snippet é incluído em todo prompt que lida com dados numéricos. Sem ele, o LLM mistura alegremente &ldquo;não coletado&rdquo; com &ldquo;zero de verdade&rdquo; e tira conclusões erradas.</p>
<h2>O Truque do Chrome Headless: Host Header Bypass<span class="hx:absolute hx:-mt-20" id="o-truque-do-chrome-headless-host-header-bypass"></span>
    <a href="#o-truque-do-chrome-headless-host-header-bypass" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto usa <code>chromedp/headless-shell</code> num container separado pra scraping. Funciona perfeitamente. Até você tentar conectar o Ferrum (a gem Ruby de automação de Chrome) nele via Docker networking.</p>
<p>O problema: <code>chromedp/headless-shell</code> usa um proxy socat na porta 9222 que rejeita qualquer request cujo header <code>Host</code> não seja <code>localhost</code> ou um IP. Quando o Ferrum tenta conectar em <code>http://chrome:9222</code>, o Host header vai como <code>chrome:9222</code>, e o socat recusa.</p>
<p>A solução foi descobrir a URL do WebSocket manualmente, forjando o header:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">discover_ws_url</span><span class="p">(</span><span class="n">chrome_url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">chrome_url</span><span class="si">}</span><span class="s2">/json/version&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">::</span><span class="no">Get</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">[</span><span class="s2">&#34;Host&#34;</span><span class="o">]</span> <span class="o">=</span> <span class="s2">&#34;localhost&#34;</span>  <span class="c1"># bypass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">start</span><span class="p">(</span><span class="n">uri</span><span class="o">.</span><span class="n">hostname</span><span class="p">,</span> <span class="n">uri</span><span class="o">.</span><span class="n">port</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">http</span><span class="o">|</span> <span class="n">http</span><span class="o">.</span><span class="n">request</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">ws_url</span> <span class="o">=</span> <span class="n">data</span><span class="o">[</span><span class="s2">&#34;webSocketDebuggerUrl&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">nil</span> <span class="k">unless</span> <span class="n">ws_url</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># WS URL aponta pra 127.0.0.1 dentro do container — troca pelo hostname acessível</span>
</span></span><span class="line"><span class="cl">  <span class="n">remote_uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">chrome_url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">ws_url</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">%r{://[^/]+}</span><span class="p">,</span> <span class="s2">&#34;://</span><span class="si">#{</span><span class="n">remote_uri</span><span class="o">.</span><span class="n">host</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">remote_uri</span><span class="o">.</span><span class="n">port</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Faço o GET em <code>/json/version</code> com <code>Host: localhost</code> pra passar pelo socat. Recebo a URL de WebSocket (que aponta pra <code>127.0.0.1</code> — inútil fora do container). Reescrevo o hostname pra <code>chrome:9222</code> (o nome do serviço no Docker Compose). Passo a <code>ws_url</code> direto pro Ferrum, que abre o WebSocket sem passar pelo HTTP do socat.</p>
<p>Esse tipo de coisa não vem à cabeça no começo. Você descobre depois de 1 hora de <code>connection refused</code> e leitura do código fonte do <code>chromedp/headless-shell</code>.</p>
<h2>Tool Calling: O Bot que Consulta o Banco Sozinho<span class="hx:absolute hx:-mt-20" id="tool-calling-o-bot-que-consulta-o-banco-sozinho"></span>
    <a href="#tool-calling-o-bot-que-consulta-o-banco-sozinho" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A parte mais legal do projeto é o chatbot no Discord. Ela digita uma pergunta em português (&ldquo;como foram meus posts essa semana?&rdquo;) e o bot chama as ferramentas certas, consulta o banco, e responde com dados reais.</p>
<p>O segredo é o tool calling do RubyLLM. Cada ferramenta é uma classe Ruby com <code>description</code> e <code>params</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PostPerformanceTool</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Retorna estatísticas de performance dos posts: baseline, &#34;</span> <span class="p">\</span>
</span></span><span class="line"><span class="cl">              <span class="s2">&#34;breakdown por tipo de conteúdo, melhores horários, melhores hashtags.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:username</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Username do perfil (ex: milaoliveira.png)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:analysis</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Tipo: baseline, content_types, timing, hashtags&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">required</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">username</span><span class="p">:,</span> <span class="ss">analysis</span><span class="p">:</span> <span class="s2">&#34;baseline&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">profile</span> <span class="o">=</span> <span class="no">SocialProfile</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">username</span><span class="p">:</span> <span class="n">username</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Perfil &#39;</span><span class="si">#{</span><span class="n">username</span><span class="si">}</span><span class="s2">&#39; não encontrado.&#34;</span> <span class="k">unless</span> <span class="n">profile</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">analysis</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span><span class="o">.</span><span class="n">downcase</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;baseline&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">baseline</span> <span class="o">=</span> <span class="no">PostPerformance</span><span class="o">.</span><span class="n">compute_baseline</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="ss">username</span><span class="p">:</span> <span class="n">profile</span><span class="o">.</span><span class="n">username</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">post_count</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:post_count</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">mean_views</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:mean_views</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="ss">stddev_views</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:stddev_views</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="ss">mean_engagement</span><span class="p">:</span> <span class="n">baseline</span><span class="o">[</span><span class="ss">:mean_engagement</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;timing&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">PostPerformance</span><span class="o">.</span><span class="n">timing_breakdown</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">entry</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">day</span><span class="p">:</span> <span class="n">days</span><span class="o">[</span><span class="n">entry</span><span class="o">[</span><span class="ss">:day</span><span class="o">]]</span><span class="p">,</span> <span class="ss">hour</span><span class="p">:</span> <span class="s2">&#34;%02d:00&#34;</span> <span class="o">%</span> <span class="n">entry</span><span class="o">[</span><span class="ss">:hour</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="ss">avg_percentile</span><span class="p">:</span> <span class="n">entry</span><span class="o">[</span><span class="ss">:avg_percentile</span><span class="o">].</span><span class="n">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O LLM recebe a lista de 40+ ferramentas com suas descrições, decide quais chamar com quais parâmetros, recebe os resultados, e formula a resposta. O bot no Discord mostra status em tempo real enquanto processa: <em>&ldquo;Consultando métricas de perfis&hellip; Verificando crescimento&hellip; Analisando desempenho de posts&hellip; (3 consultas)&rdquo;</em>.</p>
<p>O truque foi fazer cada tool retornar dados estruturados (Hashes e Arrays), não texto formatado. O LLM é muito melhor formatando dados brutos em resposta conversacional do que tentando parsear texto semi-estruturado.</p>
<p>Outro detalhe que só apareceu no uso real: clamping de parâmetros. Quando o LLM pede <code>limit: 999</code> num parâmetro que aceita no máximo 50, ao invés de retornar erro, faço <code>[[val.to_i, 1].max, 50].min</code>. O LLM erra parâmetros mais do que você imagina. Cada erro gera um round-trip de correção que custa tokens e tempo.</p>
<h2>Composable Prompts: YAML &gt; Strings Hardcoded<span class="hx:absolute hx:-mt-20" id="composable-prompts-yaml--strings-hardcoded"></span>
    <a href="#composable-prompts-yaml--strings-hardcoded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os prompts do sistema vivem em YAML no <code>config/prompts/</code>. Cada um tem um <code>system</code> (instruções fixas), um <code>template</code> (com interpolação de dados), e opcionalmente <code>includes</code> (snippets reutilizáveis):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">Llm</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">PromptBuilder</span>
</span></span><span class="line"><span class="cl">    <span class="no">PROMPTS_DIR</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;config&#34;</span><span class="p">,</span> <span class="s2">&#34;prompts&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
</span></span><span class="line"><span class="cl">      <span class="k">def</span> <span class="nf">build</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl">        <span class="n">prompt</span> <span class="o">=</span> <span class="n">load_prompt</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="n">load_prompt</span><span class="p">(</span><span class="ss">:_base_context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">includes</span> <span class="o">=</span> <span class="n">resolve_includes</span><span class="p">(</span><span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;includes&#34;</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_parts</span> <span class="o">=</span> <span class="o">[</span><span class="n">base</span><span class="o">[</span><span class="s2">&#34;system&#34;</span><span class="o">]</span><span class="p">,</span> <span class="n">includes</span><span class="p">,</span> <span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;system&#34;</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl">          <span class="o">.</span><span class="n">compact</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:blank?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_message</span> <span class="o">=</span> <span class="n">system_parts</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_message</span> <span class="o">=</span> <span class="n">interpolate</span><span class="p">(</span><span class="n">prompt</span><span class="o">[</span><span class="s2">&#34;template&#34;</span><span class="o">]</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">system</span><span class="p">:</span> <span class="n">system_message</span><span class="p">,</span> <span class="ss">user</span><span class="p">:</span> <span class="n">user_message</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>_base_context.yml</code> tem informações que todo prompt precisa (quem é a influencer, nichos, público, diretrizes gerais). Os snippets em <code>config/prompts/snippets/</code> resolvem problemas recorrentes:</p>
<ul>
<li><code>null_vs_zero.yml</code> — a distinção nil/zero explicada acima</li>
<li><code>never_invent.yml</code> — &ldquo;NUNCA invente dados, analise só o que está presente&rdquo;</li>
<li><code>json_only.yml</code> — &ldquo;responda SOMENTE em JSON válido&rdquo;</li>
</ul>
<p>Quando um prompt precisa de qualquer combinação desses, basta listar nos <code>includes</code>. Sem duplicação, sem risco de instruções conflitantes entre prompts.</p>
<p>Essa decisão veio de um bug real: dois prompts diferentes davam instruções contraditórias sobre como tratar valores nulos. Um dizia &ldquo;use 0&rdquo;, outro dizia &ldquo;use null&rdquo;. O LLM obedecia um ou outro dependendo do dia. Centralizar em snippets eliminou a classe inteira de inconsistência.</p>
<h2>Discovery Pipeline: Mineração Autônoma de Perfis<span class="hx:absolute hx:-mt-20" id="discovery-pipeline-mineração-autônoma-de-perfis"></span>
    <a href="#discovery-pipeline-minera%c3%a7%c3%a3o-aut%c3%b4noma-de-perfis" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema inicial partiu de uma lista manual de concorrentes e patrocinadores. Mas a influencer vive num ecossistema dinâmico — novos creators aparecem toda semana, marcas surgem e somem. O discovery pipeline automatiza isso.</p>
<p>Roda toda sexta-feira às 2 da manhã. Começa minerando dados que já temos no banco: quem tá sendo @mencionado nos posts, quem comenta com frequência e alto engagement, que perfis aparecem em links de bio, quem usa hashtags de marca (#publi, #parceria), que links de Linktree apontam pra outros criadores. Tudo puro SQL, sem API calls, sem risco de rate limit.</p>
<p>Os candidatos que saem daí são validados contra as APIs das plataformas (o Instagram existe? O canal do YouTube é real?), depois enriquecidos com conexões cross-platform (se validamos um Instagram, procuramos o YouTube e X do mesmo criador).</p>
<p>Uma chamada de LLM em batch avalia todos os candidatos, classifica como concorrente, patrocinador potencial ou irrelevante, e dá scores de relevância e fit com o nicho. Os top 3 viram perfis rastreados de verdade — o sistema começa a coletar dados deles automaticamente.</p>
<p>Depois, agrupa tudo em entidades <code>Creator</code>. A influencer é profundidade 0. Concorrentes diretos, profundidade 1. Menções de concorrentes, profundidade 2. Limite máximo de 3 graus de separação, pra evitar cascata infinita.</p>
<h2>De Relatório Semanal pra Digests Diários: Ouvindo a Usuária<span class="hx:absolute hx:-mt-20" id="de-relatório-semanal-pra-digests-diários-ouvindo-a-usuária"></span>
    <a href="#de-relat%c3%b3rio-semanal-pra-digests-di%c3%a1rios-ouvindo-a-usu%c3%a1ria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O plano original era um relatório semanal por email com 9 seções. Mas no meio do caminho decidi que seria mais dinâmico não sair do Discord mesmo e ter essa informação sempre disponível.</p>
<p>Pivotei pra 5 digests diários no Discord:</p>
<ul>
<li><strong>Segunda</strong> — Recap de Performance (seguidores, crescimento, top posts)</li>
<li><strong>Terça</strong> — Radar de Concorrentes (snapshot, notícias, estratégias)</li>
<li><strong>Quarta</strong> — Playbook de Conteúdo (agenda, tendências, hashtags)</li>
<li><strong>Quinta</strong> — Oportunidades e Preços (marcas, pricing, pacotes)</li>
<li><strong>Sexta</strong> — Semana que Vem (eventos, ações prioritárias)</li>
</ul>
<p>Cada digest tem itens numerados com botões de feedback. Ela marca o que achou útil e o que não achou. Esse feedback alimenta as análises futuras — o sistema aprende quais tipos de insight ela valoriza.</p>
<p>Essa mudança, que não estava em nenhum plano, provavelmente foi a que mais melhorou a experiência dela. Toda manhã às 9h ela abre o Discord e tem um resumo fresco, digerível, acionável. O email semanal ficou como backup, comentado no <code>recurring.yml</code>.</p>
<h2>O Oracle: Contexto que o Algoritmo Não Vê<span class="hx:absolute hx:-mt-20" id="o-oracle-contexto-que-o-algoritmo-não-vê"></span>
    <a href="#o-oracle-contexto-que-o-algoritmo-n%c3%a3o-v%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-24-24.jpg" alt="playbook"  loading="lazy" /></p>
<p>Dados de redes sociais sem contexto externo são meio inúteis. <em>&ldquo;Esse post teve 3x mais views que a média&rdquo;</em> é um fato. <em>&ldquo;Esse post teve 3x mais views porque saiu no dia do trailer do novo Sonic&rdquo;</em> é uma insight.</p>
<p>O Oracle é o subsistema que fornece esse contexto.</p>
<p>A parte mais crítica é o rastreamento de convenções: CCXP, Anime Friends, BGS, e dezenas de outros eventos do calendário geek. Datas, preços, venues, links de ingresso, contas de Instagram/X do evento. Monitora diariamente por mudanças (data alterada, cancelamento, anúncio de convidado). A influencer planeja o calendário inteiro em torno de convenções. Se a CCXP muda de data, ela precisa saber no dia, não na semana seguinte.</p>
<p>Também rastreia lançamentos de filmes/séries via TMDB, jogos via IGDB (com OAuth do Twitch), animes via AniList — tudo filtrado pra conteúdo geek, janela de 90 dias pra frente. Aniversários de franquias (usando TMDB, IGDB, AniList, Wikimedia) e feriados brasileiros com cálculo de datas móveis. Sim, tem um calculador de Páscoa no código baseado no algoritmo computus. E notícias via scraping de RSS e X/Twitter a cada 6 horas, classificadas por relevância ao nicho.</p>
<p>O evento tracker scrapa sites de convenção com Chrome headless e usa LLM com temperatura 0.1 pra extrair metadados estruturados (data, preço, venue). Tem fallback com regex pra datas no formato brasileiro (&ldquo;15 de março de 2026&rdquo;). Deduplicação por fuzzy matching de nome + proximidade de data — porque &ldquo;CCXP 2026&rdquo;, &ldquo;CCXP São Paulo 2026&rdquo; e &ldquo;CCXP SP&rdquo; são o mesmo evento.</p>
<h2>SQLite em Produção: Sim, Funciona<span class="hx:absolute hx:-mt-20" id="sqlite-em-produção-sim-funciona"></span>
    <a href="#sqlite-em-produ%c3%a7%c3%a3o-sim-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SQLite3 em produção. Com WAL mode, um único writer por vez é suficiente porque o sistema tem um padrão previsível de escrita (jobs sequenciais, nunca concorrentes no mesmo segundo). Os dados ficam num bind mount do Docker (<code>./data/storage:/rails/storage</code>) e backup é <code>cp data/storage/*.db backups/</code>.</p>
<p>Rails 8 trata SQLite como cidadão de primeira classe. Solid Queue e Solid Cache rodam em SQLite sem problemas. O overhead de um Postgres pra um sistema que serve uma pessoa não se justifica.</p>
<h2>25+ Jobs Agendados<span class="hx:absolute hx:-mt-20" id="25-jobs-agendados"></span>
    <a href="#25-jobs-agendados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <code>config/recurring.yml</code> tem 25+ jobs com horários distribuídos pra não sobrecarregar a máquina:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">youtube_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::YoutubeCollectorJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every day at 3am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">instagram_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::InstagramCollectorJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every day at 4am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">x_collection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Collection::XCollectorJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every 2 days at 5am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">oracle_events</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Oracle::EventTrackerJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every Monday at 2am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">convention_monitor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Oracle::ConventionMonitorJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every day at 5am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">discovery_pipeline</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Discovery::OrchestratorJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every Friday at 2am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">comment_sentiment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Analysis::CommentSentimentJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every Sunday at 5am</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">weekly_analysis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">class</span><span class="p">:</span><span class="w"> </span><span class="l">Analysis::WeeklyAnalysisJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="l">every Sunday at 6am</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Coleta roda de madrugada (3am-5am). Oracle de inteligência começa na segunda às 2am. Discovery na sexta às 2am. Análise pesada (sentimento, estratégia) no domingo quando não tem digest pra entregar. Digests de segunda a sexta às 9am, cada dia com tema diferente. Manutenção (limpeza de jobs terminados, health check de URLs, agregação de dados antigos) nos intervalos.</p>
<p>O padrão é intencional: nenhum job pesado compete com os digests da manhã. Se a análise semanal atrasar, o digest de segunda ainda entrega porque usa dados da coleta de sábado, não da análise de domingo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-29-08.jpg" alt="nano banana 2"  loading="lazy" /></p>
<h2>O Que Não Dá Pra Prever Sem Rodar<span class="hx:absolute hx:-mt-20" id="o-que-não-dá-pra-prever-sem-rodar"></span>
    <a href="#o-que-n%c3%a3o-d%c3%a1-pra-prever-sem-rodar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Algumas coisas que só descobri depois que o sistema tava rodando em produção.</p>
<p>O Instagram bloqueia scraping aleatoriamente. Não importa quão stealth seu browser é. A configuração inicial era só scraping via Ferrum. De cara, resolvi adicionar o <strong>Apify</strong> como fonte primária (API paga, mais confiável) e o scraping virou fallback.</p>
<p>LLMs alucinam timestamps. Pedi pro bot criar um lembrete pra &ldquo;amanhã às 15h&rdquo; e ele calculou uma data em 2024. A solução foi injetar <code>current_datetime</code> em todo prompt que lida com tempo e instruir explicitamente: <em>&ldquo;calcule baseado em current_datetime, converta pra ISO 8601 com UTC-3&rdquo;</em>.</p>
<p>O Discord tem limite de 2000 caracteres por mensagem, o que parece óbvio até seu bot mandar uma análise de 4000 chars e o Discord simplesmente truncar. Implementei split automático que quebra no último <code>\n</code> antes do limite.</p>
<p>Os tool calls falham com frequência em parâmetros, o que me surpreendeu. O LLM pede <code>limit: 100</code> quando o máximo é 50, ou manda o username sem o @ quando deveria ter. Clamping silencioso (<code>[[val.to_i, 1].max, 50].min</code>) e normalização de input (<code>.strip</code>, <code>.delete(&quot;@&quot;)</code>) em toda tool eliminaram uma categoria inteira de erros que estavam consumindo tokens em loops de retry.</p>
<p>Convenções mudam de data mais do que eu imaginava. Adicionei um sistema de announcements que acompanha cada mudança (data, venue, preço, cancelamento) com timestamp. O evento tracker roda toda segunda, mas o convention monitor roda DIÁRIO porque a última coisa que a influencer quer é descobrir que uma convenção mudou de data depois de já ter reservado hotel.</p>
<h2>O Bot Como Interface<span class="hx:absolute hx:-mt-20" id="o-bot-como-interface"></span>
    <a href="#o-bot-como-interface" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/03/screenshot-2026-03-04_13-28-05.jpg" alt="eventos"  loading="lazy" /></p>
<p>A decisão mais acertada do projeto foi não fazer interface web. O Discord já é onde a influencer passa o dia. O bot vive ali, disponível a qualquer momento. Ela digita <em>&ldquo;o que eu deveria postar essa semana?&rdquo;</em> e recebe uma resposta contextualizada com dados dos seus posts, dos concorrentes, do calendário de eventos, das tendências de hashtag, e dos animes/games da temporada.</p>
<p>O chatbot combina múltiplas tools numa resposta. Se ela pergunta sobre estratégia de conteúdo, o LLM chama: digest de conteúdo + calendário de eventos + tendências de hashtag + animes da temporada + jogos da Steam. Cinco consultas ao banco, tudo costurado numa resposta conversacional em português.</p>
<p>Tem até um modo de <strong>&ldquo;pensamento profundo&rdquo;</strong> que ativa quando detecta palavras como &ldquo;pesquise&rdquo;, &ldquo;analise&rdquo;, &ldquo;investigue&rdquo;. Nesse modo, o bot usa TODAS as ferramentas relevantes, faz múltiplas consultas, cruza dados entre fontes.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto inteiro saiu em 3 dias, de zero a produção, coletando dados reais, mandando digests toda manhã. Sem Jira, sem sprint planning, sem 6 meses de discovery phase. Um IDEA.md com os desejos da usuária nas palavras dela, desenvolvimento iterativo commit a commit, e validação contínua com quem vai usar de verdade.</p>
<p>Se eu tivesse começado pela arquitetura, teria passado semanas projetando um dashboard que ela não ia abrir. Começando pelo desejo, o sistema acabou sendo um chatbot no Discord que ela usa todo dia. Jobs assíncronos que não são idempotentes são bombas-relógio — o rate limit vai estourar, o scraping vai falhar, o container vai reiniciar, e se o job não sobrevive a isso sem duplicar dados, o sistema não funciona.</p>
<p>No final das contas, a medida de sucesso do projeto não é a quantidade de linhas de código, nem o número de testes passando, nem a arquitetura elegante. É a influencer abrindo o Discord segunda de manhã e tendo o que precisa pra planejar a semana.</p>
]]></content:encoded><category>rails</category><category>backend</category><category>llms</category></item><item><title>Discord como Admin Panel | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/20/discord-como-admin-panel-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/20/discord-como-admin-panel-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 03:18:24 +0000</pubDate><description>&lt;p&gt;Este post vai fazer parte de uma série; acompanhe pela tag &lt;a href="https://akitaonrails.github.io/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. Esta é a parte 6.&lt;/p&gt;
&lt;p&gt;E não deixe de assinar minha nova newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Todo projeto Rails começa igual: você cria um admin panel. Active Admin, Administrate ou um CRUD caseiro. Tabelas, formulários, botões. E a cada semana, você usa 5% das telas que construiu.&lt;/p&gt;
&lt;p&gt;Agora imagine que seu admin panel é uma conversa. Você cola um link; o sistema processa. Digita &lt;code&gt;/week&lt;/code&gt; e vê o resumo. &lt;code&gt;/approve&lt;/code&gt; e a newsletter está agendada. &lt;code&gt;/ask akita como funciona o pipeline de podcast?&lt;/code&gt; e uma IA te explica a arquitetura do seu próprio sistema.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post vai fazer parte de uma série; acompanhe pela tag <a href="/tags/the-m-akita-chronicles/">/themakitachronicles</a>. Esta é a parte 6.</p>
<p>E não deixe de assinar minha nova newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Todo projeto Rails começa igual: você cria um admin panel. Active Admin, Administrate ou um CRUD caseiro. Tabelas, formulários, botões. E a cada semana, você usa 5% das telas que construiu.</p>
<p>Agora imagine que seu admin panel é uma conversa. Você cola um link; o sistema processa. Digita <code>/week</code> e vê o resumo. <code>/approve</code> e a newsletter está agendada. <code>/ask akita como funciona o pipeline de podcast?</code> e uma IA te explica a arquitetura do seu próprio sistema.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-40-53.jpg" alt="/ask akita"  loading="lazy" /></p>
<p>Esse é o Discord como interface operacional. Não é gambiarra — é <strong>a melhor interface CRUD que existe pra um time de uma pessoa</strong>.</p>
<h2>Por Que Discord e Não Um Web Admin?<span class="hx:absolute hx:-mt-20" id="por-que-discord-e-não-um-web-admin"></span>
    <a href="#por-que-discord-e-n%c3%a3o-um-web-admin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos ser honestos sobre o que um admin panel exige:</p>
<ol>
<li><strong>Autenticação</strong>: login, sessão, CSRF, password reset</li>
<li><strong>Autorização</strong>: roles, permissions, who can do what</li>
<li><strong>UI</strong>: tabelas, paginação, formulários, validação client-side</li>
<li><strong>Deployment</strong>: outra rota, outro controller, outro set de testes</li>
<li><strong>Mobile</strong>: responsive? App? PWA?</li>
</ol>
<p>E o que o Discord já te dá de graça:</p>
<ol>
<li><strong>Auth</strong>: Discord cuida. Você verifica o <code>author_id</code></li>
<li><strong>Autorização</strong>: uma lista de admin IDs e pronto</li>
<li><strong>UI</strong>: embeds com cores, campos, thumbnails, reações como status</li>
<li><strong>Deployment</strong>: zero. Usa o que já está deployado</li>
<li><strong>Mobile</strong>: app nativo no iOS e Android, push notifications</li>
</ol>
<p>O trade-off? Não serve pra tudo. Um CRUD complexo com formulários de 20 campos? Melhor um web admin. Mas pra operações de comando/resposta — que são 90% do que você faz no dia a dia — Discord é imbatível.</p>
<h2>discordrb: A Gem Que Funciona<span class="hx:absolute hx:-mt-20" id="discordrb-a-gem-que-funciona"></span>
    <a href="#discordrb-a-gem-que-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A gem <a href="https://github.com/shardlab/discordrb"target="_blank" rel="noopener"><code>discordrb</code></a> (fork do shardlab, versão 3.7.2) é a mais madura pra Ruby. Não é perfeita — tem idiossincrasias. Mas funciona de verdade em produção.</p>
<p>O setup mínimo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span> <span class="o">=</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">Bot</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="ss">token</span><span class="p">:</span> <span class="n">token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">intents</span><span class="p">:</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">INTENTS</span><span class="o">[</span><span class="ss">:server_messages</span><span class="o">]</span> <span class="o">|</span>
</span></span><span class="line"><span class="cl">           <span class="no">Discordrb</span><span class="o">::</span><span class="no">INTENTS</span><span class="o">[</span><span class="ss">:direct_messages</span><span class="o">]</span> <span class="o">|</span>
</span></span><span class="line"><span class="cl">           <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">15</span><span class="p">)</span>  <span class="c1"># message_content intent</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">message</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Toda mensagem passa por aqui</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">run</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Aquele <code>(1 &lt;&lt; 15)</code> é o <em>Message Content Intent</em>. Sem ele, seu bot recebe eventos mas o <code>event.content</code> vem vazio. Precisa habilitar no Discord Developer Portal &gt; Bot &gt; Privileged Gateway Intents. É a primeira armadilha que pega todo mundo.</p>
<p><a href="https://discord.com/developers/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-58-46.jpg" alt="discord dashboard"  loading="lazy" /></a></p>
<p>Não vou detalhar como configurar um bot no Discord, tem <a href="https://www.ionos.com/digitalguide/server/know-how/creating-discord-bot/"target="_blank" rel="noopener">vários tutoriais</a> pra isso já, você acha fácil online. Vou focar só no meu caso de uso como exemplo.</p>
<h2>Padrão 1: Parser + Dispatcher<span class="hx:absolute hx:-mt-20" id="padrão-1-parser--dispatcher"></span>
    <a href="#padr%c3%a3o-1-parser--dispatcher" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Misturar parsing de comandos com lógica de negócio é receita pra espaguete. Separa em três camadas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Discord Event → Parser → Dispatcher → Job/Service</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O runner (que conversa com a API do Discord) é burro de propósito:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">message</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">next</span> <span class="k">if</span> <span class="n">event</span><span class="o">.</span><span class="n">author</span><span class="o">.</span><span class="n">bot_account?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">parsed</span> <span class="o">=</span> <span class="no">SlashCommandParser</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">event</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="n">urls</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">parsed</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Command</span>
</span></span><span class="line"><span class="cl">    <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">parsed</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{2705}&#34;</span><span class="p">)</span>  <span class="c1"># checkmark</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Submission</span>
</span></span><span class="line"><span class="cl">    <span class="no">ProcessMessageJob</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="ss">url</span><span class="p">:</span> <span class="n">parsed</span><span class="o">.</span><span class="n">url</span><span class="p">,</span> <span class="ss">tags</span><span class="p">:</span> <span class="n">parsed</span><span class="o">.</span><span class="n">tags</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{1F916}&#34;</span><span class="p">)</span>  <span class="c1"># robot</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">react</span><span class="p">(</span><span class="s2">&#34;\u{274C}&#34;</span><span class="p">)</span>  <span class="c1"># red X</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Três resultados visuais instantâneos: checkmark (comando aceito), robô (URL sendo processada), X vermelho (erro). O usuário sabe o que aconteceu sem esperar nenhuma resposta textual.</p>
<p>O parser retorna objetos tipados, não strings. O dispatcher faz um <code>case</code> gigante mas cada handler é um método isolado. Cada handler chama um job ou service. <strong>Nada de lógica de negócio no runner.</strong></p>
<h3>Evolução: Discord Application Commands<span class="hx:absolute hx:-mt-20" id="evolução-discord-application-commands"></span>
    <a href="#evolu%c3%a7%c3%a3o-discord-application-commands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A versão inicial parseava texto puro — o usuário digitava <code>/week</code> e o parser interpretava. Funciona, mas tem limitações: sem autocomplete, sem validação de argumentos, sem documentação inline.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-56-15.jpg" alt="help"  loading="lazy" /></p>
<p>O Discord tem um sistema nativo de <strong>Application Commands</strong> — slash commands registrados na API que aparecem com autocomplete, descrição, e tipagem de parâmetros. Quando o usuário digita <code>/</code>, o Discord mostra todos os comandos disponíveis com descrição.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">,</span> <span class="s2">&#34;List this week&#39;s stories&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:approve</span><span class="p">,</span> <span class="s2">&#34;Approve newsletter for sending&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:ask</span><span class="p">,</span> <span class="s2">&#34;Ask AI a question&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">guild_id</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="s2">&#34;question&#34;</span><span class="p">,</span> <span class="s2">&#34;Your question&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="s2">&#34;personality&#34;</span><span class="p">,</span> <span class="s2">&#34;AI personality&#34;</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&#34;Akita&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;akita&#34;</span><span class="p">,</span> <span class="s2">&#34;Marvin&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;marvin&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O handler usa <code>bot.application_command</code> ao invés de parsear texto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch_week</span><span class="p">(</span><span class="ss">channel_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">respond</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Listing stories...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os dois sistemas coexistem: Application Commands pra quem prefere autocomplete, texto puro pra quem prefere digitar rápido. O dispatcher é o mesmo — a diferença é só como o comando chega.</p>
<h2>Padrão 2: Reações Como Status<span class="hx:absolute hx:-mt-20" id="padrão-2-reações-como-status"></span>
    <a href="#padr%c3%a3o-2-rea%c3%a7%c3%b5es-como-status" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-43-55.jpg" alt="reactions"  loading="lazy" /></p>
<p>A feature mais subestimada do Discord pra bots é reações. Elas são feedback visual instantâneo que não polui o canal com mensagens:</p>
<ul>
<li><strong>Checkmark</strong> = comando recebido e enfileirado</li>
<li><strong>Robô</strong> = processamento assíncrono iniciado</li>
<li><strong>X vermelho</strong> = erro (detalhes no embed que segue)</li>
</ul>
<p>O usuário cola um link. Vê o robô aparecer. 30 segundos depois, um embed rico aparece com título, resumo, imagem e comentário da IA. Se algo falhar, aparece um embed vermelho com a mensagem de erro.</p>
<p>Isso é melhor que qualquer spinner de loading numa web UI.</p>
<h2>Padrão 3: Embeds Ricos Como Dashboard<span class="hx:absolute hx:-mt-20" id="padrão-3-embeds-ricos-como-dashboard"></span>
    <a href="#padr%c3%a3o-3-embeds-ricos-como-dashboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-44-36.jpg" alt="new story"  loading="lazy" /></p>
<p>Discord embeds são surpreendentemente poderosos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">embed</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">title</span><span class="p">:</span> <span class="s2">&#34;Story Preview&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="n">source_url</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">color</span><span class="p">:</span> <span class="mh">0xFF4500</span><span class="p">,</span>           <span class="c1"># vermelho-alaranjado pra high priority</span>
</span></span><span class="line"><span class="cl">  <span class="ss">description</span><span class="p">:</span> <span class="n">summary</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">thumbnail</span><span class="p">:</span> <span class="p">{</span> <span class="ss">url</span><span class="p">:</span> <span class="n">image</span> <span class="p">},</span>  <span class="c1"># imagem do artigo</span>
</span></span><span class="line"><span class="cl">  <span class="ss">fields</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;M.Akita diz&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="n">akita_comment</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;M.Arvin diz&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="n">marvin_comment</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;File&#34;</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="s2">&#34;`stories/2026-02-16-titulo.md`&#34;</span><span class="p">,</span> <span class="ss">inline</span><span class="p">:</span> <span class="kp">false</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">footer</span><span class="p">:</span> <span class="p">{</span> <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;fire high | tech_news | verified&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">Discordrb</span><span class="o">::</span><span class="no">API</span><span class="o">::</span><span class="no">Channel</span><span class="o">.</span><span class="n">create_message</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">channel_id</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="kp">false</span><span class="p">,</span> <span class="o">[</span><span class="n">embed</span><span class="o">]</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cores indicam prioridade (vermelho = high, âmbar = medium, verde = low). O footer mostra score, seção e status de fact-check tudo numa linha. Você bate o olho e sabe o estado da coisa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-45-11.jpg" alt="week list"  loading="lazy" /></p>
<p>Para relatórios, múltiplos embeds lado a lado (Discord aceita até 10 por mensagem) funcionam como um mini-dashboard. O comando <code>/week</code> lista todas as histórias com ações inline — <code>/load @a3f2b1</code> pra ver detalhes, <code>/delete @a3f2b1</code> pra remover. O hash é estável (baseado no path do arquivo), então não muda quando você deleta outra história.</p>
<h2>Padrão 4: Context Per Channel<span class="hx:absolute hx:-mt-20" id="padrão-4-context-per-channel"></span>
    <a href="#padr%c3%a3o-4-context-per-channel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-46-29.jpg" alt="load"  loading="lazy" /></p>
<p>Quando o usuário digita <code>/load #3</code>, o #3 se refere à listagem mais recente. Mas e se dois canais estão sendo usados simultaneamente?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ChannelContext</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="no">EXPIRY_DURATION</span> <span class="o">=</span> <span class="mi">2</span><span class="o">.</span><span class="n">hours</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">for_channel</span><span class="p">(</span><span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">find_or_create_by!</span><span class="p">(</span><span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">set_week_stories!</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">update!</span><span class="p">(</span><span class="ss">week_stories_json</span><span class="p">:</span> <span class="nb">hash</span><span class="o">.</span><span class="n">to_json</span><span class="p">,</span> <span class="ss">expires_at</span><span class="p">:</span> <span class="no">EXPIRY_DURATION</span><span class="o">.</span><span class="n">from_now</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">resolve_story_reference</span><span class="p">(</span><span class="n">ref</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">ref</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A@[a-f0-9]+\z/i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Hash reference (stable across re-listings)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">hash</span> <span class="o">=</span> <span class="n">ref</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="s2">&#34;@&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">downcase</span>
</span></span><span class="line"><span class="cl">      <span class="n">week_stories</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">find</span> <span class="p">{</span> <span class="o">|</span><span class="n">path</span><span class="o">|</span> <span class="n">md5_prefix</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">==</span> <span class="nb">hash</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">elsif</span> <span class="n">ref</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A#?\d+\z/</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">week_stories</span><span class="o">[</span><span class="n">ref</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="s2">&#34;#&#34;</span><span class="p">)</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada canal tem seu próprio contexto com expiração de 2 horas. O <code>/week</code> armazena o mapeamento número→arquivo. Depois, <code>/load #3</code> consulta esse contexto. Se expirou, o bot diz &ldquo;run <code>/week</code> first&rdquo;.</p>
<p>E o detalhe mais útil: referências por hash (<code>@a3f2b1</code>) ao invés de número. Quando você deleta a história #2, a #3 vira #2 — mas o hash <code>@a3f2b1</code> continua apontando pro mesmo arquivo. Isso evita o clássico &ldquo;deletei a errada porque a lista mudou&rdquo;.</p>
<h2>Padrão 5: Admin Check Simples<span class="hx:absolute hx:-mt-20" id="padrão-5-admin-check-simples"></span>
    <a href="#padr%c3%a3o-5-admin-check-simples" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nada de RBAC complexo. Uma lista de IDs:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ADMIN_COMMANDS</span> <span class="o">=</span> <span class="o">%</span><span class="n">i</span><span class="o">[</span><span class="n">delete</span> <span class="n">update_score</span> <span class="n">approve</span> <span class="n">newsletter</span> <span class="n">publish_blog</span><span class="o">].</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">dispatch</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:,</span> <span class="ss">author_id</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="no">ADMIN_COMMANDS</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">command</span><span class="o">.</span><span class="n">type</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">admin?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_error</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="s2">&#34;Permission denied. Admin privileges required.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">admin?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_ids</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;DISCORD_ADMIN_USER_IDS&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">admin_ids</span><span class="o">.</span><span class="n">empty?</span>  <span class="c1"># dev: sem IDs = tudo permitido</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_ids</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">author_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em dev, sem <code>DISCORD_ADMIN_USER_IDS</code> configurado, tudo é permitido. Em produção, é uma lista de Discord user IDs. Sem banco, sem roles table, sem middleware. Dez linhas resolvem autorização.</p>
<h2>Padrão 6: Operações Assíncronas com Feedback<span class="hx:absolute hx:-mt-20" id="padrão-6-operações-assíncronas-com-feedback"></span>
    <a href="#padr%c3%a3o-6-opera%c3%a7%c3%b5es-ass%c3%adncronas-com-feedback" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Operações lentas (gerar imagem, processar artigo, chamar IA) vão pra background jobs. O feedback visual é em três tempos:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">handle_ask</span><span class="p">(</span><span class="n">personality</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">attachments</span><span class="p">,</span> <span class="n">channel_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 1. Feedback imediato: &#34;estou pensando&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">thinking_id</span> <span class="o">=</span> <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Thinking...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. Job faz o trabalho pesado em background</span>
</span></span><span class="line"><span class="cl">  <span class="no">AskJob</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">question</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">channel_id</span><span class="p">:</span> <span class="n">channel_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">thinking_message_id</span><span class="p">:</span> <span class="n">thinking_id</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No AskJob:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="ss">channel_id</span><span class="p">:,</span> <span class="ss">thinking_message_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">result</span> <span class="o">=</span> <span class="no">AiChat</span><span class="o">.</span><span class="n">ask_multimodal</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="o">[</span><span class="no">GenerateImageTool</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Remove &#34;Thinking...&#34; e mostra resultado</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">delete_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="n">thinking_message_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;**Marvin:** </span><span class="si">#{</span><span class="n">result</span><span class="o">[</span><span class="ss">:text</span><span class="o">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-47-26.jpg" alt="thinking"  loading="lazy" /></p>
<p>O &ldquo;Thinking&hellip;&rdquo; aparece instantaneamente, o job roda em background, e quando termina, a mensagem temporária some e o resultado real aparece. Se o job falhar, o &ldquo;Thinking&hellip;&rdquo; some e um embed de erro aparece.</p>
<p>Sem isso, o usuário manda uma pergunta e fica olhando pra tela sem saber se o bot ouviu. UX terrível.</p>
<h2>Padrão 7: RubyLLM + Tool Calling No Discord<span class="hx:absolute hx:-mt-20" id="padrão-7-rubyllm--tool-calling-no-discord"></span>
    <a href="#padr%c3%a3o-7-rubyllm--tool-calling-no-discord" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A parte mais poderosa: dar ferramentas pro LLM. O <code>/ask</code> tem três tools disponíveis: <code>GenerateImageTool</code> (gera imagens via Gemini), <code>WebSearchTool</code> (busca na web via Brave Search), e <code>WebFetchTool</code> (busca e resume páginas web). O LLM decide sozinho qual usar baseado na pergunta. Quando o usuário pede &ldquo;gera uma imagem de um gato programando&rdquo;, o LLM escolhe o tool certo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">GenerateImageTool</span> <span class="o">&lt;</span> <span class="no">RubyLLM</span><span class="o">::</span><span class="no">Tool</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="s2">&#34;Generate an image using Gemini. Use when the user asks to create or draw.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:prompt</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;Image description&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">param</span> <span class="ss">:aspect_ratio</span><span class="p">,</span> <span class="ss">desc</span><span class="p">:</span> <span class="s2">&#34;1:1 or 16:9&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="ss">prompt</span><span class="p">:,</span> <span class="ss">aspect_ratio</span><span class="p">:</span> <span class="s2">&#34;1:1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">image_data</span> <span class="o">=</span> <span class="no">ImageGenerator</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="ss">prompt</span><span class="p">:</span> <span class="n">prompt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Failed to generate image&#34;</span> <span class="k">unless</span> <span class="n">image_data</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">save_temp_file</span><span class="p">(</span><span class="n">image_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Thread</span><span class="o">.</span><span class="n">current</span><span class="o">[</span><span class="ss">:generated_images</span><span class="o">]</span> <span class="o">&lt;&lt;</span> <span class="n">path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Image generated successfully. Tell the user it&#39;s ready.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>Thread.current[:generated_images]</code> é o truque pra passar dados de volta do tool pro job. O LLM chama o tool durante <code>chat.ask</code>, o tool salva o arquivo e registra o path. Depois que <code>chat.ask</code> retorna, o job pega os paths e manda pro Discord como file uploads.</p>
<p>Por que thread-local e não retorno direto? Porque o LLM pode chamar múltiplos tools numa única resposta, e o retorno do tool volta pro LLM (não pro seu código) — é ele que decide o que responder ao usuário. O thread-local é o side channel pra seu código saber o que aconteceu.</p>
<h2>Padrão 8: Notificações de Status Em Produção<span class="hx:absolute hx:-mt-20" id="padrão-8-notificações-de-status-em-produção"></span>
    <a href="#padr%c3%a3o-8-notifica%c3%a7%c3%b5es-de-status-em-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo job de geração de conteúdo notifica um canal dedicado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DiscordStatus</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_start</span><span class="p">(</span><span class="n">label</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;Starting </span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2">...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_done</span><span class="p">(</span><span class="n">label</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2"> complete.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">notify_error</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_to_status_channel</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2"> FAILED: </span><span class="si">#{</span><span class="n">error</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O canal de status é separado do canal de operação. Quando os 8 jobs de geração de conteúdo rodam no domingo às 17h, o canal de status vira um log em tempo real:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Starting Anime Ranking...
Starting Hacker News...
Starting YouTube Digest...
Anime Ranking complete.
YouTube Digest complete.
Hacker News complete.
Starting Market Recap...  (wave 2, after world events)
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De manhã, se algo deu errado, você abre o Discord no celular e sabe exatamente o quê — sem SSH no servidor, sem grep em logs.</p>
<p>E quando o bot reinicia após um deploy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">ready</span> <span class="k">do</span> <span class="o">|</span><span class="n">_event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">status_channel</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;DISCORD_STATUS_CHANNEL_ID&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;Bot reconnected and ready.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-48-20.jpg" alt="log"  loading="lazy" /></p>
<p>Simples, mas você sabe que o deploy funcionou sem checar nada.</p>
<h2>Padrão 9: Cross-App Communication<span class="hx:absolute hx:-mt-20" id="padrão-9-cross-app-communication"></span>
    <a href="#padr%c3%a3o-9-cross-app-communication" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O sistema tem dois apps Rails que precisam se falar. A newsletter precisa notificar o Discord quando termina de montar uma edição. Mas a newsletter não tem conexão Discord — quem tem é o marvin-bot.</p>
<p>Solução: HTTP relay.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Na newsletter — manda pro marvin-bot</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DiscordRelayClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">send_embed</span><span class="p">(</span><span class="ss">title</span><span class="p">:,</span> <span class="ss">description</span><span class="p">:,</span> <span class="ss">color</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span><span class="p">(</span><span class="s2">&#34;/api/discord_notify&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">type</span><span class="p">:</span> <span class="s2">&#34;embed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">payload</span><span class="p">:</span> <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span> <span class="ss">description</span><span class="p">:</span> <span class="n">description</span><span class="p">,</span> <span class="ss">color</span><span class="p">:</span> <span class="n">color</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No marvin-bot — recebe e repassa pro Discord</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">DiscordNotifyController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">params</span><span class="o">[</span><span class="ss">:type</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;embed&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_embed</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="n">params</span><span class="o">[</span><span class="ss">:payload</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s2">&#34;message&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">status_channel</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:payload</span><span class="o">][</span><span class="ss">:content</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A newsletter fala HTTP. O marvin-bot traduz pra Discord. Um <code>ADMIN_TOKEN</code> compartilhado autentica. Todos os métodos resgatam erros e retornam nil — se o Discord estiver fora, a newsletter não trava. Fire and forget.</p>
<p>Mas tem um segundo fluxo cross-app mais interessante: o trigger de podcast. Quando a newsletter é aprovada, ela precisa avisar o marvin-bot pra gerar o podcast:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Na newsletter — dispara geração de podcast no marvin-bot</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MarvinBotClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">trigger_podcast</span><span class="p">(</span><span class="ss">date</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span><span class="p">(</span><span class="s2">&#34;/api/trigger/podcast&#34;</span><span class="p">,</span> <span class="ss">date</span><span class="p">:</span> <span class="n">date</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso inicia um pipeline inteiro no marvin-bot: script LLM de duas passadas → TTS por turno na GPU → assembly com loudnorm → upload S3. E o <code>PublishAndSendJob</code> da newsletter fica esperando (via <code>retry_on PodcastNotReady</code>) até o podcast ficar pronto. Dois apps, duas linguagens de backend (Ruby e Python), conectados por HTTP fire-and-forget com polling de resultado. Não é elegante — mas funciona toda semana sem intervenção.</p>
<h2>Padrão 10: Cost Tracking Integrado<span class="hx:absolute hx:-mt-20" id="padrão-10-cost-tracking-integrado"></span>
    <a href="#padr%c3%a3o-10-cost-tracking-integrado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cada chamada de IA é rastreada com tokens e custo estimado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">create_chat</span><span class="p">(</span><span class="nb">caller</span><span class="p">:,</span> <span class="ss">model</span><span class="p">:,</span> <span class="ss">provider</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="ss">provider</span><span class="p">:</span> <span class="n">provider</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span><span class="o">.</span><span class="n">on_end_message</span> <span class="k">do</span> <span class="o">|</span><span class="n">message</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="no">ApiUsage</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">caller_name</span><span class="p">:</span> <span class="nb">caller</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">input_tokens</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">input_tokens</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">output_tokens</span><span class="p">:</span> <span class="n">message</span><span class="o">.</span><span class="n">output_tokens</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">cost_usd</span><span class="p">:</span> <span class="n">estimate_cost</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E no Discord: <code>/cost</code> mostra o gasto por mês, ano, e por provider. Sem dashboard separado, sem Grafana, sem nada. Um comando e a informação que importa.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-49-05.jpg" alt="cost"  loading="lazy" /></p>
<h2>Padrão 11: Nunca Falhe Silenciosamente<span class="hx:absolute hx:-mt-20" id="padrão-11-nunca-falhe-silenciosamente"></span>
    <a href="#padr%c3%a3o-11-nunca-falhe-silenciosamente" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse é o padrão que eu devia ter implementado desde o começo, mas só aprendi da pior maneira — em produção.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-50-46.jpg" alt="daily"  loading="lazy" /></p>
<p>O projeto tem ~10 daily jobs que buscam dados de fontes externas (Hacker News, GitHub Trending, Morningstar, etc.) e postam embeds no Discord. Cada client segue um padrão &ldquo;defensivo&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># O padrão que PARECE correto:</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DailyDevClient</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="no">API_URL</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="p">{</span> <span class="s2">&#34;Authorization&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;Bearer </span><span class="si">#{</span><span class="n">token</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">response</span><span class="o">.</span><span class="n">success?</span>
</span></span><span class="line"><span class="cl">    <span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;DailyDevClient failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># E no job:</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DailyDailyDevJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="n">posts</span> <span class="o">=</span> <span class="no">DailyDevClient</span><span class="o">.</span><span class="n">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">if</span> <span class="n">posts</span><span class="o">.</span><span class="n">empty?</span>  <span class="c1"># ← O PROBLEMA</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... posta embed no Discord</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse código nunca crasheia. Nunca levanta exceção. Nunca enche o log de erros. É o código mais &ldquo;robusto&rdquo; possível.</p>
<p>É também completamente <strong>inútil pra diagnóstico</strong>. Quando deployei e rodei os daily jobs manualmente, só um dos dez funcionou. Mas não havia <strong>nenhum</strong> erro nos logs. Nenhum. O <code>return [] unless response.success?</code> engolia o HTTP 401 sem logar. O <code>return if posts.empty?</code> descartava o resultado sem avisar ninguém. Tudo verde, tudo silencioso, zero dados.</p>
<p>A correção tem duas partes:</p>
<p><strong>Parte 1: Clients logam o código HTTP:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">unless</span> <span class="n">response</span><span class="o">.</span><span class="n">success?</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;DailyDevClient: API returned HTTP </span><span class="si">#{</span><span class="n">response</span><span class="o">.</span><span class="n">code</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Parece óbvio, mas é fácil esquecer quando você escreve <code>return []</code> como reflexo defensivo. O <code>response.code</code> é a informação que diferencia &ldquo;token expirado&rdquo; (401) de &ldquo;rate limited&rdquo; (429) de &ldquo;API fora do ar&rdquo; (503). Sem ele, tudo parece igual: array vazio.</p>
<p><strong>Parte 2: Jobs avisam no Discord quando não têm dados:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DailyDiscordEmbed</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">report_empty</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">title</span><span class="si">}</span><span class="s2"> returned no results&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">channel_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">discord</span><span class="o">.</span><span class="n">channel_ids</span><span class="o">.</span><span class="n">first</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">channel_id</span><span class="o">.</span><span class="n">present?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">embed</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">title</span><span class="p">:</span> <span class="s2">&#34;⚠️ </span><span class="si">#{</span><span class="n">title</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">description</span><span class="p">:</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> returned no results. Check logs.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">color</span><span class="p">:</span> <span class="mh">0xFFA500</span><span class="p">,</span>  <span class="c1"># laranja = warning, não vermelho = error</span>
</span></span><span class="line"><span class="cl">      <span class="ss">timestamp</span><span class="p">:</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="o">.</span><span class="n">iso8601</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordNotifier</span><span class="o">.</span><span class="n">send_embed</span><span class="p">(</span><span class="n">channel_id</span><span class="p">,</span> <span class="n">embed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> empty report failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora quando um daily job não traz dados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">  <span class="n">posts</span> <span class="o">=</span> <span class="no">DailyDevClient</span><span class="o">.</span><span class="n">fetch_popular</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">posts</span><span class="o">.</span><span class="n">empty?</span>
</span></span><span class="line"><span class="cl">    <span class="n">report_empty</span><span class="p">(</span><span class="s2">&#34;daily.dev Popular&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-50-02.jpg" alt="no results"  loading="lazy" /></p>
<p>Um embed laranja aparece no Discord. De manhã, abro o canal e vejo: &ldquo;⚠️ daily.dev Popular returned no results&rdquo;. Vou nos logs e descubro: &ldquo;DailyDevClient: API returned HTTP 401&rdquo;. Token expirado. Corrigido em 2 minutos.</p>
<p>Sem esse padrão, eu teria descoberto semanas depois — quando alguém perguntasse &ldquo;por que nunca tem posts do daily.dev?&rdquo;.</p>
<p>A lição: <strong><code>rescue =&gt; []</code> não é error handling. É error hiding.</strong> Cada ponto de falha precisa de dois sinais: um log com o detalhe técnico (HTTP code, mensagem de exceção) e uma notificação visual no canal onde você já está olhando (Discord). Se o bot É sua interface operacional, falhas do bot precisam aparecer NA interface.</p>
<h2>Padrão 12: Conversas Com Memória (<code>/ask</code> Multi-Turn)<span class="hx:absolute hx:-mt-20" id="padrão-12-conversas-com-memória-ask-multi-turn"></span>
    <a href="#padr%c3%a3o-12-conversas-com-mem%c3%b3ria-ask-multi-turn" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A primeira versão do <code>/ask</code> criava um <code>RubyLLM::Chat</code> novo a cada invocação. Funcionava, mas era como conversar com alguém que tem amnésia:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/ask o que é Ruby?
→ Ruby é uma linguagem de programação criada por Matz em 1995...

/ask compare com Python
→ Comparar o quê com Python? Poderia ser mais específico?</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O ruby_llm já mantém histórico internamente — cada <code>chat.ask()</code> appende à lista de mensagens e manda o contexto completo pro modelo. O que faltava era persistir o objeto <code>Chat</code> entre invocações.</p>
<p>A solução é um store in-memory com três safeguards:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">AskSessionStore</span>
</span></span><span class="line"><span class="cl">  <span class="no">MAX_SESSIONS</span> <span class="o">=</span> <span class="mi">100</span>       <span class="c1"># total across all users</span>
</span></span><span class="line"><span class="cl">  <span class="no">SESSION_TTL</span> <span class="o">=</span> <span class="mi">2</span><span class="o">.</span><span class="n">hours</span>    <span class="c1"># inactivity timeout</span>
</span></span><span class="line"><span class="cl">  <span class="no">MAX_MESSAGES</span> <span class="o">=</span> <span class="mi">50</span>        <span class="c1"># ~25 exchanges, then auto-reset</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="no">Session</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:chat</span><span class="p">,</span> <span class="ss">:last_used</span><span class="p">,</span> <span class="ss">keyword_init</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">get_or_create</span><span class="p">(</span><span class="n">author_id</span><span class="p">,</span> <span class="n">personality</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:,</span> <span class="ss">tools</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="n">synchronize</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">cleanup_expired</span>
</span></span><span class="line"><span class="cl">      <span class="n">key</span> <span class="o">=</span> <span class="o">[</span><span class="n">author_id</span><span class="p">,</span> <span class="n">personality</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="n">session</span> <span class="o">=</span> <span class="vi">@sessions</span><span class="o">[</span><span class="n">key</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">last_used</span> <span class="o">=</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">session</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">messages</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;</span> <span class="no">MAX_MESSAGES</span>
</span></span><span class="line"><span class="cl">          <span class="vi">@sessions</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">return</span> <span class="n">create_session</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:</span> <span class="n">system_prompt</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="n">tools</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">chat</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="n">evict_oldest</span> <span class="k">if</span> <span class="vi">@sessions</span><span class="o">.</span><span class="n">size</span> <span class="o">&gt;=</span> <span class="no">MAX_SESSIONS</span>
</span></span><span class="line"><span class="cl">        <span class="n">create_session</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">system_prompt</span><span class="p">:</span> <span class="n">system_prompt</span><span class="p">,</span> <span class="ss">tools</span><span class="p">:</span> <span class="n">tools</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-54-32.jpg" alt="ask memory"  loading="lazy" /></p>
<p>A chave vai ser <code>(author_id, personality)</code> — cada usuário tem sessões separadas pro Marvin e pro Akita. Volátil de propósito: reiniciou o servidor, perdeu o histórico. Sem banco, sem serialização, sem complexidade. É memória de trabalho, não persistência.</p>
<p>O <code>/ask clear</code> reseta a sessão. Sem isso, não tem como &ldquo;começar de novo&rdquo; quando a conversa fica confusa.</p>
<p>Agora:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/ask o que é Ruby?
→ Ruby é uma linguagem de programação criada por Matz em 1995...

/ask compare com Python
→ Ruby e Python têm filosofias parecidas — ambas priorizam felicidade do
  programador — mas Ruby abraça blocos e metaprogramação enquanto Python
  prefere explicitness...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O LLM sabe que &ldquo;compare&rdquo; se refere a Ruby porque o histórico da sessão inclui a pergunta anterior. É a diferença entre um chatbot e um assistente.</p>
<h2>Padrão 13: Registrando Application Commands (Autocomplete Nativo)<span class="hx:absolute hx:-mt-20" id="padrão-13-registrando-application-commands-autocomplete-nativo"></span>
    <a href="#padr%c3%a3o-13-registrando-application-commands-autocomplete-nativo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A seção sobre Application Commands acima mostrou o conceito simplificado. Na prática, registrar 18 comandos com subcommands, parâmetros tipados e choices exige uma estrutura dedicada. Veja como o projeto resolve isso.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-43-18.jpg" alt="auto complete"  loading="lazy" /></p>
<h3>O Problema Com Texto Puro<span class="hx:absolute hx:-mt-20" id="o-problema-com-texto-puro"></span>
    <a href="#o-problema-com-texto-puro" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando o bot só parseia texto, o usuário precisa decorar a sintaxe:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/update score #1 #2 #3 high       ← funciona
/update score #1 #2 #3 alta       ← falha silenciosamente
/update scoree #1 high             ← ignorado</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem autocomplete, sem validação de parâmetros, sem documentação inline. O usuário digita <code>/</code> e não sabe o que está disponível.</p>
<h3>O Módulo <code>DiscordCommands</code><span class="hx:absolute hx:-mt-20" id="o-módulo-discordcommands"></span>
    <a href="#o-m%c3%b3dulo-discordcommands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Toda a definição de comandos vive num módulo separado com dois métodos públicos: <code>register!</code> (cadastra os comandos na API do Discord) e <code>setup_handlers!</code> (conecta os handlers ao bot):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DiscordCommands</span>
</span></span><span class="line"><span class="cl">  <span class="no">SCORE_CHOICES</span>  <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;High&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;high&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span> <span class="s2">&#34;Low&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;low&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">  <span class="no">SECTION_CHOICES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Tech&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;tech&#34;</span><span class="p">,</span> <span class="s2">&#34;Global&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;global&#34;</span><span class="p">,</span> <span class="s2">&#34;Financial&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;financial&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Q&amp;A&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;qa&#34;</span><span class="p">,</span> <span class="s2">&#34;Entertainment&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;entertainment&#34;</span><span class="p">,</span> <span class="s2">&#34;Open Source&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;opensource&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Product&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;product&#34;</span><span class="p">,</span> <span class="s2">&#34;Book&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;book&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">  <span class="no">PERSONALITY_CHOICES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;Marvin (default)&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;marvin&#34;</span><span class="p">,</span> <span class="s2">&#34;Akita&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;akita&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">server_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">discord</span><span class="o">.</span><span class="n">server_id</span>
</span></span><span class="line"><span class="cl">    <span class="n">sid</span> <span class="o">=</span> <span class="n">server_id</span><span class="o">.</span><span class="n">present?</span> <span class="p">?</span> <span class="n">server_id</span><span class="o">.</span><span class="n">to_i</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_week</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_add</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_update</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">register_ask</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais 14 comandos</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">setup_handlers!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_week_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_add_handler</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_update_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">setup_ask_handlers</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais 14 handlers</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Guild vs Global<span class="hx:absolute hx:-mt-20" id="guild-vs-global"></span>
    <a href="#guild-vs-global" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>server_id</code> (ou <code>sid</code>) controla o escopo do registro. Com <code>server_id</code>, o comando aparece instantaneamente no servidor especificado. Sem, o comando é global — disponível em todos os servidores, mas pode levar até uma hora pra propagar.</p>
<p>Para um bot privado de operações internas, sempre use guild-scoped. O env var <code>DISCORD_SERVER_ID</code> é configurado no deploy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">sid</span> <span class="o">=</span> <span class="n">server_id</span><span class="o">.</span><span class="n">present?</span> <span class="p">?</span> <span class="n">server_id</span><span class="o">.</span><span class="n">to_i</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:week</span><span class="p">,</span> <span class="s2">&#34;Story list and weekly report&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se <code>sid</code> for nil, o discordrb registra globalmente. Em dev, é conveniente; em produção, guild-scoped é mais rápido e permite comandos diferentes por servidor.</p>
<h3>Anatomia De Um Comando<span class="hx:absolute hx:-mt-20" id="anatomia-de-um-comando"></span>
    <a href="#anatomia-de-um-comando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O método mais simples — sem parâmetros:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_count</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:count</span><span class="p">,</span> <span class="s2">&#34;Show story count for this week&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com parâmetros tipados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_add</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:add</span><span class="p">,</span> <span class="s2">&#34;Submit a URL for the newsletter&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:url</span><span class="p">,</span> <span class="s2">&#34;The URL to submit&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:tags</span><span class="p">,</span> <span class="s2">&#34;Tags (e.g. #tech #high)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:comment</span><span class="p">,</span> <span class="s2">&#34;Optional comment about this link&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Discord mostra isso como um formulário inline: campo <code>url</code> obrigatório, <code>tags</code> e <code>comment</code> opcionais. O usuário vê a descrição de cada parâmetro enquanto preenche.</p>
<h3>Subcommands: Agrupando Ações<span class="hx:absolute hx:-mt-20" id="subcommands-agrupando-ações"></span>
    <a href="#subcommands-agrupando-a%c3%a7%c3%b5es" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Comandos complexos usam subcommands. <code>/update</code> sozinho não faz nada — o usuário precisa escolher o que atualizar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">register_update</span><span class="p">(</span><span class="n">bot</span><span class="p">,</span> <span class="n">sid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">bot</span><span class="o">.</span><span class="n">register_application_command</span><span class="p">(</span><span class="ss">:update</span><span class="p">,</span> <span class="s2">&#34;Update story properties&#34;</span><span class="p">,</span> <span class="ss">server_id</span><span class="p">:</span> <span class="n">sid</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:score</span><span class="p">,</span> <span class="s2">&#34;Update story priority score&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:level</span><span class="p">,</span> <span class="s2">&#34;Priority level&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SCORE_CHOICES</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:tag</span><span class="p">,</span> <span class="s2">&#34;Update story section tag&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:section</span><span class="p">,</span> <span class="s2">&#34;Section tag&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SECTION_CHOICES</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:image</span><span class="p">,</span> <span class="s2">&#34;Update story image&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">sub</span><span class="o">.</span><span class="n">attachment</span><span class="p">(</span><span class="ss">:file</span><span class="p">,</span> <span class="s2">&#34;Image file (omit for AI generation)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... mais subcommands: title, description, comment, source</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando o usuário digita <code>/update</code>, o Discord mostra uma lista de subcommands: score, tag, title, description, comment, image, source. Ao escolher <code>score</code>, aparece o campo <code>stories</code> (texto livre) e <code>level</code> (dropdown com High/Medium/Low). Impossível errar a sintaxe.</p>
<h3>Choices: Dropdowns Nativos<span class="hx:absolute hx:-mt-20" id="choices-dropdowns-nativos"></span>
    <a href="#choices-dropdowns-nativos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O hash <code>SCORE_CHOICES</code> vira um dropdown no Discord. A chave é o label visível (&ldquo;High&rdquo;), o valor é o que chega no handler (&ldquo;high&rdquo;):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">SCORE_CHOICES</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&#34;High&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;high&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span> <span class="s2">&#34;Low&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;low&#34;</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># No registro:</span>
</span></span><span class="line"><span class="cl"><span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:level</span><span class="p">,</span> <span class="s2">&#34;Priority level&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">choices</span><span class="p">:</span> <span class="no">SCORE_CHOICES</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O usuário não precisa lembrar se é &ldquo;high&rdquo;, &ldquo;HIGH&rdquo;, &ldquo;alta&rdquo;, ou &ldquo;3&rdquo;. Seleciona no dropdown e pronto. Isso elimina uma categoria inteira de erros de input.</p>
<h3>Attachments: Upload de Arquivos<span class="hx:absolute hx:-mt-20" id="attachments-upload-de-arquivos"></span>
    <a href="#attachments-upload-de-arquivos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Discord suporta parâmetros do tipo attachment — o usuário pode arrastar um arquivo direto no comando:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">cmd</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:image</span><span class="p">,</span> <span class="s2">&#34;Update story image&#34;</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="nb">sub</span><span class="o">.</span><span class="n">string</span><span class="p">(</span><span class="ss">:stories</span><span class="p">,</span> <span class="s2">&#34;Story numbers (e.g. #1 #2 #3 or 1,2,3)&#34;</span><span class="p">,</span> <span class="ss">required</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nb">sub</span><span class="o">.</span><span class="n">attachment</span><span class="p">(</span><span class="ss">:file</span><span class="p">,</span> <span class="s2">&#34;Image file (omit for AI generation)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No handler, os attachments resolvidos vêm como URLs do CDN do Discord:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">resolve_attachments</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">event</span><span class="o">.</span><span class="n">resolved</span><span class="o">&amp;.</span><span class="n">attachments</span><span class="o">&amp;.</span><span class="n">any?</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">resolved</span><span class="o">.</span><span class="n">attachments</span><span class="o">.</span><span class="n">values</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Lifecycle: Onde Registrar, Onde Conectar Handlers<span class="hx:absolute hx:-mt-20" id="lifecycle-onde-registrar-onde-conectar-handlers"></span>
    <a href="#lifecycle-onde-registrar-onde-conectar-handlers" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse é o ponto que mais confunde. Os <strong>handlers</strong> precisam ser registrados <strong>antes</strong> do <code>bot.run</code>. O <strong>registro na API do Discord</strong> pode acontecer no <code>bot.ready</code> (que dispara após a conexão WebSocket):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MarvinBotRunner</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">run!</span>
</span></span><span class="line"><span class="cl">    <span class="n">bot</span> <span class="o">=</span> <span class="no">Discordrb</span><span class="o">::</span><span class="no">Bot</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">token</span><span class="p">:</span> <span class="n">token</span><span class="p">,</span> <span class="ss">intents</span><span class="p">:</span> <span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. Handlers ANTES do bot.run (senão o bot não sabe como responder)</span>
</span></span><span class="line"><span class="cl">    <span class="no">DiscordCommands</span><span class="o">.</span><span class="n">setup_handlers!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">bot</span><span class="o">.</span><span class="n">ready</span> <span class="k">do</span> <span class="o">|</span><span class="n">_event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># 2. Registro na API DEPOIS da conexão (idempotente, safe on reconnect)</span>
</span></span><span class="line"><span class="cl">      <span class="no">DiscordCommands</span><span class="o">.</span><span class="n">register!</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">bot</span><span class="o">.</span><span class="n">run</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por que no <code>bot.ready</code>? Porque o bot pode reconectar (Discord fecha WebSockets periodicamente). O <code>register!</code> é idempotente — registrar o mesmo comando duas vezes não duplica, só atualiza. Colocar no <code>ready</code> garante que os comandos existem mesmo após um reconnect.</p>
<h3>Bridge: Application Command → CommandDispatcher<span class="hx:absolute hx:-mt-20" id="bridge-application-command--commanddispatcher"></span>
    <a href="#bridge-application-command--commanddispatcher" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O ponto mais elegante da arquitetura: o handler de Application Command não tem lógica de negócio. Ele traduz os parâmetros do Discord pro formato que o <code>CommandDispatcher</code> já espera:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">dispatch_interaction</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">type</span><span class="p">,</span> <span class="ss">match</span><span class="p">:</span> <span class="o">[</span><span class="kp">nil</span><span class="o">]</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span><span class="o">.</span><span class="n">respond</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="s2">&#34;\u{2705}&#34;</span><span class="p">,</span> <span class="ss">ephemeral</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>  <span class="c1"># checkmark só visível pro autor</span>
</span></span><span class="line"><span class="cl">  <span class="n">command</span> <span class="o">=</span> <span class="no">SlashCommandParser</span><span class="o">::</span><span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="ss">type</span><span class="p">:</span> <span class="n">type</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">args</span><span class="p">:</span> <span class="p">{</span> <span class="ss">match</span><span class="p">:</span> <span class="n">match</span><span class="p">,</span> <span class="ss">attachments</span><span class="p">:</span> <span class="n">attachments</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">CommandDispatcher</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">command</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">channel_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">message_id</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">author_id</span><span class="p">:</span> <span class="n">event</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&#34;Discord interaction error (</span><span class="si">#{</span><span class="n">type</span><span class="si">}</span><span class="s2">): </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada handler de subcommand é um one-liner que monta os argumentos e chama <code>dispatch_interaction</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">application_command</span><span class="p">(</span><span class="ss">:update</span><span class="p">)</span><span class="o">.</span><span class="n">subcommand</span><span class="p">(</span><span class="ss">:score</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">stories</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">options</span><span class="o">[</span><span class="s2">&#34;stories&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">level</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">options</span><span class="o">[</span><span class="s2">&#34;level&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">dispatch_interaction</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="ss">:update_score</span><span class="p">,</span> <span class="ss">match</span><span class="p">:</span> <span class="o">[</span><span class="kp">nil</span><span class="p">,</span> <span class="n">stories</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">level</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>CommandDispatcher</code> não sabe se o comando veio de texto puro ou de Application Command. A mesma lógica, a mesma autorização, os mesmos jobs. Nenhum código duplicado.</p>
<h3>O Resultado<span class="hx:absolute hx:-mt-20" id="o-resultado"></span>
    <a href="#o-resultado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O projeto registra 18 Application Commands com combinações de subcommands, parâmetros tipados, dropdowns e upload de arquivos. O usuário digita <code>/</code> e vê tudo disponível com descrição. Escolhe um comando e o Discord guia o preenchimento campo a campo. Erros de sintaxe são impossíveis — o Discord valida antes de enviar.</p>
<p>E os dois sistemas coexistem: quem prefere autocomplete usa Application Commands, quem prefere digitar rápido usa texto puro. O <code>CommandDispatcher</code> recebe os dois da mesma forma. Adicionar um novo comando é adicionar um <code>register_*</code>, um <code>setup_*_handler</code>, e um método no dispatcher — sem mexer no runner.</p>
<h2>As Armadilhas do discordrb<span class="hx:absolute hx:-mt-20" id="as-armadilhas-do-discordrb"></span>
    <a href="#as-armadilhas-do-discordrb" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem tudo são flores. Algumas dores reais:</p>
<p><strong>Gateway disconnects</strong>: O WebSocket com o Discord cai de vez em quando. O discordrb reconecta automaticamente, mas seus handlers <code>bot.ready</code> e <code>bot.message</code> precisam ser resilientes a reconexões.</p>
<p><strong>Message Content Intent</strong>: Desde 2022, bots em mais de 75 servidores precisam de aprovação pra ler conteúdo de mensagens. Pra bots privados (que é o caso de operação interna), isso não é problema. Mas se você planeja distribuir o bot, prepare o paperwork.</p>
<p><strong>API Rate Limits</strong>: O Discord rate-limita por rota. Mandar 10 embeds de uma vez pro mesmo canal? Vai bater no limite. O discordrb faz rate limit handling internamente, mas chamadas rápidas demais causam delays visíveis.</p>
<p><strong>Embed Limits</strong>: Título máximo 256 chars. Description 4096. Field value 1024. Excedeu? Erro silencioso. Sempre trunca antes de enviar.</p>
<p><strong>File Uploads</strong>: Limit de 25MB no plano gratuito, 50MB com Nitro. Pra imagens de IA geradas isso nunca é problema, mas se for mandar PDFs ou áudio, cuidado.</p>
<h2>Quando NÃO Usar Discord Como Admin<span class="hx:absolute hx:-mt-20" id="quando-não-usar-discord-como-admin"></span>
    <a href="#quando-n%c3%a3o-usar-discord-como-admin" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><strong>Formulários complexos</strong>: editar 15 campos de um registro? Tela web</li>
<li><strong>Bulk operations</strong>: importar CSV com 1000 registros? Script</li>
<li><strong>Visualização de dados</strong>: gráficos, dashboards analíticos? Grafana/Metabase</li>
<li><strong>Aprovação multi-step</strong>: workflows com 5 etapas de aprovação? Precisa de estado persistido</li>
</ul>
<p>Mas pra o dia a dia operacional — curar conteúdo, aprovar newsletters, monitorar jobs, checar custos, conversar com IA — Discord é a interface mais produtiva que já usei. E o melhor: os assinantes nunca vão saber que a newsletter semanal deles foi curada por um cara digitando comandos no Discord do celular enquanto toma café.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_00-57-11.jpg" alt="hello"  loading="lazy" /></p>
<p>Usar Discord como admin panel não é preguiça — é pragmatismo. A melhor interface é a que você já usa o dia inteiro. Auth, push notifications, mobile, rich formatting, file sharing — tudo de graça. Você só constrói a lógica de negócio.</p>
<p>A gem <a href="https://rubyllm.com/"target="_blank" rel="noopener"><code>ruby_llm</code></a> com tool calling leva o bot de &ldquo;executor de comandos&rdquo; pra &ldquo;assistente que pensa&rdquo;. Com memória de sessão, pensa <strong>e lembra</strong>. Quando o LLM pode gerar imagens, buscar na web, manter contexto entre perguntas, e responder sobre a arquitetura do próprio sistema — aí deixou de ser CRUD e virou co-piloto.</p>
<p>E o padrão Parser → Dispatcher → Job mantém tudo testável. O parser não sabe nada de Discord. O dispatcher não sabe nada de IA. Os jobs não sabem nada de interface. Troca qualquer uma dessas camadas e as outras nem percebem.</p>
<p>No fim, a melhor admin UI é a que não precisa de deploy separado. E o pior bug é o que falha em silêncio — se o bot é sua interface operacional, as falhas do bot precisam aparecer <strong>na</strong> interface, não escondidas num log que ninguém lê.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>automacao</category><category>backend</category></item><item><title>Enviando Emails sem Spammar | Bastidores do The M.Akita Chronicles</title><link>https://akitaonrails.github.io/2026/02/17/enviando-emails-sem-spammar-bastidores-do-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/02/17/enviando-emails-sem-spammar-bastidores-do-the-m-akita-chronicles/</guid><pubDate>Tue, 17 Feb 2026 20:59:48 +0000</pubDate><description>&lt;p&gt;Este post vai fazer parte de uma série; acompanhe pela tag &lt;a href="https://akitaonrails.github.io/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. Esta é a parte 1.&lt;/p&gt;
&lt;p&gt;E não deixe de assinar minha nova newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Enviar emails parece trivial. Chama uma API, passa o HTML, manda. Pronto. Até o dia em que você descobre que enviou a mesma newsletter duas vezes pra 300 pessoas. Ou que 50 e-mails voltaram como bounce e a Amazon suspendeu sua conta. Ou que seu servidor reiniciou no meio do envio e agora você não sabe quem recebeu e quem não recebeu.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post vai fazer parte de uma série; acompanhe pela tag <a href="/tags/the-m-akita-chronicles/">/themakitachronicles</a>. Esta é a parte 1.</p>
<p>E não deixe de assinar minha nova newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Enviar emails parece trivial. Chama uma API, passa o HTML, manda. Pronto. Até o dia em que você descobre que enviou a mesma newsletter duas vezes pra 300 pessoas. Ou que 50 e-mails voltaram como bounce e a Amazon suspendeu sua conta. Ou que seu servidor reiniciou no meio do envio e agora você não sabe quem recebeu e quem não recebeu.</p>
<p>O Amazon SES é eficiente — e brutalmente impiedoso com quem não trata cada envio como uma operação que pode falhar em cinco formas. Neste post vou mostrar os padrões que transformam &ldquo;mandar email&rdquo; em &ldquo;mandar email de forma confiável&rdquo;.</p>
<h2>Por Que SES e Não Sendgrid/Mailgun/Resend?<span class="hx:absolute hx:-mt-20" id="por-que-ses-e-não-sendgridmailgunresend"></span>
    <a href="#por-que-ses-e-n%c3%a3o-sendgridmailgunresend" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Preço. Ponto. O SES cobra $0.10 por mil e-mails. Sendgrid cobra $20/mês por 50 mil. Pra uma newsletter pequena, os dois são baratos. Mas o SES escala sem mudar de plano — e como já estou na AWS pra S3, não preciso de mais um vendor.</p>
<p>A desvantagem? SES é low-level. Não tem interface bonita de drag-and-drop, não tem analytics integrado, não tem IP dedicado no plano free. Você ganha uma API que aceita HTML e retorna um <code>message_id</code>. O resto é problema seu.</p>
<p>E é exatamente aí que mora o perigo.</p>
<h2>O Problema Fundamental: At-Most-Once vs At-Least-Once<span class="hx:absolute hx:-mt-20" id="o-problema-fundamental-at-most-once-vs-at-least-once"></span>
    <a href="#o-problema-fundamental-at-most-once-vs-at-least-once" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quando um serviço externo aceita sua requisição, mas o seu processo morre antes de registrar o sucesso, você tem um dilema:</p>
<ol>
<li><strong>Reenviar</strong>: o assinante pode receber duas vezes (at-least-once)</li>
<li><strong>Não reenviar</strong>: o assinante pode não receber (at-most-once)</li>
</ol>
<p>Pra transações financeiras, at-least-once com deduplicação é o padrão. Pra email, <strong>at-most-once é preferível</strong>. Ninguém reclama de não receber uma newsletter. Todo mundo reclama de receber duas.</p>
<p>Isso define toda a arquitetura: <strong>na dúvida, não reenvie</strong>.</p>
<h2>Padrão 1: Um Registro Por Destinatário<span class="hx:absolute hx:-mt-20" id="padrão-1-um-registro-por-destinatário"></span>
    <a href="#padr%c3%a3o-1-um-registro-por-destinat%c3%a1rio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O erro mais comum em sistemas de newsletter é iterar uma lista de assinantes num loop:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ERRADO — se o job morrer no email 247, re-execução reenvia 1-246</span>
</span></span><span class="line"><span class="cl"><span class="n">subscribers</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">SesMailer</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span> <span class="nb">sub</span><span class="o">.</span><span class="n">email</span><span class="p">,</span> <span class="ss">body</span><span class="p">:</span> <span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A solução é criar um registro de entrega <strong>antes</strong> de enviar:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Cria registros em batch</span>
</span></span><span class="line"><span class="cl"><span class="n">rows</span> <span class="o">=</span> <span class="n">subscriber_ids</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="nb">id</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span> <span class="ss">newsletter_id</span><span class="p">:</span> <span class="n">newsletter</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="ss">subscriber_id</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span> <span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;pending&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="no">EmailDelivery</span><span class="o">.</span><span class="n">insert_all</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="ss">unique_by</span><span class="p">:</span> <span class="o">[</span><span class="ss">:newsletter_id</span><span class="p">,</span> <span class="ss">:subscriber_id</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enfileira um job individual por registro</span>
</span></span><span class="line"><span class="cl"><span class="no">EmailDelivery</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">newsletter</span><span class="p">:</span> <span class="n">newsletter</span><span class="p">,</span> <span class="ss">status</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;pending&#34;</span><span class="p">,</span> <span class="s2">&#34;failed&#34;</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">             <span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">delivery</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">SendSingleEmailJob</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="n">delivery</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada <code>EmailDelivery</code> é uma máquina de estados: <code>pending → sending → sent</code>. Se o job morrer, o registro fica em <code>sending</code> — e <strong>nunca</strong> é automaticamente reenviado.</p>
<p>O <code>unique_by</code> garante idempotência: rodar esse código duas vezes não cria registros duplicados.</p>
<h2>Padrão 2: Claiming Atômico<span class="hx:absolute hx:-mt-20" id="padrão-2-claiming-atômico"></span>
    <a href="#padr%c3%a3o-2-claiming-at%c3%b4mico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dois workers não podem enviar o mesmo email. O claim precisa ser atômico:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">claim_for_sending!</span>
</span></span><span class="line"><span class="cl">  <span class="n">updated</span> <span class="o">=</span> <span class="nb">self</span><span class="o">.</span><span class="n">class</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">id</span><span class="p">,</span> <span class="ss">status</span><span class="p">:</span> <span class="sx">%w[pending failed]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">update_all</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;sending&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">sending_started_at</span><span class="p">:</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">send_attempts</span><span class="p">:</span> <span class="no">Arel</span><span class="o">.</span><span class="n">sql</span><span class="p">(</span><span class="s2">&#34;COALESCE(send_attempts, 0) + 1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">updated</span> <span class="o">==</span> <span class="mi">1</span>  <span class="c1"># true se ninguém pegou antes</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso é um UPDATE com WHERE — se dois workers tentam ao mesmo tempo, só um consegue o <code>updated == 1</code>. O outro recebe <code>0</code> e desiste. Sem locks explícitos, sem race conditions.</p>
<p>O <code>send_attempts</code> é incrementado atomicamente no banco, não em Ruby. Isso garante a contagem correta, mesmo com concorrência.</p>
<h2>Padrão 3: O Fluxo do SendSingleEmailJob<span class="hx:absolute hx:-mt-20" id="padrão-3-o-fluxo-do-sendsingleemailjob"></span>
    <a href="#padr%c3%a3o-3-o-fluxo-do-sendsingleemailjob" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui está o coração do sistema. O job de envio individual segue uma sequência deliberada:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SendSingleEmailJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:mailer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">retry_on</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">TooManyRequestsException</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">           <span class="ss">wait</span><span class="p">:</span> <span class="ss">:polynomially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">delivery_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ses_accepted</span> <span class="o">=</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="n">delivery</span> <span class="o">=</span> <span class="no">EmailDelivery</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">delivery_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. Validação: assinante ainda ativo?</span>
</span></span><span class="line"><span class="cl">    <span class="k">unless</span> <span class="n">subscriber_active?</span><span class="p">(</span><span class="n">delivery</span><span class="o">.</span><span class="n">subscriber</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">delivery</span><span class="o">.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="s2">&#34;Subscriber inactive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 2. Guardrail: nunca reenviar o que já foi</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">if</span> <span class="sx">%w[sent unknown bounced]</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">delivery</span><span class="o">.</span><span class="n">status</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 3. Claim atômico</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">delivery</span><span class="o">.</span><span class="n">claim_for_sending!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 4. Renderiza e envia</span>
</span></span><span class="line"><span class="cl">    <span class="n">html</span> <span class="o">=</span> <span class="n">render_newsletter</span><span class="p">(</span><span class="n">delivery</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">message_id</span> <span class="o">=</span> <span class="n">ses_client</span><span class="o">.</span><span class="n">send_email</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span> <span class="n">email</span><span class="p">,</span> <span class="ss">body</span><span class="p">:</span> <span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ses_accepted</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 5. Marca sucesso</span>
</span></span><span class="line"><span class="cl">    <span class="n">delivery</span><span class="o">.</span><span class="n">mark_sent!</span><span class="p">(</span><span class="n">message_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">TooManyRequestsException</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># SES rejeitou ANTES de aceitar — seguro retentar</span>
</span></span><span class="line"><span class="cl">    <span class="n">delivery</span><span class="o">&amp;.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="s2">&#34;Rate limited: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span>  <span class="c1"># ActiveJob retry_on cuida do reagendamento</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">ses_accepted</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># SES ACEITOU mas algo quebrou depois — estado ambíguo</span>
</span></span><span class="line"><span class="cl">      <span class="n">delivery</span><span class="o">&amp;.</span><span class="n">mark_unknown!</span><span class="p">(</span><span class="s2">&#34;Uncertain: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">delivery</span><span class="o">&amp;.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Presta atenção na variável <code>ses_accepted</code>. Ela é o pivô de toda a lógica de erro:</p>
<ul>
<li><strong>SES rejeitou</strong> (<code>ses_accepted = false</code>): safe to retry. O email não foi aceito; pode tentar de novo.</li>
<li><strong>SES aceitou</strong> (<code>ses_accepted = true</code>): estado ambíguo. O email <em>provavelmente</em> foi enviado. Marcar como <code>unknown</code> e <strong>nunca</strong> retentar automaticamente.</li>
</ul>
<p>Essa distinção é o que impede a duplicação. A maioria dos sistemas trata tudo como &ldquo;failed&rdquo; e re-tenta — o que pode resultar em e-mails duplicados quando o SES aceitou, mas o seu processo crashou antes de registrar.</p>
<h2>Padrão 4: O Guardrail dos Status Imutáveis<span class="hx:absolute hx:-mt-20" id="padrão-4-o-guardrail-dos-status-imutáveis"></span>
    <a href="#padr%c3%a3o-4-o-guardrail-dos-status-imut%c3%a1veis" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Observe a linha:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">return</span> <span class="k">if</span> <span class="sx">%w[sent unknown bounced]</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">delivery</span><span class="o">.</span><span class="n">status</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso não é defensivo à toa. É uma regra de negócio: esses três estados são <strong>terminais</strong>. Uma vez que um delivery chega em <code>sent</code>, <code>unknown</code> ou <code>bounced</code>, nenhum código no sistema pode movê-lo automaticamente de volta pra <code>pending</code>.</p>
<p>A única forma de reenviar um email <code>unknown</code> é por meio de intervenção manual no console. E é assim que deve ser — se você não tem certeza de que o e-mail foi entregue, a decisão de reenviar é humana, não do sistema.</p>
<h2>Padrão 5: Recuperação de Deliveries Travados<span class="hx:absolute hx:-mt-20" id="padrão-5-recuperação-de-deliveries-travados"></span>
    <a href="#padr%c3%a3o-5-recupera%c3%a7%c3%a3o-de-deliveries-travados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se o worker crashar com um delivery em <code>sending</code>, ele fica preso nesse estado pra sempre. Um job periódico limpa isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RecoverStaleDeliveriesJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="no">STALE_AFTER</span> <span class="o">=</span> <span class="mi">30</span><span class="o">.</span><span class="n">minutes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="n">stale</span> <span class="o">=</span> <span class="no">EmailDelivery</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;sending&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                         <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;sending_started_at &lt; ?&#34;</span><span class="p">,</span> <span class="no">STALE_AFTER</span><span class="o">.</span><span class="n">ago</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">count</span> <span class="o">=</span> <span class="n">stale</span><span class="o">.</span><span class="n">update_all</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="s2">&#34;unknown&#34;</span><span class="p">,</span> <span class="ss">sending_started_at</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">notify_team</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">count</span><span class="si">}</span><span class="s2"> stale deliveries moved to unknown&#34;</span><span class="p">)</span> <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por que <code>unknown</code> e não <code>failed</code>? Por que um delivery em <code>sending</code> há 30 minutos pode ter sido aceito pelo SES, mas não registrado? Mover pra <code>failed</code> significaria retentar — e, potencialmente, duplicar. <code>unknown</code> é o limbo seguro: &ldquo;não sei o que aconteceu; um humano precisa decidir&rdquo;.</p>
<h2>Padrão 6: Retry Inteligente no SES<span class="hx:absolute hx:-mt-20" id="padrão-6-retry-inteligente-no-ses"></span>
    <a href="#padr%c3%a3o-6-retry-inteligente-no-ses" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SES tem três categorias de erro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Transitório — SES está ocupado, tente depois</span>
</span></span><span class="line"><span class="cl"><span class="n">retry_on</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">TooManyRequestsException</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="ss">wait</span><span class="p">:</span> <span class="ss">:polynomially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">8</span>
</span></span><span class="line"><span class="cl"><span class="n">retry_on</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">LimitExceededException</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="ss">wait</span><span class="p">:</span> <span class="ss">:polynomially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Permanente — sua conta ou email tem problema</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">MessageRejected</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="n">delivery</span><span class="o">.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># NÃO re-tenta. O SES rejeitou esse email especificamente.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">Aws</span><span class="o">::</span><span class="no">SESV2</span><span class="o">::</span><span class="no">Errors</span><span class="o">::</span><span class="no">AccountSuspendedException</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">  <span class="n">delivery</span><span class="o">.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Sua conta foi suspensa. Não adianta retentar nada.</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>polynomially_longer</code> é chave: 3s, 18s, 83s, 293s&hellip; O backoff polinomial é mais agressivo que o exponencial, o que é bom pro SES — ele quer que você desacelere rápido.</p>
<p><strong>Nunca</strong> use <code>retry_on StandardError</code>. Isso re-tenta erros de programação (nil pointer, método inexistente) que vão falhar eternamente.</p>
<h2>Padrão 7: Tags SES para Rastreabilidade<span class="hx:absolute hx:-mt-20" id="padrão-7-tags-ses-para-rastreabilidade"></span>
    <a href="#padr%c3%a3o-7-tags-ses-para-rastreabilidade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cada email enviado carrega tags que permitem correlacionar eventos depois:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">mailer</span><span class="o">.</span><span class="n">send_email</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="ss">to</span><span class="p">:</span> <span class="n">subscriber</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">subject</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">html_body</span><span class="p">:</span> <span class="n">html</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">tags</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;NewsletterID&#34;</span> <span class="o">=&gt;</span> <span class="n">newsletter</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;DeliveryID&#34;</span>   <span class="o">=&gt;</span> <span class="n">delivery</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando o SES notifica um bounce ou um complaint via SNS, o payload inclui essas tags. O reconciliador encontra o <code>EmailDelivery</code> correspondente e atualiza o status:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">reconcile_event</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">delivery</span> <span class="o">=</span> <span class="n">find_by_tag</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="s2">&#34;DeliveryID&#34;</span><span class="p">)</span> <span class="o">||</span>
</span></span><span class="line"><span class="cl">             <span class="n">find_by_message_id</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="k">unless</span> <span class="n">delivery</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">event_type</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="s2">&#34;delivery&#34;</span>  <span class="k">then</span> <span class="n">delivery</span><span class="o">.</span><span class="n">mark_sent!</span><span class="p">(</span><span class="n">message_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="s2">&#34;bounce&#34;</span>    <span class="k">then</span> <span class="n">delivery</span><span class="o">.</span><span class="n">mark_bounced!</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="s2">&#34;complaint&#34;</span> <span class="k">then</span> <span class="n">delivery</span><span class="o">.</span><span class="n">mark_bounced!</span>
</span></span><span class="line"><span class="cl">  <span class="k">when</span> <span class="s2">&#34;reject&#34;</span>    <span class="k">then</span> <span class="n">delivery</span><span class="o">.</span><span class="n">mark_failed!</span><span class="p">(</span><span class="s2">&#34;SES rejected&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso fecha o loop: mesmo que o delivery tenha ficado como <code>unknown</code> porque o worker tenha crashado, o SES notifica eventualmente se entregou ou não. O reconciliador corrige o estado.</p>
<h2>Padrão 8: Configuration Sets para Monitoramento<span class="hx:absolute hx:-mt-20" id="padrão-8-configuration-sets-para-monitoramento"></span>
    <a href="#padr%c3%a3o-8-configuration-sets-para-monitoramento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SES tem um recurso chamado Configuration Sets que é absurdamente subutilizado:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">params</span><span class="o">[</span><span class="ss">:configuration_set_name</span><span class="o">]</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;SES_CONFIGURATION_SET&#34;</span><span class="o">]</span> <span class="k">if</span> <span class="n">configured?</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O Configuration Set te dá:</p>
<ul>
<li><strong>Delivery events</strong>: notificações de entrega, bounce, complaint via SNS</li>
<li><strong>Reputation dashboard</strong>: taxa de bounce e complaint em tempo real</li>
<li><strong>Sending statistics</strong>: volume por hora/dia</li>
</ul>
<p>Sem Configuration Set, você está voando cego. Com ele, sabe exatamente o estado da sua reputação como sender.</p>
<p>A Amazon suspende sua conta se a taxa de bounce passar de 5% ou se as complaints passarem de 0,1%. Com o dashboard, você vê isso acontecendo antes que seja tarde demais.</p>
<h2>Padrão 9: Relatório de Envio Diferido<span class="hx:absolute hx:-mt-20" id="padrão-9-relatório-de-envio-diferido"></span>
    <a href="#padr%c3%a3o-9-relat%c3%b3rio-de-envio-diferido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não adianta reportar resultado imediatamente após enfileirar. Os jobs ainda estão rodando. O relatório precisa ser <strong>diferido</strong>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Enfileira relatório 30 minutos depois do envio</span>
</span></span><span class="line"><span class="cl"><span class="no">SendingReportJob</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="ss">wait</span><span class="p">:</span> <span class="mi">30</span><span class="o">.</span><span class="n">minutes</span><span class="p">)</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="n">newsletter</span><span class="o">.</span><span class="n">id</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E o relatório se reagenda se muitos deliveries ainda estão pendentes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">newsletter_id</span><span class="p">,</span> <span class="n">reschedule_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">counts</span> <span class="o">=</span> <span class="n">newsletter</span><span class="o">.</span><span class="n">email_deliveries</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="ss">:status</span><span class="p">)</span><span class="o">.</span><span class="n">count</span>
</span></span><span class="line"><span class="cl">  <span class="n">pending_ratio</span> <span class="o">=</span> <span class="n">pending_plus_sending</span> <span class="o">/</span> <span class="n">total</span><span class="o">.</span><span class="n">to_f</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">pending_ratio</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="o">.</span><span class="mi">10</span> <span class="o">&amp;&amp;</span> <span class="n">reschedule_count</span> <span class="o">&lt;</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Mais de 10% ainda em andamento — checa de novo em 15min</span>
</span></span><span class="line"><span class="cl">    <span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="ss">wait</span><span class="p">:</span> <span class="mi">15</span><span class="o">.</span><span class="n">minutes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="n">newsletter_id</span><span class="p">,</span> <span class="n">reschedule_count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">send_report</span><span class="p">(</span><span class="n">counts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O relatório final mostra sent/failed/pending/unknown/bounced. Se tem <code>unknown</code> &gt; 0, a cor é vermelha — precisa de atenção humana.</p>
<h2>O Sandbox Trap<span class="hx:absolute hx:-mt-20" id="o-sandbox-trap"></span>
    <a href="#o-sandbox-trap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma dor que todo mundo passa e ninguém documenta: o SES começa em modo sandbox. Você só pode enviar para e-mails verificados. Pra sair do sandbox, precisa de um request manual explicando seu caso de uso, volume estimado, e como trata bounces/complaints.</p>
<p>Dicas pra aprovação:</p>
<ol>
<li><strong>Tenha domínio verificado com DKIM</strong> — não mande de <code>@gmail.com</code></li>
<li><strong>Explique o opt-in duplo</strong> — &ldquo;assinante confirma email antes de receber&rdquo;</li>
<li><strong>Mostre tratamento de bounce</strong> — &ldquo;bounces são automaticamente processados e removidos&rdquo;</li>
<li><strong>Comece pequeno</strong> — peça 200/dia, escale depois</li>
</ol>
<p>A Amazon é paranoica em relação à reputação. Mostra que você respeita isso.</p>
<h2>DKIM, SPF, e DMARC: A Trilogia que Mantém Seus E-mails na Inbox<span class="hx:absolute hx:-mt-20" id="dkim-spf-e-dmarc-a-trilogia-que-mantém-seus-e-mails-na-inbox"></span>
    <a href="#dkim-spf-e-dmarc-a-trilogia-que-mant%c3%a9m-seus-e-mails-na-inbox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sem autenticação DNS correta, seus e-mails vão pro spam. Três registros são obrigatórios:</p>
<ul>
<li><strong>SPF</strong>: declara quais servidores podem enviar em nome do seu domínio</li>
<li><strong>DKIM</strong>: assinatura criptográfica que prova que o email não foi alterado</li>
<li><strong>DMARC</strong>: política que diz o que fazer com emails que falham SPF/DKIM</li>
</ul>
<p>O SES gera os registros DKIM automaticamente (são 3 CNAMEs). SPF e DMARC você configura manualmente no DNS.</p>
<p>Se você não fizer isso, mesmo o Gmail vai marcar seus e-mails como suspeitos. E o pior: provedores como Microsoft e Yahoo estão cada vez mais agressivos com e-mails sem DMARC.</p>
<h2>Padrão 10: List-Unsubscribe — O Header que o Gmail Exige<span class="hx:absolute hx:-mt-20" id="padrão-10-list-unsubscribe--o-header-que-o-gmail-exige"></span>
    <a href="#padr%c3%a3o-10-list-unsubscribe--o-header-que-o-gmail-exige" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde fevereiro de 2024, o Gmail tem uma regra dura pra bulk senders (quem manda mais de 5.000 emails/dia pra endereços do Gmail): todo email precisa ter os headers <code>List-Unsubscribe</code> e <code>List-Unsubscribe-Post</code>. Sem eles, o Gmail <strong>penaliza a reputação do domínio inteiro</strong> — não só os e-mails em massa, mas também os transacionais (como confirmação de cadastro, reset de senha, etc.).</p>
<p>Descobri isso da pior forma: usuários do Gmail não recebiam o e-mail de confirmação de inscrição. DKIM passando, SPF passando, DMARC passando, SES aceitando — tudo verde. Mas o Gmail estava silenciosamente encaminhando para o spam porque a newsletter não tinha <code>List-Unsubscribe</code>.</p>
<p>A implementação tem duas partes: o header no email e o endpoint que processa o unsubscribe.</p>
<h3>O Header<span class="hx:absolute hx:-mt-20" id="o-header"></span>
    <a href="#o-header" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Cada email precisa de dois headers — com URL personalizada por assinante:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">mailer</span><span class="o">.</span><span class="n">send_email</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="ss">to</span><span class="p">:</span> <span class="n">subscriber</span><span class="o">.</span><span class="n">email</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">subject</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">html_body</span><span class="p">:</span> <span class="n">html</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">headers</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;List-Unsubscribe&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;&lt;https://seudominio.com/unsubscribe?token=</span><span class="si">#{</span><span class="n">subscriber</span><span class="o">.</span><span class="n">unsubscribe_token</span><span class="si">}</span><span class="s2">&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;List-Unsubscribe-Post&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;List-Unsubscribe=One-Click&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No SES v2, headers customizados vão no campo <code>headers</code> do <code>simple</code> content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">simple</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">subject</span><span class="p">:</span> <span class="p">{</span> <span class="ss">data</span><span class="p">:</span> <span class="n">subject</span><span class="p">,</span> <span class="ss">charset</span><span class="p">:</span> <span class="s2">&#34;UTF-8&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="ss">body</span><span class="p">:</span> <span class="p">{</span> <span class="ss">html</span><span class="p">:</span> <span class="p">{</span> <span class="o">...</span> <span class="p">},</span> <span class="ss">text</span><span class="p">:</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="ss">headers</span><span class="p">:</span> <span class="n">headers</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span> <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="nb">name</span><span class="o">.</span><span class="n">to_s</span><span class="p">,</span> <span class="ss">value</span><span class="p">:</span> <span class="n">value</span><span class="o">.</span><span class="n">to_s</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Atenção</strong>: o link de unsubscribe no <strong>corpo</strong> do email (o que está no footer) não conta. O Gmail exige especificamente o <strong>header HTTP</strong>. São coisas diferentes. Você precisa dos dois.</p>
<h3>O Endpoint RFC 8058<span class="hx:absolute hx:-mt-20" id="o-endpoint-rfc-8058"></span>
    <a href="#o-endpoint-rfc-8058" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando o usuário clica em &ldquo;Cancelar inscrição&rdquo; no Gmail, o que acontece não é um GET no browser. O Gmail faz um POST direto pro seu servidor com o body <code>List-Unsubscribe=One-Click</code>. Sem cookies, sem CSRF token, sem redirect.</p>
<p>Isso significa que seu controller precisa:</p>
<ol>
<li><strong>Pular verificação CSRF</strong> pra esse tipo de request</li>
<li><strong>Retornar 200 OK</strong> ao invés de redirect (clients de email não seguem redirects)</li>
<li><strong>Continuar funcionando normalmente</strong> pra unsubscribes via browser</li>
</ol>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UnsubscriptionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># RFC 8058: email clients POST sem CSRF token</span>
</span></span><span class="line"><span class="cl">  <span class="n">skip_forgery_protection</span> <span class="ss">only</span><span class="p">:</span> <span class="ss">:destroy</span><span class="p">,</span> <span class="k">if</span><span class="p">:</span> <span class="ss">:one_click_unsubscribe?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">destroy</span>
</span></span><span class="line"><span class="cl">    <span class="n">subscriber</span> <span class="o">=</span> <span class="no">Subscriber</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">unsubscribe_token</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:token</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">subscriber</span><span class="o">&amp;.</span><span class="n">unsubscribe!</span> <span class="k">unless</span> <span class="n">subscriber</span><span class="o">&amp;.</span><span class="n">unsubscribed?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">one_click_unsubscribe?</span>
</span></span><span class="line"><span class="cl">      <span class="n">head</span> <span class="ss">:ok</span>  <span class="c1"># Gmail espera 200, não redirect</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">redirect_to</span> <span class="n">unsubscribe_path</span><span class="p">(</span><span class="ss">token</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:token</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">private</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">one_click_unsubscribe?</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">[</span><span class="s2">&#34;List-Unsubscribe&#34;</span><span class="o">]</span> <span class="o">==</span> <span class="s2">&#34;One-Click&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>skip_forgery_protection</code> é condicional — só dispara quando o request contém o parâmetro <code>List-Unsubscribe=One-Click</code>. Requests normais do browser continuam com proteção CSRF total.</p>
<p>Depois de implementar isso, a reputação do domínio no Gmail leva 1-2 dias pra se recuperar. Os emails de confirmação voltaram a chegar na inbox.</p>
<h2>Padrão 11: A Suppression List Silenciosa do SES<span class="hx:absolute hx:-mt-20" id="padrão-11-a-suppression-list-silenciosa-do-ses"></span>
    <a href="#padr%c3%a3o-11-a-suppression-list-silenciosa-do-ses" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O SES tem uma armadilha que pega muita gente: a <strong>account-level suppression list</strong>. Quando um email dá hard bounce (endereço inexistente) ou gera um complaint (marcou como spam), o SES automaticamente adiciona esse endereço a uma lista interna. A partir daí, toda vez que você manda pra esse endereço, o SES <strong>aceita a requisição, retorna um message_id e, silenciosamente, não entrega</strong>.</p>
<p>Leu de novo: <strong>o SES retorna sucesso, mas não manda</strong>. Seu sistema acha que mandou. O assinante nunca recebe. Nenhum erro, nenhum bounce, nenhum log.</p>
<p>Pra ver quem está na lista:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">aws sesv2 list-suppressed-destinations --output table</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra remover um endereço (por exemplo, depois de corrigir um problema de deliverability):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">aws sesv2 delete-suppressed-destination --email-address <span class="s2">&#34;usuario@gmail.com&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Cuidado</strong>: não saia removendo tudo. Se o endereço deu bounce por typo (<code>usuario@gnail.com</code> em vez de <code>@gmail.com</code>), remover da suppression list só vai gerar outro bounce — e piorar sua taxa de bounce, exatamente o que o SES monitora pra decidir se suspende sua conta.</p>
<p>A regra: só remova endereços que você tenha motivo concreto pra acreditar que são válidos. Typos e endereços abandonados ficam na lista — é o SES que te protege de você mesmo.</p>
<h2>Encriptação de Emails em Repouso<span class="hx:absolute hx:-mt-20" id="encriptação-de-emails-em-repouso"></span>
    <a href="#encripta%c3%a7%c3%a3o-de-emails-em-repouso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um detalhe que muitos ignoram: os e-mails dos seus assinantes estão no banco. Se o banco vazar, vazam todos os e-mails. O Rails 8 tem Active Record Encryption nativo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Subscriber</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="n">encrypts</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">deterministic</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>deterministic: true</code> é crucial: permite que <code>find_by(email: &quot;x@y.com&quot;)</code> funcione normalmente, pois o mesmo input sempre gera o mesmo ciphertext. O índice unique continua funcionando. Do ponto de vista da aplicação, nada muda — mas, no banco, os e-mails são cifrados.</p>
<p>As chaves vivem em variáveis de ambiente, não no banco de dados. O banco vazou? Os e-mails são ilegíveis sem as chaves.</p>
<h2>O Custo Total<span class="hx:absolute hx:-mt-20" id="o-custo-total"></span>
    <a href="#o-custo-total" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra uma newsletter com 1.000 assinantes enviando 4 edições por mês:</p>
<table>
  <thead>
      <tr>
          <th>Item</th>
          <th>Custo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SES (4.000 emails)</td>
          <td>$0.40/mês</td>
      </tr>
      <tr>
          <td>Domínio verificado</td>
          <td>$0</td>
      </tr>
      <tr>
          <td>Configuration Set</td>
          <td>$0</td>
      </tr>
      <tr>
          <td>SNS notifications</td>
          <td>~$0 (free tier)</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>$0.40/mês</strong></td>
      </tr>
  </tbody>
</table>
<p>Compara com o Mailchimp ($13/mês), o ConvertKit ($15/mês) ou o Buttondown ($9/mês). A diferença é que, com SES, você <strong>construiu</strong> a infraestrutura em vez de alugar. É mais trabalho inicial, mas com zero lock-in e controle total.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Enviar e-mail confiável não é sobre chamar uma API. É sobre:</p>
<ul>
<li><strong>Registros por destinatário</strong> com claiming atômico</li>
<li><strong>Estados terminais</strong> que nunca são revertidos automaticamente</li>
<li><strong>Distinção explícita</strong> entre &ldquo;SES rejeitou&rdquo; e &ldquo;SES aceitou mas algo quebrou&rdquo;</li>
<li><strong>Recuperação de estados ambíguos</strong> sempre pra <code>unknown</code>, nunca pra <code>failed</code></li>
<li><strong>Relatórios diferidos</strong> que se reagendam até ter dados completos</li>
<li><strong>Reconciliação via webhooks</strong> que fecha o loop de entrega</li>
<li><strong>DNS correto</strong> (DKIM/SPF/DMARC) pra não ir pro spam</li>
<li><strong><code>List-Unsubscribe</code> em todo email</strong> — sem ele, Gmail penaliza seu domínio inteiro</li>
<li><strong>Consciência da suppression list</strong> — SES pode aceitar e silenciosamente não entregar</li>
</ul>
<p>A regra de ouro: <strong>na dúvida, não reenvie</strong>. É melhor um assinante não receber uma edição do que receber duas. A reputação do seu domínio — e a paciência dos seus assinantes — dependem disso.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>seguranca</category><category>backend</category></item><item><title>[Akitando] #145 - 16 Linguagens em 16 Dias: Minha Saga da Rinha de Backend</title><link>https://akitaonrails.github.io/2023/09/20/akitando-145-16-linguagens-em-16-dias-minha-saga-da-rinha-de-backend/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/09/20/akitando-145-16-linguagens-em-16-dias-minha-saga-da-rinha-de-backend/</guid><pubDate>Wed, 20 Sep 2023 15:35:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/EifK2a_5K_U"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;A Rinha de Backend que aconteceu em Agosto de 2023 foi muito divertida. Eu só fiquei sabendo quando acabou, mas não quer dizer que não pude me divertir. Hoje quero resumir tudo que eu fiz nos 16 dias seguintes do evento, detalhes sobre os projetos dos participantes, a controvérsia do Ranking de Linguagens, quais os truques por trás dos vencedores, e como você também poderia ser um vencedor!&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/EifK2a_5K_U"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>A Rinha de Backend que aconteceu em Agosto de 2023 foi muito divertida. Eu só fiquei sabendo quando acabou, mas não quer dizer que não pude me divertir. Hoje quero resumir tudo que eu fiz nos 16 dias seguintes do evento, detalhes sobre os projetos dos participantes, a controvérsia do Ranking de Linguagens, quais os truques por trás dos vencedores, e como você também poderia ser um vencedor!</p>
<p>Finalmente vou demonstrar o que significa &ldquo;ser promíscuo&rdquo; com linguagens de programação. Vamos entender porque como de fato ler um ranking. E como podemos fazer TODO MUNDO alcançar o primeiro lugar do Rust!</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00:00 - Intro</li>
<li>00:01:41 - CAP 01 - As Regras - Requerimentos da Rinha</li>
<li>00:09:46 - CAP 02 - Os Participantes - Vencedores da Rinha</li>
<li>00:15:25 - CAP 03 - Dia 1: Entrando na Rinha - Minha versão em Ruby on Rails</li>
<li>00:21:08 - CAP 04 - MrPowerGamerBR entra em cena - A chegada dos Piratas!</li>
<li>00:27:55 - CAP 05 - Tentando com Crystal - Aprendendo Lucky Framework</li>
<li>00:34:01 - CAP 06 - Dia 8: Gerenciando Baratie - Fluxo de Restaurante</li>
<li>00:38:37 - CAP 07 - Consertando Meu Rails - Aprendendo com Erros</li>
<li>00:42:54 - CAP 08 - Aprendendo com Node.js - Muita Refatoração</li>
<li>00:49:48 - CAP 09 - Apanhando de Erlang - Segredos de Elixir</li>
<li>00:55:51 - CAP 10 - Explorando Go Lang - Essa foi fácil</li>
<li>00:57:31 - CAP 11 - Dando Moral pra NATS - C# Vencedor</li>
<li>00:58:15 - CAP 12 - Dia 12: Explorando PHP Moderno - De Node a Swoole</li>
<li>01:00:12 - CAP 13 - Elevando Python - Pequeno Erro</li>
<li>01:02:21 - CAP 14 - &ldquo;foi lá, E FEZ&rdquo; - A Saga de Lean4</li>
<li>01:06:28 - CAP 15 - Pré-Feriado: Tentando NIM - A decepção</li>
<li>01:08:47 - CAP 16 - Diário de Bordo: Resumo - Chegando na Grand Line</li>
<li>01:13:05 - CAP 17 - Feriadão! - Encontrando o One Piece??</li>
<li>01:19:16 - CAP 18 - Testando o One Piece! Todos ao Primeiro Lugar!</li>
<li>01:27:16 - CAP 19 - Conclusão: Por que todo mundo não usa Rust?? - Escala de Mercados</li>
<li>01:37:33 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Repo Oficial da Rinha: <a href="https://github.com/zanfranceschi/rinha-de-backend-2023-q3/tree/main"target="_blank" rel="noopener">https://github.com/zanfranceschi/rinha-de-backend-2023-q3/tree/main</a></li>
<li>MrPowerGamerBR: # Os Resultados da RINHA DE BACKEND estão ERRADOS, e eu posso provar <a href="https://www.youtube.com/watch?v=XqYdhlkRlus"target="_blank" rel="noopener">https://www.youtube.com/watch?v=XqYdhlkRlus</a></li>
<li>Raciocínio Automatizado com Leonardo de Moura, Pesquisador na Microsoft Research <a href="https://dev.to/elixir_utfpr/raciocinio-automatizado-com-leonardo-de-moura-pesquisador-na-microsoft-research-4d1k"target="_blank" rel="noopener">https://dev.to/elixir_utfpr/raciocinio-automatizado-com-leonardo-de-moura-pesquisador-na-microsoft-research-4d1k</a></li>
<li>Rinha versão Algebraic <a href="https://github.com/meoowers/rinha"target="_blank" rel="noopener">https://github.com/meoowers/rinha</a>
Repositórios da Sofia <a href="https://github.com/algebraic-sofia?tab=repositories"target="_blank" rel="noopener">https://github.com/algebraic-sofia?tab=repositories</a></li>
</ul>
<h2>Tweets<span class="hx:absolute hx:-mt-20" id="tweets"></span>
    <a href="#tweets" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Tweet: Início da minha versão de Rails <a href="https://x.com/AkitaOnRails/status/1695526606503022923?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1695526606503022923?s=20</a></li>
<li>Tweet: O ranking oficial não é importante, discutir as técnicas, sim <a href="https://twitter.com/AkitaOnRails/status/1695913459202838839"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1695913459202838839</a></li>
<li>Tweet: 1o resultado com Crystal/Lucky <a href="https://x.com/AkitaOnRails/status/1696207752043872433?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1696207752043872433?s=20</a></li>
<li>Tweet: Minhas impressões aprendendo framework Lucky, em Crystal <a href="https://twitter.com/AkitaOnRails/status/1696220345152176478"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1696220345152176478</a></li>
<li>Tweet Leandro: diminuir workers do nginx melhora uso no lado do postgres <a href="https://x.com/leandronsp/status/1696292520106299417?s=20"target="_blank" rel="noopener">https://x.com/leandronsp/status/1696292520106299417?s=20</a></li>
<li>Tweet: Mais discussão das técnicas de otimização como batch insert <a href="https://x.com/AkitaOnRails/status/1696255958328938745?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1696255958328938745?s=20</a></li>
<li>Tweet Leandro: experimentando a teoria de diminuir workers e pool na versão de Ruby, conseguindo quase 30k <a href="https://x.com/leandronsp/status/1696219396316836331?s=20"target="_blank" rel="noopener">https://x.com/leandronsp/status/1696219396316836331?s=20</a></li>
<li>Tweet: contribuindo com o projeto Kiwi e comentando sobre LevelDB <a href="https://x.com/AkitaOnRails/status/1697283886156226818?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1697283886156226818?s=20</a></li>
<li>Tweet: meu mantra da programação <a href="https://x.com/AkitaOnRails/status/1697317688006066432?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1697317688006066432?s=20</a></li>
<li>Tweet: 1o resultado positivo com Crystal <a href="https://x.com/AkitaOnRails/status/1697651647273427429?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1697651647273427429?s=20</a></li>
<li>Tweet: 1o resultado positivo com Rails <a href="https://x.com/AkitaOnRails/status/1698475713920119117?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698475713920119117?s=20</a></li>
<li>Tweet: mexendo na versão Node do Lucas Weis <a href="https://x.com/AkitaOnRails/status/1698565631560282590?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698565631560282590?s=20</a></li>
<li>Tweet: desafio: todas conseguem bater os 40k <a href="https://x.com/AkitaOnRails/status/1698491304366219744?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698491304366219744?s=20</a></li>
<li>Tweet: apanhando da versão Elixir do Gabriel Oliveira <a href="https://x.com/AkitaOnRails/status/1698760144924905474?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698760144924905474?s=20</a></li>
<li>Tweet: revisando a versão Elixir <a href="https://x.com/AkitaOnRails/status/1698809091055874491?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698809091055874491?s=20</a></li>
<li>Tweet Gabriel: consertando o crash de Elixir no meu PC <a href="https://x.com/oliveigah/status/1698866344853151857?s=20"target="_blank" rel="noopener">https://x.com/oliveigah/status/1698866344853151857?s=20</a></li>
<li>Tweet: resolvido problema do Elixir <a href="https://twitter.com/AkitaOnRails/status/1698888695376437677"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698888695376437677</a></li>
<li>Tweet: mexendo no Go do Luan, 29k <a href="https://x.com/AkitaOnRails/status/1698818634397647092?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1698818634397647092?s=20</a></li>
<li>Tweet: fuçando a versão finalista de Dotnet <a href="https://twitter.com/AkitaOnRails/status/1698851765658013900"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698851765658013900</a></li>
<li>Tweet: 1o resultado da versão PHP do Lauro <a href="https://twitter.com/AkitaOnRails/status/1698923556631990453"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698923556631990453</a></li>
<li>Tweet: 1o resultado da versão Python do Ian Cambrea: <a href="https://twitter.com/AkitaOnRails/status/1698966472083517931"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698966472083517931</a></li>
<li>Tweet: Primeiro contato com Lean4 <a href="https://twitter.com/AkitaOnRails/status/1699064101111218212"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1699064101111218212</a></li>
<li>Tweet: Bibliotecas em Lean da Sofia <a href="https://twitter.com/AkitaOnRails/status/1699069812079767739"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1699069812079767739</a></li>
<li>Tweet: Resumo até agora <a href="https://twitter.com/AkitaOnRails/status/1698900197068153027"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698900197068153027</a></li>
<li>Tweet: Resumo das técnicas até agora <a href="https://twitter.com/AkitaOnRails/status/1698903125338226739"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698903125338226739</a></li>
<li>Tweet: Tentando versão em Nim (fracasso) <a href="https://twitter.com/AkitaOnRails/status/1699533869739856257"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1699533869739856257</a></li>
<li>Tweet: versão nova em Java do Junior Leão <a href="https://twitter.com/juniorleaoo/status/1699427149831114850"target="_blank" rel="noopener">https://twitter.com/juniorleaoo/status/1699427149831114850</a></li>
<li>Tweet: versão desclassificada em V Lang do Carlos Silva <a href="https://twitter.com/_insalubre/status/1699498542245544269"target="_blank" rel="noopener">https://twitter.com/_insalubre/status/1699498542245544269</a></li>
<li>Tweet: Leandro explica a lógica de diminuir vazão do nginx <a href="https://twitter.com/leandronsp/status/1699568664859603184"target="_blank" rel="noopener">https://twitter.com/leandronsp/status/1699568664859603184</a></li>
<li>Tweet: resumo das técnicas até agora: <a href="https://twitter.com/AkitaOnRails/status/1698903125338226739"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1698903125338226739</a></li>
<li>Tweet: Vinicius Ferraz descobre e publica network_mode: host <a href="https://twitter.com/AkitaOnRails/status/1700488994323128673"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1700488994323128673</a></li>
<li>Tweet: Reinaldo/Zsantana, atinge Zero knockouts: <a href="https://twitter.com/reijsantana/status/1701000310280573287"target="_blank" rel="noopener">https://twitter.com/reijsantana/status/1701000310280573287</a></li>
<li>Tweet: Chegando de viagem e ajustando todo mundo de novo: <a href="https://x.com/AkitaOnRails/status/1701230737922339143?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701230737922339143?s=20</a></li>
<li>Tweet: Rails bate 45k <a href="https://x.com/AkitaOnRails/status/1701248237586219416?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701248237586219416?s=20</a></li>
<li>Tweet: Ruby do Leandro bate 47k <a href="https://x.com/AkitaOnRails/status/1701423430870954312?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701423430870954312?s=20</a></li>
<li>Tweet: Rails do Lazaro bate 46k <a href="https://x.com/AkitaOnRails/status/1701446727910199459?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701446727910199459?s=20</a></li>
<li>Tweet: Node.js bate 46k <a href="https://x.com/AkitaOnRails/status/1701295880844775425?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701295880844775425?s=20</a></li>
<li>Tweet: Go do Luan bate 46k <a href="https://x.com/AkitaOnRails/status/1701286259459523033?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701286259459523033?s=20</a></li>
<li>Tweet: Bun bate 43k <a href="https://x.com/AkitaOnRails/status/1701630742642471420?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701630742642471420?s=20</a></li>
<li>Tweet: Python bate 46k <a href="https://x.com/AkitaOnRails/status/1701267343521767606?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701267343521767606?s=20</a></li>
<li>Tweet: V Lang bate 46k <a href="https://x.com/AkitaOnRails/status/1701256544904438210?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701256544904438210?s=20</a></li>
<li>Tweet: Consertando Java do Reinaldo, bate 46k <a href="https://x.com/AkitaOnRails/status/1701402351913910693?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1701402351913910693?s=20</a></li>
<li>Tweet: C++ do Lucas William bate 46k <a href="https://x.com/AkitaOnRails/status/1702008261736534294?s=20"target="_blank" rel="noopener">https://x.com/AkitaOnRails/status/1702008261736534294?s=20</a></li>
<li>Tweet: Refatorando o Rust do Vinicius <a href="https://twitter.com/AkitaOnRails/status/1701816747274260698"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1701816747274260698</a></li>
</ul>
<h2>Pull Requests<span class="hx:absolute hx:-mt-20" id="pull-requests"></span>
    <a href="#pull-requests" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>PR versão Node.js <a href="https://github.com/lukas8219/rinha-be-2023-q3/pull/1"target="_blank" rel="noopener">https://github.com/lukas8219/rinha-be-2023-q3/pull/1</a></li>
<li>PR versão Node.js, network mode <a href="https://github.com/lukas8219/rinha-be-2023-q3/pull/2"target="_blank" rel="noopener">https://github.com/lukas8219/rinha-be-2023-q3/pull/2</a></li>
<li>PR versão Go Lang <a href="https://github.com/luanpontes100/rinha-de-backend-2023-q3-golang/pull/1"target="_blank" rel="noopener">https://github.com/luanpontes100/rinha-de-backend-2023-q3-golang/pull/1</a></li>
<li>PR versão Go Lang, network mode <a href="https://github.com/luanpontes100/rinha-de-backend-2023-q3-golang/pull/1"target="_blank" rel="noopener">https://github.com/luanpontes100/rinha-de-backend-2023-q3-golang/pull/1</a></li>
<li>PR versão PHP/Swoole <a href="https://github.com/lauroappelt/rinha-de-backend-2023/pull/1"target="_blank" rel="noopener">https://github.com/lauroappelt/rinha-de-backend-2023/pull/1</a></li>
<li>PR versão Python/Sanic <a href="https://github.com/iancambrea/rinha-python-sanic/pull/1"target="_blank" rel="noopener">https://github.com/iancambrea/rinha-python-sanic/pull/1</a></li>
<li>PR versão de Lean4 fix de Location: <a href="https://github.com/meoowers/rinha/pull/1"target="_blank" rel="noopener">https://github.com/meoowers/rinha/pull/1</a></li>
<li>PR versão de Lean4 fix de data: <a href="https://github.com/meoowers/rinha/pull/2"target="_blank" rel="noopener">https://github.com/meoowers/rinha/pull/2</a></li>
<li>Versão em Nim/Jesper: <a href="https://github.com/akitaonrails/rinhabackend-nim-jester-api"target="_blank" rel="noopener">https://github.com/akitaonrails/rinhabackend-nim-jester-api</a></li>
<li>Versão em Java/Quarkus: <a href="https://github.com/zsantana/rinha-backend-by-bruno-borges/tree/main"target="_blank" rel="noopener">https://github.com/zsantana/rinha-backend-by-bruno-borges/tree/main</a></li>
<li>PR versão de C++: <a href="https://github.com/lucaswilliameufrasio/backend-cockfighting-q3-2023/pull/1"target="_blank" rel="noopener">https://github.com/lucaswilliameufrasio/backend-cockfighting-q3-2023/pull/1</a></li>
<li>PR versão de C# do Zan: <a href="https://github.com/zanfranceschi/rinha-de-backend-2023-q3-csharp/pull/3"target="_blank" rel="noopener">https://github.com/zanfranceschi/rinha-de-backend-2023-q3-csharp/pull/3</a></li>
<li>PR versão de Ruby do Leandro: <a href="https://github.com/leandronsp/rinha-backend-ruby/pull/4"target="_blank" rel="noopener">https://github.com/leandronsp/rinha-backend-ruby/pull/4</a></li>
<li>PR versão de Bun: <a href="https://github.com/Met4tron/rinha-bun/pull/1"target="_blank" rel="noopener">https://github.com/Met4tron/rinha-bun/pull/1</a></li>
<li>PR versão de Java do Reinaldo, fix de data: <a href="https://github.com/zsantana/rinha-backend-sem-cache/pull/1"target="_blank" rel="noopener">https://github.com/zsantana/rinha-backend-sem-cache/pull/1</a></li>
<li>PR versão de V Lang, network mode <a href="https://github.com/carlosqsilva/rinha-2023-q3/pull/1"target="_blank" rel="noopener">https://github.com/carlosqsilva/rinha-2023-q3/pull/1</a></li>
<li>PR versão de Rust do Vinicius, refactoring e fix: <a href="https://github.com/viniciusfonseca/rinha-backend-rust/pull/2"target="_blank" rel="noopener">https://github.com/viniciusfonseca/rinha-backend-rust/pull/2</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Se costuma acompanhar a comunidade de desenvolvimento no Twitter, talvez tenha ouvido falar que em agosto de 2023 aconteceu um mini evento online chamado de Rinha de Backend, organizado pelo Zanfranceschi, desenvolvedor experiente, atualmente trabalhando no Nubank.</p>
<p>Eu não conhecia ele e só fiquei sabendo que esse evento aconteceu quase uma semana depois que já tinha acabado. Mesmo se soubesse antes não teria participado porque nunca fui de competições, hackathons ou algo assim. Mas eu gostei muito do que os participantes fizeram e, sem querer, isso tomou boa parte do meu tempo, incluindo várias noites mau dormidas.</p>
<p>Sem sombra de dúvidas este vai ser o video mais trabalhoso que já fiz na história deste canal. Não só este como o próximo. Hoje vou resumir minha saga de 16 dias do pós-rinha avaliando diversos projetos dos participantes. Neste percurso, aprendi muita coisa nova que não sabia ou que achava que fosse ser diferente. Foi uma longa saga, muitas noites mau dormidas, frustração, ansiedade, crises existenciais, suor e lágrimas. É bastante coisa e só o resumo vai dar um video de mais de 1 hora.</p>
<p>Por isso vou pedir muita paciência de vocês. Vai ter várias técnicas importantes que vou mencionar o nome e os efeitos que causam, mas vai ser bem por cima. Se ficarem confusos com termos que parecem complicados, não se preocupem, vou explicar os detalhes no próximo. Também vai ser no próximo que vou falar detalhes das linguagens que vou mencionar. Então segurem as críticas. Hoje quero só compartilhar como foi minha experiência.</p>
<p>Primeiro vou resumir como foi o evento em si, um pouco sobre os projetos que ganharam e o ranking que gerou alguma controvérsia, pelo que ouvi falar. Daí vou contar o que pensei a respeito e como resolvi começar a abrir os projetos dos participantes com uma missão: fazer todo mundo chegar no primeiro lugar!</p>
<p>(&hellip;)</p>
<p>Como disse, eu não participei do evento, nem acompanhei o dia a dia. Acho que o desafio foi lançado primeiro mas ninguém sabia exatamente como seriam os critérios pra definir o vencedor até dias depois. E parte da graça de uma &ldquo;rinha&rdquo; como essa não é ser uma competição tecnicamente, perfeitamente justa, tem muito fator sorte e incerteza envolvido, como em qualquer esporte.</p>
<p>Nem sempre é o melhor atleta técnico que ganha todos os jogos, mas quem aprendeu mais nos jogos anteriores, quem consegue se adaptar rápido e quem teve menos azar. Assim foi esta rinha também, por isso em nenhum momento encarem qualquer coisa que parece uma crítica como uma crítica ao evento e sim como dicas pros próximos. Minha discussão vai ser puramente técnica. A crítica real vai pra quem tentou levar isso a sério demais. Ser torcedor apaixonado de um time é normal, sair xingando e brigando na rua quando vê o cara do outro time é ser um animal irracional imbecil. Vou assumir que estou falando com homo sapiens e não neandertais.</p>
<p>O desafio foi divulgado num repositório de GitHub do Zan que vou deixar o link nas descrições abaixo. Aliás, sempre olhem a descrição de todo video. Eu coloco links pra várias coisas extras pra vocês. Além de ter a descrição de todos os capítulos pra ficar mais fácil achar seções de videos longos como este. Enfim, leiam as instruções do desafio mas em resumo, o objetivo era escolher qualquer linguagem de programação, com ou sem frameworks, implementar 4 endpoints de APIs e configurar pra rodar com Docker Compose numa configuração específica de infraestrutura.</p>
<p>Fazer a API em si, em qualquer linguagem, é super simples. É um endpoint POST &ldquo;/pessoas&rdquo; que recebe um JSON simples com dados como apelido, nome, data de nascimento e stack de programação, um array com elementos como &ldquo;php&rdquo; ou &ldquo;java&rdquo;. É como se fosse o cadastro de um sisteminha de recrutamento ou algo assim. Tem que validar que apelido tem no máximo 32 letras, nome no máximo 100 letras, data de nascimento tem que ser minimamente válido. Inclusive ele não fala que 31 de fevereiro tem que ser inválido, só fala que o formato tem que ser ano, tracinho, me, tracinho, dia.</p>
<p>A lista de stack tem que ser um array sem elementos nulos e cada elemento não podia ter mais que 32 letras. E finalmente, o ID tinha que ser um UUID, que é um tipo de número aleatório. Isso foi um bom requerimento como vou mostrar mais pra frente. Mas em termos de validação era só isso. Se passar dados inválidos temos que devolver o cabeçalho HTTP 400 de bad request.</p>
<p>Ou se voltar erro no insert do SQL por causa de constraints inválidas como nome não poder ser nulo, apelido já existir ou algo assim, tem que devolver cabeçalho HTTP 422 de erro unprocessable entity. A resposta pra requisição de criação precisa ter o cabeçalho HTTP 201 created e um cabeçalho &ldquo;Location&rdquo; com o caminho pra essa pessoa recém criada no formato GET &ldquo;/pessoa/uuid&rdquo;. Isso é comum numa API restful básica.</p>
<p>Esse endpoint devolvido no Location deve conseguir encontrar a pessoa recém criada e devolver o JSON com os dados. Se passar um UUID que não existe, tem que devolver o cabeçalho HTTP de 404 not found. Depois precisava ter outro endpoint GET &ldquo;/pessoas&rdquo; que receba um parâmetro chamado &ldquo;t&rdquo; pra ser um &ldquo;termo de procura&rdquo;, pra pesquisarmos o termo no apelido ou nome ou na stack. Seria tipo fazer um SQL com apelido like &ldquo;%termo%&rdquo; OR nome like termo OR stack like termo. Já vou explicar porque esse seria o pior jeito.</p>
<p>Devolvemos o resultado em JSON com código HTTP 200 ou devolve código 400 de erro se não passar termo nenhum. Não encontrando nada bastava devolver um JSON com array vazio em vez de 404 not found. E finalmente o 4o endpoint era um simples GET &ldquo;contagem-erro&rdquo; que deveria fazer um count na tabela e devolver quantas inserções de fato aconteceram. Esse é o único endpoint que não faz parte do stress test e isso pode se tornar um ponto importante pra alguns truques como vou explicar no próximo video.</p>
<p>Como podem ver, são endpoints super triviais. Se você é um programador experiente tenho certeza que já tem de cabeça como faria na sua linguagem mais fluente. Dá pra fazer rapidinho uma versão que funciona em 1 ou 2 horas se estiver um pouco enferrujado. Mesmo se for bem iniciante, seguindo qualquer tutorial de APIs restful no framework que estiver aprendendo, é pra conseguir fazer em 1 dia. Essa é a ordem de grandeza e talvez sirva pra vocês saberem em que pé estão. Vai demorar mais do que isso se quiser experimentar técnicas diferentes. Teve versão que eu levei uns dois dias pra terminar porque resolvei aprender um framework novo do zero, por exemplo.</p>
<p>No próximo video vou demonstrar como dá pra fazer uma versão simples em poucos minutos, por agora só acreditem que dá. Aliás, vamos começar a listar coisas pro próximo episódio. &ldquo;Mostrar como fazer uma versão simples do zero&rdquo;. Mas agora vem a parte que separa os adultos das crianças: a infraestrutura. Não basta só fazer o código e dizer &ldquo;roda na minha máquina&rdquo;. Tem que rodar dentro das restrições da rinha. Então não é só subir um banco de dados, subir sua aplicação sozinha e acabou.</p>
<p>Segundo as regras, tem que subir duas instâncias da sua aplicação, pra rodar em paralelo. E ambos precisam ficar embaixo de um balanceador de carga, no caso pediram o Nginx. No mundo real aplicações rodam com várias instâncias em paralelo embaixo de um balanceador de um Kubernetes, Seasaw, HAProxy ou várias outras alternativas. Azure tem o Application Gateway, AWS tem o ELB, o Elastic Load Balancing, tem o Traefik, F5 Big-IP e mais. Mas NGINX é fácil e simples o suficiente pra este desafio. Não é o mais rápido, mas também não é o mais difícil de usar.</p>
<p>E aqui começa a parte realmente difícil: uma instância de NGINX, uma instância de banco de dados, que pode ser postgres, mysql ou mongodb, mais duas instâncias da sua aplicação sendo balanceadas, precisam caber em 1 CPU e meio e no máximo usar 3 gigabytes de RAM e só. Essa é a regra da rinha. Fazer a API não é o desafio. Essa limitação que é. Mas tudo bem, fazer caber é fácil, o problema mesmo é que no final esse sistema tem que aguentar um stress test, um teste de carga.</p>
<p>Stress test é um teste automatizado que simula como pessoas ou sistemas consumiriam o seu sistema, mandando requisições http pros endpoints da API. No caso eles escolheram a ferramenta Gatling, que tem versão aberta e uma paga com mais recursos. A versão gratuita é razoavelmente simples de usar e o script pro cenário de carga está no GitHub da rinha. Vamos anotar aqui pra eu explicar &ldquo;O Básico de Stress Test&rdquo; no próximo video.</p>
<p>Lembrando que quando a rinha começou acho que os participantes ainda não tinham acesso a esse script de testes, justamente pra não tentar burlar os critérios. Eu não sei se eles sequer tiveram acesso ao script durante a rinha. Imagino que não, só no final. Vamos assumir que ninguém sabia. Isso que diferencia os participantes da rinha de programadores como eu agora, que tem a oportunidade de avaliar sabendo tudo que eles não sabiam. Não comparem o meu resultado com o deles, isso seria injusto.</p>
<p>Em resumo, você podia escolher linguagem, framework e implementar uma aplicação web que serve 4 endpoints de APIs. Tinha que ser o mais leve possível pra aguentar carregar duas instâncias, mais nginx, mais banco, em 1.5 CPUs e 3GB de RAM, orquestrado por docker compose.</p>
<p>Eu ainda não fiz um video explicando o que é um framework web, então vamos anotar aqui pra explicar no próximo video. Eu já tinha feito um video sobre Docker, recomendo que assistam depois, mas aí também no próximo vou explicar como usar o Docker Composer pra subir todos os componentes de um sistema web na sua máquina também.</p>
<p>A idéia é que os participantes deviam empacotar a aplicação numa imagem de Docker, subir no Dockerhub, postar o arquivo de docker compose como pull request no GitHub da rinha e aí os organizadores iam rodar esse docker compose e aplicar algum teste de carga. Quem passasse, ia pro ranking final.</p>
<p>O ranking foi interessante. Teve 90 participantes. Desses, 39 foram desclassificados por diversas razões. Ou o docker compose tinha algum bug e nem carregava, ou algum dos endpoints da API não respondia como deveria, ou deixaram de implementar alguma coisa que tinha nas regras. Sobraram 51 projetos que funcionaram, mais da metade. Um número respeitável. No final, além de implementar os endpoints corretamente, o resultado pro ranking foi baseado na quantidade de inserts que foram feitos no banco de dados.</p>
<p>Ordenando a lista por essa quantidade de inserts, tivemos os Top 10. Quem ganhou a rinha foi o Vinicius Fonseca, com sua implementação usando Rust, com o framework Actix com Tokio. Em segundo lugar ficou o Leo Vargas com sua versão em Go e Fiber, o framework que usa FastHTTP. Em terceiro ficou o André Marra e Albert Kliemke, que fizeram em dupla uma versão em Dotnet 8 com C#. Em quarto ficou a Isadora Souza, que também usou Go com Gin. Em quinto ficou a dupla algebraic, a Sofia e a Gabi, que fizeram em Lean4, a linguagem mais desconhecida da rinha toda. Depois vou falar delas.</p>
<p>Em sexto ficou o Vinicius Santos que também fez em Rust. Em sétimo ficou o Jean Rodrigues que também fez em Go. Em oitavo ficou o Luiz Picanço, com outra versão em Rust. Em nono lugar &ldquo;e&rdquo; também em décimo ficou o Rodrigo Navarro, que participou com duas versões de Rust, uma feita com o framework Axum e outra com o mais leve Touche que, por ironia, ficou depois da versão em Axum. Aliás, parece que ele fez pelo menos uma dessas versões numa live no canal dele, pra quem se interessar corre atrás pra assistir.</p>
<p>Portanto, nos Top 10 ficou assim: 5 versões em Rust, 3 versões em Go, 1 em Lean e 1 em dotnet. Aí as redes sociais foram à loucura. Primeiro, ninguém achou estranho já que isso confirma a hegemonia do Rust e do Go como as linguagens mais rápidas da atualidade. Pior ainda: o fato de dar um dotnet nos Top 10 mas não um Java, fez um monte de gente ficar ridicularizando o Java. Sabe o que falei do torcedor de time neandertal? Sim, afinal na cabeça deles, Java é uma linguagem velha e ultrapassada, certo? Vamos ver como os neandertais leram o resto do ranking.</p>
<p>Seguindo pros próximos 10. Em décimo primeiro ficou o Gabriel Oliveira com uma versão em Elixir. Alguns ficaram tipo, &ldquo;puts, não era o Elixir que se vendia como rápido? Perdeu até pro dotnet?&rdquo; Em décimo segundo ficou o Lucas William com uma versão em C++. &ldquo;Ué, C++ não era pra ser o mais rápido? Rust e Go são foda mesmo, muito melhor que C++&rdquo;. Em décimo terceiro ficou o Lucas Weis com uma versão em Node.js com Express. &ldquo;Pois é, bem que me falaram que Javascript não é tudo isso que falam, né?&rdquo;</p>
<p>Em décimo quarto ficou o Yuri Gomes que fez uma versão em Bun, antes do lançamento da versão 1.0 alguns dias atrás. E pois é, Bun é só hype mesmo, nem lançou ainda e já é lento. Em décimo quinto ficou o Thales Maciel que fez em Rust também. Coitado, deve ser um júnior que né, não é culpa do Rust. Em décimo sexto ficou um cara de apelido saiintbrisson, que não deixou arquivo de README pra trás com o nome nem links, então nem fui tentar procurar.</p>
<p>Em décimo sétimo ficou o Lauro Appelt com sua versão em PHP. Finalmente uma linguagem que se associa mais com Web clássica. Quem diria, PHP ainda não morreu, mas tá lá embaixo com um pé na cova né? E só em décimo oitavo ficou o Bruno Borges com a primeira versão em Java da rinha. E só pra piorar a situação do Java, ele ficou encostado no décimo novo que foi o Lázaro Nixon com uma versão em Ruby on Rails e o vigésimo do Leandro Proença que também fez uma versão em Ruby. Peraí, Java tá tão ruim agora que fica encostado em Ruby?? Que fase heim?</p>
<p>Eu queria parar no vigésimo, mas só no vigésimo primeiro lugar tivemos o Ian Cambrea com a primeira versão em Python, usando Sanic, na competição. Pelo visto Python também é só hype né? Não ganhou nem de PHP, nem de Ruby e nem de Java. Espero que vocês que acompanharam não tenham pensado desse jeito, porque seria vergonhoso.</p>
<p>Dentre os 51 projetos enviados, 8 foram feitos em Go, 7 em javascript, 6 em Rust, 6 em Java. 5 em PHP. 4 em dotnet. 3 em Python, 2 em Ruby, 2 em Elixir, 1 em C++, 1 em Lean, e o último lugar foi 1 feito em Bash pelo Leandro. Teve uns 3 no ranking que o README não dizia em que linguagem foi feito e nem o link pro Github então também não fui atrás pra saber.</p>
<p>O Lean da Sofia e Gabi foi o mais inesperado, ainda mais por ter entrado nos Top 10 e o último de Bash do Leandro foi o mais inusitado porque acho nem ele esperaria ganhar com isso, mas quis representar e mandou super bem. Sim, Bash de terminal que vocês usam no Linux como shell. Tem gente que roda Doom numa calculadora, porque não uma aplicação web em Bash? Mas nesse caso, como esperado, Bash não tinha como competir mesmo.</p>
<p>Alguns participaram com mais de uma implementação, como o Leandro que fez em Ruby e Bash, o Lauro Appelt com duas versões em PHP, o Rodrigo Navarro com duas em Rust. Não sei como foi a divulgação do Zanfranchesci, mas ele mandou muito bem. Foi uma participação bem variada e ao meu ver cumpriu exatamente o que eu imaginaria de uma rinha: galos de diferentes tamanhos e cores. Rust e Go na frente, Javascript e Java pra trás. É o que a platéia gostaria de ver. Foi show de bola.</p>
<p>Tendo visto os resultados, se no topo tivesse sido uma hegemonia absoluta de Rust e Go provavelmente eu teria aceitado como todo mundo: sim, talvez nessas restrições de infra, só eles mesmo pra aguentar a carga. Mas como tinha um dotnet ali na terceira posição, e meus parabéns pro André e Albert por isso, mas isso chamou minha atenção: não vejo porque dotnet consegue estar nos Top 10 e outra linguagem não, seja Java ou seja Javascript.</p>
<p>Com essa pulga atrás da orelha, minha saga começa na sexta feira, dia 25 de agosto, eu abri o editor e comecei a fazer minha versão. Tava um pouco enferrujado, então levei acho que umas 2 horas pra implementar em Ruby on Rails mesmo. Levou mais tempo do que deveria porque pensei: &ldquo;hum, se é um teste de carga em inserção, talvez o postgres seja o gargalo. Depois do HTTP POST, o teste vai checar se inseriu pelo ID. Não tem como deixar só em memória porque são duas instâncias isoladas. Como tem balanceador de carga, o normal vai ser a inserção cair numa instância e a pesquisa cair na outra instância&rdquo;.</p>
<p>A conclusão imediata que qualquer um chegaria é: vou colocar um cache compartilhado, talvez memcached. Mas como queria já lidar com o que achei que fosse o gargalo, pensei, &ldquo;vou jogar jobs em fila de redis pra inserir em background&rdquo;. Então esquece memcache, vou usar redis tanto como cache quanto fila de jobs. Deve caber nas restrições de RAM porque aí o postgres deve trabalhar um pouco menos e eu divido os recursos.</p>
<p>Pelo que avaliei depois, não fui só eu que pensei assim. Várias das versões fizeram a mesma coisa: usaram redis ou nats como cache e fila de jobs assíncronos. É uma coisa que todos nós, que já temos cicatrizes de guerra, fazemos meio sem pensar e isso é um sério problema. Não existem soluções automáticas. Cada caso é um caso e precisa ser medido antes de implementar qualquer truque. Cache ou filas não deixam nada rápido automaticamente em todo caso. Eles também custam recursos. De novo, vamos deixar anotado aqui pra eu explicar no próximo video, como é essa estratégia de usar jobs e filas pra inserir e porque cache não fazia diferença aqui.</p>
<p>Pelo mesmo motivo achei que fosse ver mais gente usando MongoDB, assumindo que muitos imaginam que é mais rápido, mas do total de 90 projetos só 7 tentaram. E mesmo MySQL só 2 tentaram. A maioria esmagadora acreditou no Postgres por padrão. A conclusão: coisas relativamente fáceis, como adicionar caches e filas, todo mundo faz na frente. Coisas mais complicadas, como mudar o paradigma de armazenamento, aí deixamos pra depois. Eu também escolhei ir de Postgres.</p>
<p>Sendo eu criador de conteúdo que, por 20 anos venho evangelizando coisas como uso de Postgres, cache e jobs assíncronos com Redis, me deu uma ponta de dor na consciência. Mesmo aqui no canal já fiz um video chamado &ldquo;Tornando sua App Web Mais Rápida&rdquo;, onde faço justamente essas recomendações. Eu tento explicar em que casos usar, mas acho que dá pra explicar isso em mais detalhes, por isso deixei anotado pra voltar nesse assunto no próximo video.</p>
<p>Quando terminei minha versão de Rails, com cache, jobs e tudo mais, rodei o stress test. Já imaginava que poderia não ser rápido, mas sempre tem aquela esperança de &ldquo;vai que&rdquo; né? E pra minha decepção não deu mais que uns 15 mil inserts, abaixo ainda das versões de Rails do Lazaro e do Leandro. Foi quando comecei a postar tudo que ia descobrindo no Twitter. Esse foi meu primeiro tweet.</p>
<p>Olha que situação ridícula. Quem gera esses relatórios é a própria ferramenta Gatling, que roda o teste de carga. No próximo video vou mostrar como ele funciona. Nesse gráfico interessa a barrinha verde, olha como tá lá embaixo e a vermelha tá lá em cima. É a quantidade de knockouts ou KOs, de requisições que não conseguiram ser processadas e foram perdidas. Em resumo, só entenda que queremos que verde seja grande e vermelho seja pequeno ou idealmente, zero. Nesse momento eu pensei: &ldquo;é, talvez essa carga seja muito pesada pra uma linguagem interpretada como Ruby numa infra de docker tão apertada&rdquo;.</p>
<p>Resolvi baixar a versão do Leandro que usa Ruby mas sem framework pesado como o Rails. E os resultados dele foram bem melhores que os meus, mas ainda assim tem uma barra vermelha considerável de knockouts. Eu conseguia 15 mil inserts e a versão dele batia acima de 30 mil inserts, na minha máquina. Na minha cabeça essa discrepância fazia sentido. Imagino que façam na de vocês também, afinal quanto menos coisa tiver, mais rápido deveria ficar.</p>
<p>Só pra testar os limites, tirei a restrição de 1.5 CPU e 3GB de RAM e configurei o docker compose pra usar 24 CPUs e 20GB de RAM, afinal meu PC aguenta muito mais que isso. Assim a barrinha verde de fato sobe, mas olha que ainda tem uma barrinha vermelha. Eu ainda estou perdendo requisições, e pára pra pensar, isso não faz sentido, em nenhuma linguagem.</p>
<p>Nesse ponto as coisas não estavam fazendo muito sentido. Bacana, eu sei que Rust e Go são rápidos, afinal são compilados e tals, mas se o problema fosse só falta de recurso, na minha máquina, que é absurdamente potente, não deveria estar dando nenhum knockout. Isso foi aumentando a pulga atrás da minha orelha. A conclusão nunca pode ser &ldquo;a linguagem é uma bosta&rdquo;, a primeira conclusão sempre tem que ser &ldquo;eu estou fazendo alguma coisa errada&rdquo; e ir atrás de descobrir o que.</p>
<p>Resolvi tentar uma outra versão, aproveitar o domingo, dia 27 de agosto. Fazia algum tempo que queria desenferrujar meu Crystal e aprender o framework web chamado Lucky. Ótima desculpa. Deixei a documentação do site aberta de um lado, editor do outro e fui fazendo tentativas. Minha premissa é que Crystal, sendo uma linguagem que compila binário nativo, como Go, deveria ser bem mais rápido que minha versão em Rails. No final gastei o domingo inteiro mais a segunda feira seguinte pra fazer, porque nunca tinha usado Lucky antes.</p>
<p>Vamos anotar isso também pro próximo video: falar um pouco mais das minhas experiências mexendo em linguagens diferentes, mas pra hoje só precisa entender que os criadores do Crystal, o Ary e o Brian, quiseram inventar uma linguagem com sintaxe o mais próximo possível de Ruby, adicionando coisas modernas como inferência de tipos, fibers, channels, mas com perfil de performance similar de Go, e em alguns casos até de Rust.</p>
<p>Foi nesse ponto que fui vendo outros desenvolvedores na comunidade começarem a esmiuçar mais os critérios do ranking, em particular o MrPowerGamerBR. Sendo javeiro ele foi um dos que ficaram sofrendo bullying do povo tirando sarro do Java ter se saído tão mau no ranking. Em vez de só sair dando reply xingando todo mundo, como todo twiteiro faz, ele fez o que um programador de verdade deveria fazer: abrir o código e provar com números.</p>
<p>Ele fez a mesma coisa que eu: começou uma nova versão na linguagem que mais gosta: Kotlin. Pra quem não sabe, Kotlin foi inventado pela JetBrains, a empresa que faz a IDE IntelliJ e o Android Studio. Eu também gosto dela e vou deixar anotado pra comentar a respeito no próximo video. Pra hoje, basta entender que é compatível com Java e tecnicamente oferece a mesma performance.</p>
<p>Dia 28 de agosto ele postou essa versão, que não só melhora a posição de Java, mas iguala na mesma posição Top 1 da versão de Rust do Vinicius Fonseca. Mais do que isso, ele estudou mais a fundo o script de stress test em Gatling e determinou que existe um número máximo de requisições e de inserts possível durante os 3 minutos fixos do teste: 46 mil quinhentos e setenta e seis inserts.</p>
<p>Na verdade vai ser 46 mil quinhentos ou seissentos e alguma coisa porque no script de stress test do Gatling tem essa chamada pra &ldquo;randomized&rdquo; que afeta a quantidade de requisições pra criação de registros. Se rodar o teste várias vezes, vai dar números diferentes. Imagino que o Zanfranchesci fez isso de propósito pra adicionar o fator de &ldquo;sorte&rdquo; no espírito de uma rinha. Portanto o resultado numérico nunca foi pensado pra ser totalmente justo. E antes que os haters apareçam de novo: eu estou super ok com isso.</p>
<p>Mais do que isso, o MrPowerGamerBR olhou com mais calma a versão de Rust do Vinicius. Sim, ele estava inconformado, e nada é mais motivador do que a força do ódio. Ele notou que surgiam inconsistências esquisitas. Às vezes o total dava 300 a mais do que o máximo. Às vezes dava 500 a mais. Como isso poderia ser possível? Não tem como aparecer requisições do nada. Se existe um máximo e passa do máximo, obviamente existe um bug.</p>
<p>E de fato tem. Não foi de propósito, a gente esquece bugs. Por isso sempre falamos: não é porque uma linguagem compila, não é porque uma linguagem tem checagem de tipos, que não precisa de testes. No código do Vinicius tem uma rotina de aquecimento que insere mais de 500 registros falsos, não sabemos porque, mas depois tem um código que deleta isso. Só que esse código tem uma race condition, uma condição de corrida. Um erro clássico em programação concorrente. Ou seja, às vezes a rotina de limpeza roda antes de terminar de inserir tudo, aí sobra registros e isso polui o total no final.</p>
<p>Não chega a ser um problema, porque mesmo se corrigir esse bug, ainda assim a versão dele atinge o máximo, então sempre seria o primeiro. Mas estudando o stress test, estudando o código do Vinicius e também o do Lucas Weis que fez em Node.js, o MrPowerGamer aprendeu que existem truques que poderiam ser usados que a maioria não pensou. Não foram só eles, mas alguns pensaram além do óbvio e conseguiram resultados melhores.</p>
<p>Em 10 de setembro, o MrPowerGamerBR soltou um video detalhando essas descobertas e recomendo que assistam depois. Eu vou explicar tudo que ele falou e muito mais no próximo video. Uma dessas técnicas foi bulk inserts, a outra técnica foi gerar um índice especial pra pesquisas. Vamos deixar anotado aqui pro próximo video: explicar o que são bulk inserts e explicar como fazer pesquisas fuzzy no postgres e os segredos de ajustes de performance.</p>
<p>Se consertar o bug de race condition do código do Vinicius e se tirar o fator de aleatoriedade do stress test, o ranking fica bem diferente. Como o MrPowerGamer mostra nos seus tweets, o primeiro lugar seria compartilhado entre a versão de Rust do Vinicius, que continua sendo o primeiro, mas a versão de Go do Leonardo Vargas também sobe pra primeiro assim como o 6o lugar da Isadora e o 9o lugar do Navarro também virariam primeiro lugar.</p>
<p>Por causa disso o terceiro lugar de dotnet do André e Albert pula pro segundo lugar. Eles raspam no máximo mas não ultrapassam por meros sessenta e cinco inserts. O quinto lugar do Luiz Picanço em Rust sobe pro terceiro lugar. O Navarro, que tinha feito duas implementações em Rust, também sobe a outra versão, do 10o pra quarto. O oitavo lugar do Jean Rodrigues, em Go, sobe pra quinto lugar. Todo mundo vai subindo no ranking.</p>
<p>Mais ainda: sabendo o número máximo de inserts, se alguma versão ultrapassar, sabemos que tem bug nas validações. E isso aconteceu com o quarto lugar em Lean4 da Sofia e da Gabi, e a que ficou em sétimo lugar do Vinicius Santos, em Rust. Ambos ultrapassam os 48 mil e quinhentos e tanto, mas como tinha aquele fator de aleatoriedade no script, na rodada oficial, passaram batido, mas deveriam ter sido desclassificados. E que fique claro que não foi de propósito, que atire a primeira pedra quem nunca deixou um bug pra trás. Como já expliquei, numa rinha tem fator sorte, e está ok.</p>
<p>O problema é que o script de stress test, e o critério de ranking, não tinham sido divulgados desde o começo e nem testam tudo que está nas instruções. Por exemplo, eles deveriam checar as constraints da DDL do SQL que cria a tabela e índices pra garantir que coisas como apelido tem índice de unicidade, que o campo de data de nascimento era realmente de tipo data e coisas assim. Se fizer um campo de nascimento sendo um varchar, um string, e simplesmente inserir o que vier sem checar, no final vai ter mais registros do que deveria.</p>
<p>Se fosse pra ser mais rígido, deveriam ter implementado uma rotina de testes automatizados em Cypress, por exemplo, uma suite de testes de aceitação que testam todas as regras que estão nas instruções. Assim seria possível ver se a aplicação está devolvendo os códigos de erro quando enviamos dados inválidos. Sem isso, vários bugs passam despercebidos. Fica a dica pra uma próxima rinha. E também pra projetos de verdade: sem testes, tudo compila, tudo roda, mas bugs de validação vão passar despercebidos, em todas as linguagens, compiladas, tipadas, ou não.</p>
<p>Vou repetir: não encarem como uma crítica à rinha, o próprio nome &ldquo;rinha&rdquo; é pra ser uma brincadeira. Inclusive, eu acho que se tivesse um teste de aceitação mais rígido assim, teria muito menos participantes e muito mais projetos desclassificados, e meio que perderia a graça.</p>
<p>Mas o que me motivou a gastar tanto tempo nisso é que só olhando o ranking oficial, sem todo o contexto, eu também estava aceitando que Rails ou Java iam ser lentos mesmo e bola pra frente. Mas à medida que fui explorando o que o Vinicius, o Lucas, o Leandro, o MrPowerGamerBR, fizeram nas versões deles, resolvi copiar na minha versão de Crystal, pra tirar a prova dos nove. Não era pra ser difícil. Na minha cabeça, coisa de poucas horas.</p>
<p>Demorei um tanto pra entender o framework Lucky, o Avram que é o ORM que fala com o banco, tive até que abrir o código fonte deles pra entender. Na dúvida, na falta de documentação ou stackoverflow, nunca penso duas vezes e vou ler direto na fonte. Vantagem de usar projetos de código aberto é que o código é aberto, sacaram? É pra ler. E eu fiquei empolgado em tentar implementar o sistema de cache com job assíncrono pra fazer bulk insert como o Vinicius fez em Rust. Sendo Crystal compilado, era pra ter performance similar, né?</p>
<p>Ledo engano. Assim que terminei e fui testar com o Gatling, a decepção. Lembra a barrinha vermelha? Olha ela aqui firme e forte. Minha implementação nem entraria nos Top 20. Meros 20 mil inserts. Só que diferente do que tava pensando sobre Ruby, que é lento por ser interpretado e não tinha o que fazer, ou Java que come muita RAM e não tinha o que fazer, aqui eu fiquei meio puto. A culpa não podia ser do Crystal, nem do Lucky, tinha que ser minha. Eu estava fazendo alguma coisa muito errada. Será que eu sou uma farsa? Será que eu sou tão ruim assim?</p>
<p>Quando gastamos um tempo tentando descobrir um problema num código e depois de &ldquo;ver tudo&rdquo;, ou achar que viu tudo, ainda assim não encontrar, eu sempre tenho uma saída que funciona. Olhem neste código de Crystal. Pra passar o stress test, basta devolver HTTP 201 created e um Location válido pra próxima requisição. Até tinha deixado comentado aqui esse código de teste aqui. Posso descomentar essas duas linhas e comentar todas as de baixo. É isso mesmo, na dúvida, tire tudo e faça a resposta mais simples e idiota, devolver uma constante.</p>
<p>Mas esta Location não vai existir, porque se não inserir nada, não existe ninguém com ID número 1. Então, no show.cr, também tinha deixado esta linha comentada, que sequer tenta fazer a pesquisa no banco, só devolve outra constante. Descomento e comento o resto. Pro teste de consulta passar certo, basta devolver HTTP 200 de OK. Finalmente, na pesquisa por termos, mesma coisa, descomento esta linha pra devolver código 200 e comento o resto.</p>
<p>Só fazendo isso o teste de carga já passa. Esta vai ser a velocidade máxima  desse app, sem processar nada no banco, manjam? Estou removendo uma das variáveis de incerteza. Rodando o Gatling, só esperar 3 minutos e esse é o resultado. Tudo verde, nenhum vermelho de knockout, e olhem aqui do lado, todos os tempos de resposta da ordem de 1 milissegundo. Na verdade é menos de 1 milissegundo, mas ele arredonda. Esse é o tempo perfeito, porque a aplicação não tá fazendo praticamente nada. Ótimo, isso mostra como o Crystal sozinho é rápido.</p>
<p>Agora posso ir voltando meu código, uma linha de cada vez. Salvo, subo o docker-compose, que vai recompilar a imagem, e rodo o Gatling de novo. Checo e vejo se a diferença no resultado condiz com o código extra que coloquei. Volto, descomento mais uma linha, salvo, reinicio docker-compose, testo de novo, meço. E vou fazendo isso, uma linha de cada vez. E como esperado, nenhuma dessas coisas extras deu nada de estranho.</p>
<p>Até eu chegar nesta linha, a que monta a URL de Location. A única coisa que isso vai fazer é gerar uma linha como esta aqui em cima, um http://localhost etc. Claro, este método faz mais do que só concatenar strings, serve pra montar todo tipo de URL complexa que quiser incluindo encoding de parâmetros e tudo mais. Imagina aquelas URLs gigantes da Amazon. Mas eu nunca esperaria isso causar qualquer diferença significativa.</p>
<p>Na realidade, isso tá até errado. Pensei melhor e Location na realidade só precisa devolver o path, só a parte de &ldquo;/pessoas/id&rdquo;. Não precisa do protocolo nem domínio a menos que eu queira redirecionar o cliente consumindo a API pra outro domínio diferente, que não é o caso. Substituindo o método .url pra .path e testando de novo, batata, agora o resultado melhorou.</p>
<p>Eu twitei sobre isso, daí uma conta da comunidade Crystal retwitou e um dos desenvolvedores do framework Lucky, o Jeremy, ficou interessado. Ele foi tão legal que abriu uma issue no projeto. Fui lá explicar esses detalhes. Ele já tinha uma suspeita em mente e me pediu pra testar um ajuste. Este foi meu post no ticket aberto. Quando testei sem o patch dele, tava tomando knockouts, mas colocando o patch, caiu de mais de 600 knockouts pra menos de 10, então tinha alguma regressão de performance mesmo.</p>
<p>Não sei explicar exatamente o motivo ainda. A versão anterior parece que só concatenava strings, e o patch usa tipo um string builder. Concatenar gera duplicatas da string. Um Builder pré-aloca espaço e vai só adicionando as novas partes sem duplicar. É um trade off de CPU por RAM. Eu sei que isso faz alguma diferença, mas não sei ainda explicar porque faria diferença aqui. Mesmo tendo, não deveria ser significativo. Mas de qualquer forma, no número de stress test do Gatling fez muita diferença.</p>
<p>Sem querer, acabei achando um gargalo de performance no framework que levou a um conserto. É assim que a gente acaba contribuindo em projetos open source: sem querer. Mas essa não era a única otimização. Olhando outras versões notei que ninguém tava devolvendo mensagens de erro completas, como &ldquo;apelido já existe&rdquo; ou &ldquo;formato de data inválida&rdquo;. As instruções só falam que precisa devolver código de status 400 ou 422.</p>
<p>Na minha versão, antes, além do código eu devolvia um JSON com o erro. E processar JSON também custa tempo. Tirei todos os métodos de render ou json e usei só o método &ldquo;head&rdquo; que, como o nome diz, devolve só o cabeçalho, o código de erro. É uma coisa besta, que em testes individuais não faria diferença, mas o  Gatling manda centena de milhares de requisições. Aí parece que faz diferença.</p>
<p>Mesma coisa logs. O teste não exige logs. No mundo real precisamos ter mensagens de erro gravadas e retornadas pra ficar mais fácil achar bugs e defeitos, mas pra este desafio de performance que só dura 3 minutos, podemos desligar todos os logs e ganhar alguns milissegundos importantes. E fazendo pequenos ajustes assim, o resultado de inserts de Crystal foi subindo. Dos pífios 15 mil inserts, foi pra 30 mil, daí pra quase 40 mil inserts.</p>
<p>Não bati no máximo mas chegando perto dos 40 mil inserts, já foi um salto muito bom. Quando postei esse resultado já era dia 1o de setembro. Ainda me distraí no processo porque queria uma biblioteca pra auxiliar o caching em Redis mas não tinha nenhuma bem feito. Esbarrei num bem antigo chamado Kiwi que ninguém atualizava fazia tempo, então resolvi arrumar e melhorar o código dela. E essa foi minha segunda pequena contribuição open source.</p>
<p>Enquanto isso, em paralelo, o Leandro e o Lazaro, das versões de Ruby mínimo e de Ruby on Rails, continuaram mexendo nas versões deles. O lázaro que, no ranking oficial da rinha, tinha ficado lá na 19a posição com uma faixa de 24 mil inserts, também saiu ajustando as configurações dele e com muito pouco ajuste já tinha conseguido bater os 40 mil inserts também. O Leandro também. Com esses resultados eles já teriam entrado nos Top 10.</p>
<p>O Leandro experimentou mais com as configurações de postgres e de nginx. Muitos de nós achávamos que esses dois componentes não eram tão importantes. Pior, achávamos que eles não ajudavam em nada, então fizemos outra coisa feia que muitos fazem sem pensar: colocamos nginx pra aceitar o máximo de conexões quanto possível aumentando o número de worker connections lá pra cima de 10 mil. E no caso do Postgres a gente configurava os pools de conexão pra dezenas ou centenas de conexões. E isso é um enorme erro.</p>
<p>Vamos anotar pra explicar no próximo video, mas deixa eu tentar ilustrar aqui. Pra ter uma visão na cabeça vamos imaginar uma lanchonete, com vários caixas tirando pedido e mandando pros cozinheiros atrás. Os caixas são nossa aplicação, feitos em qualquer das nossas linguagens. Os cozinheiros são as conexões do banco de dados que é a cozinha.</p>
<p>O nginx é como o tamanho da porta da lanchonete. Se fizer uma porta que aguenta passar 100 mil pessoas de uma só vez, o que acontece? O stress test começa com poucas pessoas, umas 3, mas ao longo de 3 minutos, dispara 600 simultaneamente. Só que essas 600 não são atendidas ao mesmo tempo. Não tem caixa suficiente, daí vai formar um amontoado de gente desordenado.</p>
<p>Do lado da cozinha, o banco de dados, cada cozinheiro extra exige pelo menos uns 3 MB de RAM de espaço e vai consumir CPU. Mas a cozinha, é limitada. Lembra da regra da rinha? No máximo 1.5 CPU. Não adianta aumentar cozinheiro infinitamente, que a cozinha não aguenta. Se falarmos pros caixas tirarem quantos pedidos conseguirem, rapidamente esses pedidos vão ficar empilhados porque não tem cozinheiro suficiente pra atender.</p>
<p>A vontade é aumentar os cozinheiros, o pool de conexões. Digamos, temos duas instâncias da aplicação, os caixas, e cada um vai poder tirar até 100 pedidos de uma só vez. Então 200 pedidos. Mas não tem como ter 400 cozinheiros. Primeiro porque eles ocupam espaço, 3 mega cada já vai dar no mínimo 1.2 gigabytes de RAM. Mas beleza, o estabelecimento inteiro aguenta 3 gigabytes que é o limite imposto pela rinha lembra? Mas não tem fogão suficiente pra cozinhar, a cozinha tá apertada, temos menos de 1 CPU, cada cozinheiro vai demorar mais e mais tempo pra atender cada pedido. Entenderam?</p>
<p>Então o cenário é assim: a porta do estabelecimento permite entrar todo mundo de uma só vez, mil pessoas, 10 mil pessoas, entra tudo. Só tem 2 caixas, duas instâncias. Mesmo tirando pedidos rápido, fica tudo acumulado esperando. Não adianta contratar mais cozinheiro, porque a cozinha é limitada, e quanto mais cozinheiros tentamos colocar, mais cada cozinheiro demora. E enquanto o pedido não é atendido, a pessoa fica lá pendurada na frente do caixa esperando. E assim, vai acumulando centenas de pessoas na lanchonete esperando, até começarem a perder a paciência e ir embora. São as requisições perdidas ou knockouts.</p>
<p>A solução que o Leandro publicou não é intuitiva. Em vez de fazer o nginx aceitar quantas pessoas puder, fazemos diferente: limitamos o tamanho da porta. Digamos, no máximo 1000 pessoas. O NGINX coloca um sistema de senhas do lado de fora e vai deixando entrar só 1000 de cada vez, de forma mais ordenada. É igual num Poupa Tempo ou no seu banco. Pega a senha, senta e espera ser chamado. Agora os caixas atendem menos pessoas de uma vez, mais ordenado, e tiram menos pedidos de uma só vez, digamos, no máximo 50 em vez de 100 ou 200. Esse é o pool de conexões.</p>
<p>E na cozinha, em vez de precisarmos de 300 ou 400 cozinheiros, agora precisamos só de 100. Mas aí não vai andar mais devagar? Não, porque sobra mais espaço na cozinha e cada cozinheiro consegue cozinhar mais rápido. É melhor 100 cozinheiros terminando os pratos em 1 minuto ou 400 cozinheiros levando 10 minutos cada porque tão disputando fogão? Entenderam?</p>
<p>Eu estava cometendo esse erro também. Tava deixando nginx lá em cima, com portão gigante pra suportar 10 mil conexões e compensando com 200 conexões no pool de cada instância do Rails. E pra piorar ainda estava cometendo um erro no postgres. Eu tinha esta linha no docker-compose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>volumes: - ./postgresql.conf:/docker-entrypoint-initdb.d/postgresql.conf</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso diz pro Docker Compose pegar esse arquivo postgresql.conf do meu diretório local e copiar pra dentro do container de postgres. Mas tava faltando modificar o comando de inicialização pra ter &ldquo;-c&rdquo; com o nome desse arquivo. É nesse arquivo que digo pro postgres que ele deve suportar, digamos, um máximo de 400 conexões.</p>
<p>Como não tava carregando o arquivo, subia com o limite padrão de 100 conexões. E isso confunde, porque eu pensava que ia conseguir abrir mais de 100, mas não ia. E não sabia porque. Esse foi o primeiro motivo de porque meus resultados tavam lá na casa dos 15 mil inserts: nesse caso faltava cozinheiro e eu achava que tinha suficiente. Falta de atenção que chama isso. Em vez de consertar no lugar certo, eu ficava tentando compensar em outros lugares, como no NGINX.</p>
<p>No próximo episódio vou explicar como eu poderia ter evitado essa confusão. Vamos deixar anotado aqui. Depois que descobri isso, e li o tweet do Leandro, reconfigurei o nginx pra faixa de 1000 workers de conexão e depois diminuí o número de cozinheiros no pool de conexões pra uns 40. O problema nem era o máximo de conexões, era a compensação excessiva que tava fazendo no NGINX. Um erro levando a outro erro.</p>
<p>À medida que ia diminuindo esses dois números, passava a aumentar a quantidade de inserts no final, a quantidade de pessoas atendidas de fato. Eu tava achando que minha versão de Rails, pelo fato de ter exagerado na solução e enfiado Sidekiq pra jobs, Redis pra fila e cache, ia faltar espaço na cozinha, mas não foi isso, foi questão de gerenciar o fluxo de pessoas.</p>
<p>Limitar a porta e os caixas em vez de escancarar. Lembrem-se disso: atendimento não é tentar atender todo mundo de uma só vez, é atender grupos organizados de cada vez. É uma questão de vazão e fluxo. Isso é essencial em operações. Seja atendimento numa lanchonete, seja numa linha de fábrica, seja num servidor web.</p>
<p>E foi assim que em 3 de setembro, postei o resultado da minha versão de Rails. Faixa de 39 mil inserts, quase batendo os 40 mil que queria. O cenário que descrevi de gerenciar o fluxo de pessoas na lanchonete é um caso clássico, que demonstra que o problema não é a capacidade de atendimento do caixa, ou de cozinhar dos cozinheiros: é de gerenciar o fluxo de requisições, das pessoas ou sessões. É quando sabemos que o problema principal é gerenciar I/O, entrada e saída.</p>
<p>Nós que somos mais experientes já deveríamos ter parado pra pensar nisso primeiro, mas assumimos errado no começo e essa hipótese errada nos fez perder muito tempo. Assumimos que o problema seria a cozinha, o banco de dados. Aí tentamos compensar aumentando número de cozinheiros ou cozinheiros mais rápidos que seriam os caches. Seria o caso se o problema fosse CPU bound, limitação de CPU. Mas na realidade era problema de fluxo, de entrada e saída, no fluxo dos caixas, o problema era I/O bound, como a maioria dos problemas de Web comum.</p>
<p>Uma vez controlado o fluxo, podemos otimizar o caixa pra em vez de só tirar o pedido e ficar esperando de braços cruzados esperando; enquanto o cozinheiro tá ocupado, o caixa pode fazer outras coisas na frente, como receber o pagamento, pegar a bandeja, separar o refrigerante, limpar o balcão. Não precisa esperar o prato chegar pra depois fazer tudo isso, dá pra fazer em paralelo. É pra isso que servem threads ou os async/await de várias linguagens, como Javascript no Node.js.</p>
<p>Nesse ponto eu já tinha visto o MrPowerGamer e outros colaboradores como o Bruno Borges conseguindo subir Kotlin e Java pro mesmo nível do Rust. Já vi o Leandro e o Lazaro subindo Ruby e Rails pra perto do mesmo nível, faixa dos 40 mil inserts. Eu mesmo já tinha conseguido subir Crystal e Rails. Foi quando minha intuição dizia que a maioria dos participantes, se tivessem conhecimento de tudo que a gente descobriu até agora, seriam capazes de chegar perto desses 40 mil inserts ou até bater o máximo dos 46 mil e quinhentos do Rust do Vinicius.</p>
<p>Alguns não gostaram de ouvir isso, porque agora aquele bullying contra o Java ou a fanboyisse de Rust perderiam valor. E como eu detesto dizer uma coisa sem ter provado antes, resolvi fazer a única coisa racional: demonstrar na prática. Ainda era a mesma segunda-feira, 4 de setembro. Escolhi começar aleatoriamente mexendo na versão em Node.js e Express do Lukas Weis que já tava perto do topo, na faixa de 34 mil inserts, em 13o lugar no ranking oficial. Na minha máquina tava dando abaixo disso, uns 27 mil.</p>
<p>A versão dele foi uma das que mexi mais. Como foi um código feito pra competição, ele e muitos outros não se preocuparam em seguir boas práticas, clean code, nem nada disso, só cuspiram código até funcionar e publicaram. Não tem absolutamente nada de errado nisso e não considerem esses códigos como representação do que eles conseguem fazer com mais tempo, em projeto de verdade. Isso tem que ser repetido o tempo todo: código pra competição é diferente de código de verdade.</p>
<p>Mas como eu estava com tempo e não era pra participar da competição, aproveitei pra desenferrujar meu Javascript e fui refatorar um pouco o código pra ficar mais organizado e mais legível. Meu objetivo era totalmente diferente da rinha: dar uma limpada e otimizar o suficiente pra bater pelo menos a faixa dos 40 mil inserts, o que elevaria o projeto pros Top 5 oficiais.</p>
<p>Recomendo que leiam meu pull request com calma, deixei o link na descrição do video abaixo, mas em resumo, depois de organizar o código comecei tirando fora a otimização prematura de usar Redis como cache. Como falei antes, o problema não era a velocidade dos cozinheiros, o postgres. Portanto a solução não era tentar achar cozinheiro mais rápido, um Redis. Com a configuração adequada do fluxo de nginx, do pool por postgres, dividindo CPU e RAM melhor no Docker Compose, já resolvia. Arranquei o Redis fora, daí teria mais recursos pro postgres.</p>
<p>Esse cache, além de tudo, não estava sendo efetivo porque o Lucas tentava cachear os resultados do endpoint de pesquisa por termos, só que o stress test manda pesquisas com termos aleatórios, raramente repetidos, então o cache nunca ia ajudar, porque toda pesquisa ia ser nova e não ia ter o resultado no cache. Portanto cache na pesquisa de termos era inútil, só um peso a mais de gravar no cache.</p>
<p>Além disso, no endpoint de criar novo registro, ele deixava o banco de dados criar a chave primária, daí recebia de volta no final e só aí gravava no cache. Isso tem que acontecer um atrás do outro, não dá pra ser assíncrono. Adicionar o valor no cache só aumentava o tempo da requisição. Mais uma vez, era um trabalho extra sem benefício.</p>
<p>No endpoint seguinte, de pesquisar pessoa direto por ID, ele buscava no cache. Mas a pesquisa por chave primária no postgres é igualmente rápido. De novo, o cache não fazia diferença. Só faria sentido se primeiro gerar a chave primária, com uma biblioteca de UUID na aplicação, e mandar o insert pro banco já com essa ID e em paralelo já mandar gravar no cache também com a mesma ID. Como não precisa esperar o banco devolver uma nova ID, não precisamos esperar pra gravar no cache, entenderam? É uma das vantagens de usar um gerador de chaves primárias externo ao banco. Muito iniciante não sabe que não é obrigatório deixar o banco responsável por IDs.</p>
<p>O Lucas também tava devolvendo códigos de erro HTTP junto com mensagens em JSON descrevendo o erro, mas a rinha não se incomodava com isso, então só arranquei fora. O stress test não liga pra mensagem, só quer respostas rápidas. No mundo real, o certo é dar respostas completas, porque sem logs bem descritos depois fica difícil achar bugs e problemas. Novamente, é uma otimização que só serve pra uma competição.</p>
<p>O Node.js tem recurso de cluster, subir forks do processo pra escalar melhor. Mesma coisa que Rails também faz. O Lucas parece que experimentou, mas desistiu. Eu ia tirar fora, mas voltei e passei a usar pra tentar distribuir um pouco mais a carga. Ter um reactor com corrotinas assíncronas e conseguir ter concorrência é diferente de ter paralelismo e o ideal é ter as duas coisas. Eu explico isso no video de concorrência e paralelismo e no próximo video talvez eu tente explicar melhor neste contexto. Vamos deixar anotado.</p>
<p>A parte que possivelmente pesou mais na versão do Lucas foi o endpoint de pesquisa que faz select LIKE na tabela. Muitos erraram nisso: precisa fazer select LIKE nas colunas apelido, nome e stack, então é apelido LIKE termo OR nome LIKE termo OR stack like termo, e esse é o pior jeito de fazer select. O melhor jeito é pré-criar um único campo com as três colunas concatenados durante o insert. Daí fazer um índice usando GIST com a extensão de trigrams. Alguns tentaram GIN, mas pra este desafio o melhor era GIST. Já tínhamos anotado, mas vou explicar isso no próximo video.</p>
<p>A segunda coisa foram as validações. Validar os dados antes de inserir no banco costuma ser mais rápido do que tentar inserir e deixar o banco devolver erro. Por exemplo, que a data de nascimento é inválido, tipo mês 13 ou dia 31 em fevereiro. Podemos só checar se a string tem o formato de ano, tracinho, mês, tracinho dia, no mínimo. Ou tentar converter essa string num objeto válido de data pra garantir. Eu não medi exatamente esse ponto, mas o Lucas usou a biblioteca moment.js, que antigamente já foi bem popular mas acho que é considerada meio obsoleta.</p>
<p>O jeito mais popular hoje é usar outra biblioteca, a date-fns ou simplesmente usar o construtor da classe Date do Javascript nativo. Eu comecei tentando usar o date-fns mas achei mais fácil só checar se o parse da string pra um new Date is not a number (isNaN). Com todas essas mexidas fui mexer no Docker Compose. Ele tava subindo 3 instâncias de Node no Docker, mas como reativei o código de Node Cluster, desci pra 2, cada uma com 1 fork extra, totalizando 4 processos.</p>
<p>Diminuindo a carga do código de cache, que não tava servindo pra nada. Melhorando as pesquisas por termo. Diminuindo a carga do código de validação. Ou seja, fazendo nossos caixas trabalhar menos, em seguida era o problema de diminuir a montanha de gente que o nginx tava deixando entrar tudo de uma só vez. Diminuí a quantidade de workers do nginx dos absurdos 20 mil, pra 10 mil até 1024. Notei que uns 1000 é ideal pra maioria das aplicações. Muitos até menos.</p>
<p>Fazendo isso os caixas trabalham de forma mais organizada e eficiente, precisando de menos cozinheiros, menos conexões de banco no pool. O Lucas tinha configurado com 200, mas agora dava pra descer pra uns 45, talvez até menos. E fazendo tudo isso, essa versão de Node.js saiu da faixa de 27 mil inserts, na minha máquina, ou 34 mil que conseguiu na máquina dos organizadores da rinha, e pulou pra faixa dos 40 mil também. Do meu ponto de vista, foi uma mudança significativa. E cimentou na minha cabeça que dava pra fazer a mesma coisa pra outras versões.</p>
<p>Desculpas ao Lucas por ter pego ele pra Judas, mas não entendam errado. Vários outros, incluindo eu mesmo, cometemos os mesmos erros. Só aproveitei pra explicar mais na versão dele pra não me repetir em todos os próximos. Enfim, ainda no dia 4 de setembro, depois de atingir os 40 mil inserts com o Node.js, resolvi mexer em outro velho conhecido, Elixir. Na minha cabeça não fazia sentido Elixir ter se saído abaixo dos Top 10. Mas eu fiquei surpreso com um problema: essa versão do Gabriel Oliveira se recusava a carregar na minha máquina!</p>
<p>Como chegou a figurar no ranking oficial, significa que o Zan conseguiu rodar pra avaliar. Portanto, se a versão é a mesma (e tem que ser, é a mesma imagem de Docker), por exclusão, tem que ser alguma coisa na minha máquina. Mas toda vez que eu tentava subir o Docker Compose, via as instâncias da app crasheando sem nenhuma mensagem de erro, simplesmente morriam. Puf, zero, nada.</p>
<p>Sem entender o motivo, fiz um teste: mudei o comando do docker compose pra só subir um  &ldquo;sleep infinity&rdquo; do Linux. Todo mundo conhece o conceito de sleep, certo? Pausa o processamento pelo tempo que mandar. Linux suporta infinity, daí fica pausado pra sempre, como um loop infinito, while true da vida. Fazendo isso o container fica de pé pra eu poder fuçar. Daí é possível usar o comando docker-compose exec pra abrir um bash dentro do container.</p>
<p>De dentro, tentei executar o binário do app na mão, e de fato, crasheava. Achei isso muito estranho, aí me ocorreu, será que o app é muito pesado? Vou tentar carregar o iex que é só a linha de comando do REPL do Elixir, sem carregar nada da aplicação. E crasheava também. Estranhíssimo. Vamos pular o Elixir e carregar direto só o Erlang, o comando &ldquo;erl&rdquo;. E crasheava! Bizarro! A sensação que eu fiquei é que os programas pediam mais recurso do que a máquina conseguia entregar e crasheavam, como falta de RAM.</p>
<p>Resolvi editar o arquivo de docker compose pra tirar a limitação da rinha de CPU e RAM. Tentando rodar de novo, aí subia! Por alguma razão tava faltando RAM. Abri o HTOP pra ver quanto de RAM tava usando e era um absurdo, coisa de quase 1 gigabyte e meio! Fiquei bem confuso. Como fazia tempo que não mexia com Elixir fiquei pensando, &ldquo;ué, será que nos últimos anos eles enfiaram tanta coisa no Elixir que agora precisa de mais memória que uma JVM do Java?&rdquo;. É confuso porque quem já mexeu com web de Java, com Spring e tudo, sabe que 1GB de RAM não é fora do comum. Mas não esperava isso do Elixir.</p>
<p>Eu tava quase jogando a toalha, então resolvi reportar no meu Twitter. Todo mundo achou estranho, mas o próprio Gabriel conseguiu achar a resposta, era o limite máximo de porta que o Erlang detectava no meu sistema. Em Erlang e portanto também em Elixir, portas são usadas pra se conectar com o mundo externo. Por exemplo, pra ler do standard input, STDIN, ou escrever no standard output, STDOUT, são portas. Pra abrir arquivos, pra receber conexões de rede, são portas.</p>
<p>Erlang pré-aloca no mínimo 1024 portas. Pra determinar o máximo, podemos abrir o shell &ldquo;erl&rdquo; e rodar o comando <code>erlang:system_info(port_limit).</code> E olha só: meu sistema devolve mais de 1 milhão de portas. Daí tem o detalhe que ainda não entendi: na aplicação em Elixir parece que tenta pré-alocar esse máximo sei lá porque, em vez de ir só alocando se precisar. Daí dá mais de 1 gigabyte de RAM. E se limito dentro das restrições da rinha, iria faltar memória no container, crasheando na inicialização.</p>
<p>Felizmente, com uma simples variável de ambiente <code>ERL_MAX_PORTS</code>, podemos colocar um número mais sano, tipo 2048 ou algo assim. Limitando de 1 milhão pra 2 mil, fez uma puta diferença, agora o app sobe, consumindo na faixa de 160 megabytes por instância, que é bem mais razoável e próximo até de versões como Rails ou Node.</p>
<p>Isso é um problema que só aconteceu no meu sistema porque a configuração de Arch Linux é mais agressiva nos limites do que um Debian ou Ubuntu, que são mais conservadores. Sendo mais específico, isso é limite de file descriptors, ulimit do sistema, depois pesquisem sobre isso. De qualquer forma, a versão Elixir do Gabriel eu gostei bastante porque usou características exclusivas de Elixir. Ele fez as duas instâncias rodando em containers diferentes se conectarem num cluster.</p>
<p>Clusters saem de graça em Erlang, porque a máquina virtual já trás toda a infraestrutura pronta. É fácil de usar. Não serve pra tudo, mas quando precisa é bem útil. Lembram como falei que não valia a pena usar um cache como Redis, primeiro porque o postgres já era rápido o suficiente, mas também porque subir um container de Redis significa dar menos recursos pro postgres? Com o recurso de cluster do Erlang, dá pra ter um cache compartilhado entre as duas instâncias, sem precisar de um Redis entre os dois.</p>
<p>O app em Elixir consegue consultar na memória da primeira instância, e a segunda instância no outro container, consegue pedir pra primeira instância porque estão no mesmo cluster. Assim não precisam de uma memória externa na forma de um Redis da vida. À primeira vista isso parece mais esperto, mas como já disse antes, o problema não é o postgres, e adicionar um sistema de cache só adiciona peso desnecessário.</p>
<p>Mesmo assim esta versão, ajustando só nginx e o pool de conexões, consegue passar dos 41 mil inserts e entra nos Top 5. Mas tem um porém, além do cache, as instâncias faziam chamadas RPC entre eles, ou seja a 1a instância podia pedir pra 2a instância gravar ou pesquisar no banco e vice versa. Isso eu achei desnecessário e adiciona uma comunicação externa via rede pra toda chamada.</p>
<p>Eu não tive tempo de testar, acho que se arrancar fora essas chamadas remotas, o código de cache e dar mais recursos pro postgres, talvez seja possível conseguir um resultado melhor. Mesmo assim eu esperava que Elixir conseguisse um número maior. De todas as versões que eu mexi, essa ainda foi a que performou um pouco pior. Então tem espaço pra simplificar mais o código. Elixir certamente consegue mais. Estão notando o padrão: em vez de adicionar coisa, estou tirando coisas e com isso deixando mais rápido. Por isso sempre repetimos tanto que otimização prematura é a raíz de todo o mal.</p>
<p>Com a versão Elixir do Gabriel batendo acima dos 40 mil inserts, resolvi procurar uma versão em Go Lang. E escolhi um dos que ficou mais embaixo no ranking. A do Luan ficou em 23o lugar com só 21 mil inserts. Assim como Elixir, não tem porque Go ficar lá embaixo e isso já se provou porque pelo menos 3 versões tinham alcançado os Top 10.</p>
<p>Fiz pequenos ajustes no docker compose, pra dar um pouco menos de CPU pras instâncias de app, assumindo que Go sem nenhum framework iria precisar de menos recursos que Elixir. Daí podemos aumentar CPU do postgres. Teve duas modificações de código que quis mexer. Primeiro, fazer erros devolverem só os códigos de erro 400 ou 422 de HTTP sem mensagens de erro. É pouca coisa mas já faz um pouco de diferença.</p>
<p>A segunda coisa é que ele tava usando Regular Expressions pra validar o formato da data de nascimento. Na real, acho que isso é meio besteira, mas como Regex costuma ser mais lento do que dar split na string e checar cada componente, troquei também. Acho que nenhuma dessas duas coisas faz muita diferença no final. O que deu diferença sim foi o que já falei antes. Primeiro, diminuir o tamanho da porta de entrada: diminuir os 10 mil workers de nginx pra 1024. E aumentar o pool de conexões, embora isso também nem tenha sido necessário.</p>
<p>Só de limitar e controlar o fluxo de entrada de requisições, literalmente uma linha, já disparou o resultado da faixa de 29 mil inserts pra 34 mil. Não chegou nos 40 mil que eu queria, mas por hora resolvi subir meu pull request, que também tem link na descrição abaixo, e pular pra outra coisa. Só pra terminar esse dia 4 de setembro, fui dar uma fuçada na tal versão em dotnet do André e Albert que alcançaram terceiro lugar.</p>
<p>Como muitos outros, eles também optaram por fazer uma versão codificada de forma rápida, sem muitas boas práticas, então ficou bem verbose, tudo super longo. A curiosidade é que como muitos,  também optaram por implementar um cache, mas diferente da maioria, em vez de Redis escolheram usar Nats.</p>
<p>Nats deveria ser mais usado, e parece que povo de Dotnet usa mais. É uma alternativa até mais confiável do que Redis pras funções de cache ou como uma fila de jobs simples. Em cima do nats, usaram os recursos de async do C# com channels pra fazer a implementação de bulk insert. E com isso conseguiram atingir quase o máximo, 44 mil inserts. Esta versão não mexi em nada, só fucei e tweetei a respeito.</p>
<p>No dia seguinte, dia 5 de setembro, me sugeriram fuçar alguma versão de PHP então puxei pra rodar a versão do Lauro Appelt que ficou em 17o lugar no ranking, com meros 25 mil inserts. Fazia muitos anos que não mexia com PHP, então precisei da ajuda dos universitários.</p>
<p>Pacotes de PHP hoje em dia se instalam com Composer, que é parecido com NPM de Javascript ou Cargo de Rust. Foram-se os dias de baixar zip de sites como Freshmeat ou SourceForge. Mas PHP ainda tem o conceito de extensions, que como o nome diz, carrega junto com o PHP em si, e modificam as estruturas internas da linguagem.</p>
<p>Antigamente lembro de usar PEAR que eram as extensions em C, mas hoje parece que são PECL. Nesse estágio precisava carregar essa nova extension chamada Swoole, que adiciona funcionalidades de corrotinas e modelo de I/O não-bloqueante, o que transforma o PHP em algo parecido com Node.js. Basta baixar, compilar e instalar a extension e editar o php.ini pra carregar ela na inicialização.</p>
<p>Depois de bater cabeça pra entender isso e instalar os pacotes, pude rodar essa versão do Lauro, que ele implementou com o framework Hyperf. PHP 7 tem uma sintaxe bem mais moderna do que o antigo PHP 3 que eu usava, mais de 20 anos atrás. Mudou bastante coisa, tá bem mais legível, bem mais organizado. E o Lauro foi um dos poucos que deu uma caprichada e deixou tudo fácil de entender e legível. Recomendo darem uma olhada.</p>
<p>Nessa versão também não mexi em nada no código, só parei pra entender, desenferrujar um pouco do meu PHP. A única coisa que ajustei foi o pool de conexões. Aqui, ao contrário da maioria, o Lauro foi super conservador e limitou o pool de conexões com 10 conexões, muito pouco. Resolvi soltar tudo e jogar lá pra cima, em 500, que é exagerado. Mas fazendo isso e rodando o stress test, essa versão também subiu pra quase 40 mil inserts, o que também o colocaria lá pelos Top 5.</p>
<p>Por causa disso já me dei por satisfeito por enquanto, já que queria pular pra ver outras versões. Nesse dia não mexi em muita coisa. Dei uma pausa e a noite resolvi que ia mexer mais no dia seguinte, mas acabei não conseguindo dormir. Levantei e voltei pro PC. Queria mexer em pelo menos mais uma versão, a de Python. Escolhi o 21o lugar do Ian Cambrea, com menos de 24 mil inserts.</p>
<p>Ele não fez em Django, que seria o mais comum pra web em geral, nem em FastAPI que parece ser o mais comum pra APIs. Ele escolheu Sanic, que é um framework que eu não conhecia. A única modificação significativa que eu fiz foi notar que ele também resolveu usar Redis pra cache, mas não implementou um pool de conexões pro Redis. Tava usando uma única conexão pra tudo.</p>
<p>Não consigo afirmar que era só isso, mas testando na minha máquina como os outros, o resultado bateu nos 40 mil inserts também. De curiosidade, essa foi a única versão que eu vi que usou NGINX diferente. Normalmente a gente configura NGINX pra fazer balanceamento de carga usando protocolo HTTP, portanto usando rede TCP por baixo, fazendo proxy reverso pra outro servidor web HTTP atrás, no caso o Sanic.</p>
<p>Mas NGINX, e outros serviços que rodam em Linux, como o próprio Postgres, que também costumamos conectar via TCP na porta 5432, suportam conexões usando UNIX Sockets. Se tudo for rodar na mesma máquina, que não é comum no mundo real, não tem necessidade de usar rede TCP/IP. Tá tudo na mesma máquina. Podemos fazer um processo se comunicar diretamente com outro processo usando IPC ou inter-process communication, e um dos jeitos de fazer isso é via UNIX Sockets.</p>
<p>De forma resumida, sabe quando fazemos tipo um cat num arquivo, adicionamos um Pipe, e rodamos um grep do outro lado pra filtrar o conteúdo do arquivo? Isso é um jeito do processo grep se comunicar com o processo cat via esse pipe que liga o stdout do cat ao stdin do grep. É mais ou menos isso que acontece com o NGINX usando unix sockets pra falar com as instâncias de Sanic. Basta todos enxergarem os mesmos arquivos .sock. Depois dêem uma olhada no repositório do Ian pra ver como ele fez.</p>
<p>Como a versão de Python também foi fácil e não exigiu nenhum trabalho meu além de ajustes triviais, fui dormir e no dia seguinte resolvi olhar pro topo da lista de novo. Fiquei curioso com isso de Lean4 que a Sofia e a Gabi usaram e alcançaram o Top 4 do ranking. Nunca tinha ouvido falar nessa linguagem, mas o que vi foi impressionante.</p>
<p>Lean4 é uma linguagem experimental ainda, longe de ser 1.0, criada na Microsoft Research pelo brasileiro Leonardo de Moura em 2013. O objetivo dele foi criar uma linguagem pra ser fácil criar provas de teoremas. É uma linguagem mais focada em formalidades matemáticas, inspirada em linguagens funcionais como Haskell ou OCaML. Ele tem um cheiro de dialeto de linguagem da família ML. Se curte linguagens funcionais e formalidade matemática, essa pode ser uma boa alternativa.</p>
<p>Mais impressionante foi a dupla Algebraic, as autoras do projeto da rinha, a Sofia que tem só 21 anos e a Gabi que tem só 17 ou 18 anos. Como falei desde o começo, implementar esses endpoints de API, em si, não é nenhum desafio. Mas neste caso foi. Lean4 é experimental, comunidade pequena, e quase nada pra Web, porque nunca foi o foco. Ela é quase toda focada em teoremas matemáticos e não criar APIs pra Web.</p>
<p>Pra fazer APIs, elas precisavam de um parser de JSON, já que na regra da rinha, o stress test ia mandar milhares de requisições com JSON. Mas não tinha uma biblioteca de JSON, então a Sofia fez um do zero. Quando se recebe as requisições HTTP, algumas contendo esses JSON, precisa conseguir parsear o pacote. Existe já parser de HTTP em C, que todo mundo usa em várias linguagens, mas pra Lean4 não tinha. Então a sofia fez um wrapper em FFI pra integrar.</p>
<p>Pra implementar a aplicação em si, ela queria usar algum framework web, como em Node você tem Express ou em Java tem Spring. Mas em Lean4 não tinha, então a Sofia fez um pequeno do zero.  Além disso, pra rodar uma aplicação web, duh, precisa de um servidor Web. Sabe? Tipo Node.js, tipo um web2py, ou um Tomcat. E claro, também não tinha. E claro, a Sofia fez um. Ou seja, esse foi aquele caso de &ldquo;não sabendo que era impossível, foi lá e fez&rdquo;.</p>
<p>Só esse esforço já merece uma menção honrosa, porque eu acho que ninguém teve metade do trabalho que elas tiveram. Mais foda ainda porque essa versão foi uma das mais performáticas. Também é igualmente impressionante que uma linguagem desconhecida, experimental e que provavelmente ninguém nunca usou pra web, seja tão performática.</p>
<p>O criador, Leonardo, focou em fazer uma linguagem com objetivo de provar teoremas e verificação formal em vez de performance bruta. Mesmo assim foi bem sucedido em performance. Não deixem de olhar o código da rinha no repositório da Gabi, além das bibliotecas no repositório da Sofia. Links abaixo.</p>
<p>No video de análise do MrPowerGamerBR, onde ele descobriu os limites do stress test e das regras da rinha, também conseguiu identificar que algumas das versões nos Top 10 tinham bugs, como de validação, que acabaram deixando passar inserts que não deveriam, em particular por falta de validação de data.</p>
<p>Essa versão em Lean4 tinha esse tipo de bug. Elas esqueceram de colocar validação da data de nascimento. Tudo bem, mesmo com a validação, a versão delas ainda ia ficar entre os Top 10, por pouco. Então aproveitei pra tentar molhar meus dedos em Lean4 pela primeira vez e implementei uma validação simples. Foi o código menos intuitivo que eu fiz em muito tempo. Acredito que quem for mais experiente em linguagens como Haskell ou F# devem conseguir entender mais fácil.</p>
<p>Tinha outro pequeno bugzinho na hora de gerar a URL pro cabeçalho de Location depois de inserir o registro no banco, então corrigi isso também. De qualquer forma, vejam o link dos meus pull requests na seção de links abaixo. No fim desse dia postei um tweet de resumo até este momento com tudo que descobri. Neste estágio da minha saga, já tinha conseguido fazer minha própria versão de Rails e de Crystal bater os 40 mil inserts, ajustei a versão em Node do Lucas, Elixir do Gabriel, Go do Luan, PHP do Lauro, Python do Ian, além dos ajustes no Lean da Sofia e Gabi.</p>
<p>Chegamos no dia 6 de setembro, véspera de feriado prolongado de Independência e que eu tinha planejado viajar&hellip; Depois de ter batido cabeça com Lean4, resolvi que queria bater cabeça com outra linguagem que nunca mexi antes. Tentei fuçar Zig, mas achei que tava experimental demais, o ferramental super instável, na verdade, tudo instável. Coisas da versão 0.10.0 quebram na 0.12.0. Pra uma experiência de um dia só, não ia compensar. Eu acho que é uma das linguagens mais promissoras, tanto que foi usada pra fazer o novo Bun, mas vou deixar pra outra ocasião.</p>
<p>Então lembrei que sempre quis tentar a linguagem Nim, outra que é compilada como Go, tem uma sintaxe inspirada em Python, existe já faz alguns anos então assumi que seria mais estável e melhor documentada do que Zig. Provavelmente fácil de chegar numa boa performance, assim como consegui com Crystal. Ledo engano. Apanhei bastante, a começar pela falta de documentação.</p>
<p>Não ajudou nada que a biblioteca assíncrona pra Postgres se chama asyncpg, que é o mesmo nome da de Python. Quando pesquisa no Google, adivinha quem aparece primeiro? Pra piorar, a linguagem e suas bibliotecas usam e abusam de macros, que modificam a sintaxe da linguagem. Sem ter uma boa experiência, foi confuso diferenciar o que era da linguagem e o que eram macros. As mensagens de erro do compilador confundiam mais do que ajudavam. Reclamações de amador, claro, não estou dizendo que é um defeito, mas é uma curva de aprendizado maior pra iniciantes.</p>
<p>Recursos de corrotinas, tipos como Option, pareciam ainda meio experimentais, com hacks pra funcionar certas sintaxes. Escolhi o framework web Jesper ,que parece um Sinatra de Ruby. Apanhei um bocado mas consegui implementar os 4 endpoints numa manhã. Só que contrário às minhas expectativas, o resultado foi pífio, uma verdadeira droga mesmo.</p>
<p>Já tentei compilar com opção speed, nada. Já tentei compilar com modo multithread, nada. Suspeito que a biblioteca de async pool que estou usando é imatura e vaza conexões. Depois preciso investigar isso melhor. Mesmo aumentando pool pra 100 ou mais, nem chega até o fim do stress test, abre o bico muito antes. Male male insere 2000 inserts. Isso deixaria esta versão lá no fim do ranking oficial, só ganhando da versão de brincadeira feita em Bash do Leandro.</p>
<p>A probabilidade maior é que eu, sendo amador, posso ter feito erros de iniciantes e por isso foi ruim. Mas na maioria das linguagens mais maduras, um iniciante teria dificuldade de deixar tão lento assim, mesmo se fosse num Javascript. Se tiver alguém assistindo que tem experiência com Nim, não deixe de dar uma olhada no meu pull request linkado abaixo pra ver se descobre o problema, mas no estado que tá agora, eu não recomendaria usar Nim pra uma aplicação web, mesmo se fosse bem simples. Se for pra usar uma linguagem imatura com bibliotecas instáveis e mau testadas, é melhor ir direto pra Zig ou V Lang que são mais modernas.</p>
<p>Apesar dessa tentativa frustrada, outros desenvolvedores acompanhando minha saga começaram a se mexer pra fazer versões melhores. Por exemplo, nesse mesmo dia vi o Junior Leão mostrando uma outra versão em Java com Spring Boot, usando Redis pra cache com JDBC direto e também conseguiu alcançar os 40 mil inserts.</p>
<p>Outra tentativa bem sucedida foi do Carlos Silva, conhecido como Insalubre, que tinha participado com uma versão em V Lang, mas tinha algum problema e ele foi desclassificado, então nem entrou no ranking oficial. V Lang é uma nova linguagem com cara de Go. Ele se inspirou nas discussões de técnicas que ficamos fazendo no Twitter, consertou a versão dele, e atingiu os 46 mil inserts.</p>
<p>O Leandro continuou mexendo na sua versão de Ruby e neste dia   reportou que conseguiu bater os 46 mil inserts. Ele foi quem mais testou a hipótese de diminuir a porta de entrada do NGINX e controlar a vazão de requisições. Com só 256 workers do NGINX e 30 conexões totais de Postgres no pool, conseguiu alcançar os 46 mil inserts, demonstrando que nem o nginx e nem o postgres nunca foram os gargalos desta rinha. Veja o fio do tweet dele nos links abaixo pra mais detalhes.</p>
<p>No fim do dia resumi num tweet todas as técnicas que descobrimos até agora. Por exemplo, validação pode consumir  tempo precioso, quanto mais otimizar, melhor. Não precisa devolver mensagens de erro elaboradas, muito menos em JSON, só o cabeçalho HTTP com o código de erro tá otimo e vai ser mais rápido. O postgres não é o gargalo, não com essa quantidade mixuruca de dados. 50 mil inserts não é nem um peido. Por isso não precisa tentar otimizar prematuramente com Redis pra cache.</p>
<p>Linguagens que escalam com threads ou forks precisam de mais conexões com o banco. Um exclusivo pra cada thread. Então tem que dar um pouco mais de RAM pro banco subir essas conexões extras. Cada conexão de Postgres custa caro, uns 2 a 3 megabytes, porque são forks. Então 100 conexões exigem no mínimo 300 megabytes. E ainda tem que sobrar RAM pra processar as queries. Ou seja, menos de 500 megabytes é inviável. Linguagens com suporte a fibers exigem um pouco menos conexões, porque compartilham a mesma conexão na mesma thread. De novo, tá anotado já pra eu explicar isso no próximo episódio.</p>
<p>Uma coisa que eu e o MrPowerGamerBR vimos na implementação vencedora em Rust do Vinicius Fonseca, é que ele entendeu mas nem todo mundo pensou na forma correta de lidar com searches fuzzy do Postgres. Também já anotamos pra eu explicar no próximo episódio. O Vinicius fez certinho. Ao tornar o search mais eficiente, sobra mais CPU pro postgres conseguir lidar com os inserts.</p>
<p>E por fim, o mais importante, é lembrar do conceito de controlar a vazão na entrada, diminuindo quantidade de workers de NGINX. Esse nunca foi o gargalo, com meio CPU e quase nada de RAM,  aguenta todas as requisições do stress test sem reclamar. Alguns não gostam de NGINX porque algo usando FastHTTP de Go é muito mais eficiente, mas nesta rinha o NGINX nem chega perto de ser gargalo.</p>
<p>Colocando 0.15 CPU e uns 200 megabytes pro NGINX, perto de 0.75 CPU e pelo menos 1.2 GB de RAM pro Postgres, daria pra colocar um pool exagerado de mais de 200 conexões, embora os 100 padrão no total seja suficiente pra maioria.
O que sobrar, dá pra dividir com sua aplicação, ou seja, 0.3 CPU e até 0.8 GB de RAM pra cada instância.
Depende se sua aplicação for mais pesada que isso, como as versões em Java ou Rails ou mesmo Node, aí precisa ir tirando e testando de 0.05 em 0.05 do postgres, dando pra aplicação e medindo pra ver os limites.</p>
<p>E chegamos no feriadão de 7 de setembro. Apesar de eu estar coçando pra investigar mais códigos da rinha, tive que parar por alguns dias. Eu corri pra tentar desvendar tudo antes do feriado, e apesar de ter conseguido ajustar várias versões pra chegar nos 40 mil inserts, ainda tava coçando a cabeça com o elusivo máximo de 46 mil. Tinha algumas suspeitas pra testar quando voltasse, mas fui viajar com essa pulga atrás da orelha ainda.</p>
<p>Já tinha planejado de levar minha namorada pra conhecer Gramado, no Rio Grande do Sul. Nos divertimos bastante apesar de estar super cheio e com tempo meio duvidoso. Visitamos diversas atrações, como a Super Carros, onde pude fazer test driver de Ferrari e Nissam GT-R! Mas, este video não é de turismo. Quem me acompanhou no Instagram viu as stories em tempo real. Se não acompanhou e ficou curioso, deixei um destaque lá. Procurem o link na descrição abaixo.</p>
<p>Mesmo durante o feriado, vários desenvolvedores continuaram empenhados em desvendar os mistérios da Rinha e na sexta feira, dia 8 de setembro, outro dos participantes, o Vinicius Ferraz, fez umaa descoberta muito importante. Ele twitou assim: &ldquo;você que participou da rinha, tomou um j.i.IOException: Premature Close? Tunou o banco, sua app, o nginx e não conseguiu resolver 100%? Sabe onde poderia estar o problema? Na rede do docker. Como resolver? network_mode: host em todos os serviços.</p>
<p>Caraca!!! Nas primeiras versões que mexi, na minha em Rails, em Crystal, a de Node, onde gastei mais horas, chegava perto dos 40 mil, 39 mil, o comportamento ficava estranho. Olhando no gráfico não tinha muita explicação. Ficava esse tremido esquisito no fim do gráfico, como se estivesse tentando ultrapassar um teto invisível.</p>
<p>Mesmo nas versões vencedoras, como o Rust do Vinicius, dava pra ver um tremido menor no fim. Mas ele conseguia ir aguentar até o fim. Independente da versão, mais cedo ou mais tarde, aparecia isso. Eram requisições que começavam a se perder! Não perdia tudo nos inserts, mas principalmente nas pesquisas por termo, até em pesquisas inválidas, o que era mais absurdo, já que  só checa se o parâmetro de termo não existe e devolve cabeçalho de erro. Esse endpoint nunca tinha que dar knockout, porque não processa nada.</p>
<p>Como teve aquele caso do Elixir do Gabriel, que descobrimos que meu sistema Manjaro baseado em ArchLinux tem ulimit maior e por isso afetava o uso de memória do Erlang, imaginei que tinha alguma coisa na minha infra que também pudesse estar afetando isso. Cheguei a suspeitar do Podman, que eu estava usando no lugar do Docker. Mas não deu tempo pra investigar isso porque chegou o feriadão e fui obrigado a parar.</p>
<p>E era mesmo no Docker, e o mesmo problema acontece no Podman. Explicando. Quando subimos container de Docker, não é uma máquina virtual. Eu expliquei isso em detalhes no meu video de Docker, então assistam lá depois. O processo roda nativo no sistema operacional host. Só que a infra do RunC de Linux, que é a base do Docker e do Podman, nos dá opção de enganar o processo rodando. Uma dessas formas é quando mapeamos volumes. Dizemos que um diretório dentro do container na verdade mapeia pra outro diretório do lado de fora, no host.</p>
<p>O mesmo vale pra rede. Digamos que eu queira subir vários postgres na mesma máquina. Não ia conseguir, porque o primeiro ia subir e dar bind na porta 5432. O segundo ia tentar dar bind na mesma porta, mas como já tá ocupado, ia falhar. Com containers, cada processo consegue dar bind na porta 5432 de uma rede falsa, virtual. Ele acha que tá sozinho na máquina. Daí podemos mapear a porta interna do container pra uma porta na rede de verdade do lado de fora.</p>
<p>Pra isso precisamos de uma ponte de rede, um &ldquo;bridge&rdquo;, que é o modo de rede ou network mode padrão. Mas podemos escolher fazer bind direto na porta da rede de verdade, do host. É isso que o Vinicius sugeriu: network_mode: host. Ao não usar o modo ponte, estamos retirando o gargalo da tradução de tráfego da rede virtual pra rede de verdade. Isso custa recursos, é mais pesado. Não existe almoço grátis. Tudo custa alguma coisa. O que a gente não sabia é que era pesado a ponto de afetar esse stress test.</p>
<p>Sem querer, esse era o elo perdido da rinha, a última peça que faltava. E só pra me deixar ainda mais ansioso pra voltar do feriadão, no domingo dia 10 o desenvolvedor Reinaldo resolveu fazer uma versão nova em Java usando Quarkus, Reactive e VertX seguindo as dicas do Bruno Borges e Vinicius Ferraz. Depois vejam o repositório dele, o usuário é zsantana. Ele twitou como conseguiu uma versão que atingia mais de 51 mil inserts.</p>
<p>Mas já falamos que o máximo é 46 mil e quinhentos, portanto certamente esta versão tem bug de validações, como eu demonstrei na versão em Lean4 da Sofia. Mais importante, esta versão em Java atingia um critério que a rinha não media mas que pra mim era importante e eu passei os últimos dias tentando entender porque ninguém atingia: Zero Knockouts. Não perder nenhuma requisição. Esse é o ponto onde vencemos o stress test.</p>
<p>Entenderam? Até este momento a noção era que o stress test era tão pesado que nenhuma aplicação conseguia segurar tudo e eventualmente ia perder algumas requisições. Alguns mais, alguns menos. Isso é comum em testes de carga. Mas assim como nginx e postgres nunca foram o gargalo, a carga também nunca foi tão pesada assim. Quando o Zan foi testar os participantes pra encontrar os finalistas, o teste era ainda mais leve.</p>
<p>Ele dobrou o teste, senão todo mundo ia passar. Mas mesmo dobrando, ainda não é pesado o suficiente. Todos os teste que fiz até agora foi com essa versão já dobrada. É importante ter essas ordens de grandeza em mente. Vamos deixar anotado porque preciso explicar isso pra vocês, o que significa sessões simultâneas, tempo de resposta, throughput.</p>
<p>Faixa de 50 mil linhas numa tabela é super leve pra um banco de dados como Postgres. Só vamos sentir que ele tá pedindo água quando atingirmos faixa de milhões de linhas e isso se estiver sem índices adequados. 600 sessões fazendo requisições, não é pesado. Mesmo com a restrição de menos de 1 CPU no Docker Compose. 1000 sessões simultâneas continua não sendo pesado.</p>
<p>A viagem de volta de Gramado foi cansativa. Varamos noite no domingo pra segunda. Estava de carro alugado e saímos quase 2h da manhã de Gramado pra devolver o carro às 4h em Porto Alegre e pegar o vôo de volta pra Congonhas, em São Paulo às 6h. Pegamos táxi perto das 9h da manhã. Antes das 10h chegamos em casa. Eu só pude cochilar rapidinho no avião e quando cheguei já liguei meu PC.</p>
<p>A primeira versão que queria testar foi a de Crystal com Lucky. Troquei o network mode no docker compose pra host e &hellip; foi! Era isso. Bateu 47 mil inserts. Provavelmente algum bug de validação porque ultrapassou o máximo. Zero knockouts. Sabe a tal tremida nos gráficos que falei? Liso, até o fim, uma linha reta em todos os gráficos. Era isso que eu sabia que Crystal tinha capacidade, e confirmamos, a rede do Docker que não estava deixando!</p>
<p>Próxima versão, mexi no meu Rails overengineered com Sidekiq e tudo mais. Network mode host e bateu acima de 45 mil inserts, faltou pouco pra atingir o máximo dos 46 mil e tantos. Não gastei muito tempo ajustando valores no docker composer, mas mesmo assim deu só 4% de knockouts. Antes dava 20%, 30% ou mais. Mas isso prova meu ponto: o gargalo nunca foi nem falta de CPU e nem falta de RAM, nem postgres e nem nginx, era um problema de I/O e isso é gerenciável em quase qualquer linguagem. Se Crystal e Ruby batem o máximo, qualquer outra deveria conseguir.</p>
<p>Peguei a versão de Lean4 e ainda tem algum bug de validação, mas bate acima dos 47 mil inserts. Mais uma versão que vence o stress test sem suar. Peguei a versão em Node.js do Lucas e também bateu no máximo de 46 mil e tantos. E isso foi se repetindo. A versão em Python e Sanic do Ian Cambrea, a versão em V Lang do Carlos, a versão em Go do Luan, a de PHP com Swoole do Lauro, a outra versão em Rails do Lazaro, todos batem o máximo.</p>
<p>De bônus, resolvi testar a versão em C++ do Lucas William que tinha ficado em 12o lugar com menos de 35 mil inserts, também bate perto do máximo agora, 43 mil inserts. Não bateu por algum bug na validação. Enquanto algumas versões faltava validação e deixava inserir mais que o máximo, nesta versão tem alguma coisa filtrando demais e impedindo de gravar mais. Olhando os gráficos claramente podemos ver que tá sobrando recursos. Zero knockouts, não tá perdendo requisições, então tem recursos sobrando.</p>
<p>Nesta segunda-feira, tava todo mundo só falando do lançamento da versão 1.0 do Bun, a nova alternativa a Node.js e Deno que promete ser mais performático. Ainda é cedo pra dizer e ainda nem é totalmente compatível. Mas o Yuri Gomes tinha participado da rinha com a versão pré-lançamento do Bun, 0.8 e tinha ficado em 14o lugar com menos de 28 mil inserts. Mas com network mode em host, ele também bateu o máximo e com tempos similares da versão de Rust. Neste exemplo com escopo pequeno, seu desempenho foi à altura do hype, o que é bem promissor.</p>
<p>Alguns ficaram me cutucando pra mexer em C# também. Eu não tinha mexido em nenhum até agora porque a versão do André e do Albert já provaram que alcançava o máximo. Mas, nada mais justo do que finalizar essa segunda-feira super corrida pós-feriado, do que avaliar a versão do nosso querido organizador da rinha, Zanfranceschi. Por coincidência ele participou com uma versão de Dotnet.</p>
<p>Não vi ele no ranking oficial. Acho que, como organizador ele se absteve de se colocar na disputa, participou pela diversão mesmo. Mas cometeu o mesmo erro que eu e outros: assumimos que postgres ia ser o gargalo e colocamos cache de Redis antes de realmente medir a necessidade. Mas espero que a esta altura tenha ficado claro que postgres nunca foi o gargalo. Não estou dizendo que nunca é, em outros projetos pode ser, mas neste desafio não era. Por isso é caso a caso, precisa medir.</p>
<p>Por causa disso ele implementou um cache em memória usando um ConcurrentDictionary. Lembra aquelas pesquisas com select LIKE que mencionei antes? No Postgres, se indexarmos com GIST e trigrams, fica bem rápido, mas com uma estrutura rudimentar como um dicionário, um hash, ele é obrigado a percorrer 100% do dicionário o tempo todo, extremamente lento.</p>
<p>Isso limitou o resultado a 40 mil inserts. Eu tentei fazer os mesmos ajustes de todo mundo: limitar vazão do nginx, mexer no pool de conexões, dividir melhor recursos do docker composer, colocar em network mode host, mesmo assim não dava. Minha suspeita é que esse ConcurrentDictionary matou esta versão, se alguém quiser tentar tirar e retestar, vá no repositório dele nos links abaixo.</p>
<p>De qualquer forma, com a ajuda de todo mundo, do Leandro, do MrPowerGamer, do Gabriel Oliveira, do Vinicius Ferraz e outros, conseguimos atingir o objetivo não oficial que eu estabeleci pra rinha: vencer o stress test e colocar todas as linguagens no mesmo 1o lugar ou dentro dos Top 3. Fizemos isso com 3 versões de Ruby, Node.js, PHP, Python, Java e Kotlin, Bun, V Lang, Go Lang, Crystal, C++, todo mundo bateu os máximos.</p>
<p>As duas exceções que não bateram foi a de C# do Zan e a de Elixir do Gabriel. Em ambos os casos, acho que a lógica de cache engargalou. Que é lição aprendida: não faça otimização prematura. Em vez de ficar mais rápido, pode ficar mais lento. Se tirar esse cache, provavelmente bateriam os Top 3. E falando em Top 1, agora que consegui cumprir minha missão nesta rinha, só faltava uma última coisa que queria fazer: desenferrujar meu Rust mexendo na versão do ganhador oficial, o Vinicius Fonseca. Finalmente chegamos no 16o dia da minha saga, dia 13 de setembro.</p>
<p>Assim como a versão em C++ do Lucas, ou em Node do outro Lucas, que colocaram tudo num arquivo só. O Vinicius fez a mesma coisa em Rust. Repetindo: é uma rinha, o critério não era o código durar pra sempre, era só aguentar o stress test. Beleza do código, mantenabilidade, legibilidade, segurança, nada disso eram critérios. Eu que fui narigudo e fiquei fuçando e mexendo no código dos outros sem ninguém pedir. Meu objetivo era só desenferrujar meus skills. Uma boa forma de fazer isso era pegar o código dos outros e sair reorganizando.</p>
<p>Essa foi a primeira coisa que fiz no código do Vinicius: refatorar. Rust já não é uma linguagem muito fácil de bater o olho. Cheio de anotações, um monte de .clone() e .unwraps() e tipos e generics pra lá e pra cá. E tudo amontoado. Dividindo em módulos fica mais fácil de bater o olho. Mas eu queria corrigir o bug que o MrPowerGamerBR identificou na análise dele: a condição de corrida que acabava deixando sobrar mais linhas na tabela do que o máximo possível.</p>
<p>Isso acontece porque o Vinicius manda inserir mais de 500 linhas na inicialização pra algum tipo de aquecimento. Que eu saiba o Rust não tem e nem precisa de um just in time compiler, um JIT, já que é pré-compilado em binário. Aquecimento faz mais sentido pra Java, que tem JIT. Enfim, ele deixa essa rotina rodando numa corrotina da biblioteca Tokio, a mais famosa que dá suporte à semântica de Futures, o equivalente a Promises de Node.js.</p>
<p>Na sequência, inicia um segundo Future assíncrono. Essa rotina tem um timer, um sleep de 2 segundos. Quando passa 2 segundos  manda um comando de SQL pra deletar as linhas de aquecimento. É aqui que acontece a condição de corrida. Nada garante que 2 segundos é suficiente pra inserir essas 500 linhas. Muitas vezes o delete acontece antes. Daí sobra linhas na tabela.</p>
<p>Não havia necessidade do delete estar num future separado. Bastava disparar o delete depois do último insert. Ou uma gambiarra seria aumentar esse timer de 2 pra mais segundos. Eu fiz isso. E pra garantir, alterei o último endpoint, o de contagem final da tabela, pra filtrar os registros de aquecimento. Com isso batemos os mesmos 46 mil e tantos inserts, como todo mundo. Depois dêem uma olhada no meu pull request pra conseguir entender melhor essa versão de Rust.</p>
<p>Eu não curto competições de código. Não porque sou contra. Cada um com seu entretenimento. Não tem nada de errado em competir com código. Só não pode achar que código de competição é a mesma coisa que código pra projeto de verdade. As técnicas são diferentes. Os requerimentos são diferentes. Muitos truques que mencionei, não devem ser usadas sem critério, vai dar errado. E nenhuma comparação generalizada deve ser feita entre as diferentes tecnologias usadas.</p>
<p>Uma competição no estilo dessa rinha serve pra mostrar quem foi mais esperto, quem teve mais sorte, quem foi mais cuidadoso. O Zan teve bastante trabalho pra organizar e pra avaliar um a um. Cada rodada do stress test do Gatling custa 3 minutos. 50 projetos que passaram significa que ele gastou quase 3 horas só esperando os testes rodarem, sem contar os outros projetos participantes que tinham bugs. Agora que passou é fácil dizer &ldquo;ah, poderia ter feito X melhor, poderia ter feito Y melhor&rdquo;, mas a posteriori tudo é fácil.</p>
<p>Eu gastei mais de 2 semanas avaliando 18 repositórios em 16 linguagens diferentes. Eu tive a sorte de contar com a contribuição de diversos outros desenvolvedores que se animaram pra investigar junto. Acho que conseguimos desvendar todos os truques possíveis desta rinha, incluindo vencer o stress test do Gatling. Mas isso não é competição, é análise técnica. Não é divertido pra todo mundo, só pra nós mais nerds.</p>
<p>Mesmo se você for torcedor de um Neymar, Messi, Cristiano Ronaldo, seja lá quem acha que é o melhor jogador do mundo, se no dia do jogo eles torcerem o tornozelo e não participarem, o resultado vai ser o que der no dia. Quando perdem, não significa que são ruins no geral. Só significa que perderam aquele jogo, ponto final. É o conjunto da obra que determina os melhores e não um jogo individual. Sorte faz parte do jogo. Mesma coisa nesta rinha. Só porque um Java ou um PHP não apareceram no topo, desta vez, não tem nenhuma correlação com a qualidade deles em outros jogos.</p>
<p>Amador desbocado é quem faz mais barulho nas redes sociais, então meu objetivo foi educacional: demonstrar porque estão errados e fazer todos entenderem que sim: com mais tempo, experiência, skill, qualquer uma das linguagens tinha capacidade de alcançar resultados semelhantes, alguns com mais trabalho, alguns com menos trabalho, alguns gastando mais recursos, alguns menos, não importa. Sua linguagem não te define.</p>
<p>Assistiram meus videos de Sua Linguagem Não é Especial? O que eu falei lá? Um bom desenvolvedor é promíscuo com linguagens. Eu mexo em qualquer uma, a qualquer hora, quando quiser e como quiser. O que eu falei em videos como Aprendendo a Aprender ou Aprendizado na Beira do Caos? Várias das linguagens que mexi nesta saga, ou nunca tinha mexido, como Nim, Lean4, V Lang, ou tava enferrujado fazia anos, como C++, C#, Rust, Go, PHP, Crystal. Mesmo linguagens que tenho um pouco mais de intimidade, como Ruby, Javascript, Python, tive que quebrar a cabeça.</p>
<p>O que eu fiz? Instalei uma a uma e fui fuçar código dos outros. Quebrei elas várias vezes. Consertei, refatorei, reorganizei, fiquei em muita tentativa e erro, reimplementei técnicas diferentes, apliquei técnicas de uma linguagem em outra, li muito código fonte. Mesmo sem nunca ter mexido num framework como Lucky, mesmo Bun que acabou de lançar, não levou mais que algumas horas pra me acostumar. Essa é a diferença: as primeiras linguagens que aprender vai levar meses, anos pra dominar.</p>
<p>As próximas vão ficando exponencialmente mais rápido de pegar, o suficiente pra conseguir fazer o básico. Novamente, demonstrei este ponto na prática. Em um dia já dá pra fazer alguma coisa, se me der poucos dias, consigo me virar em qualquer uma. Em poucas semanas já vou saber truques avançados de cada uma. Eu consigo me tornar produtivo numa linguagem nova em 1 mês. Onde vou levar mais tempo? Em linguagens que tem paradigmas muito diferentes, como Haskell, Ocaml, F#, Clojure ou o Lean4 que vimos hoje.</p>
<p>Com todos esses disclaimers e avisos feitos, ouçam até o final antes de sair comentando. Sim, Rust e Go aparecem no topo do ranking oficial porque, se não cometer erros graves, o tempo de resposta individual de cada requisição vai ser na faixa abaixo de 1 milissegundo, de microssegundos. Frameworks como Rails ou um Laravel da vida vão responder na faixa de milissegundos. Quanto menor o tempo de resposta, mais requisições dá pra responder no mesmo período de tempo. Rust ou Go realmente tem mais velocidade bruta e conseguiram bater a barreira acima dos 40 mil inserts, mesmo com docker em modo ponte, e chegar no máximo durante o evento.</p>
<p>Porque todo mundo não passa a adotar Rust, Go, V Lang, Zig, de uma vez? Tudo compilado em binário nativo super rápido? Vamos colocar assim: não existe um único motivo. Não pensem que as pessoas ou empresas são burras e não sabem que são rápidos. C e C++ existem muito antes da maioria de vocês nascerem e sempre foram rápido. Todo mundo sabe disso.</p>
<p>No caso de um V Lang ou Zig é fácil explicar: é porque são novos demais e instáveis demais, tem suporte baixo, tem ferramental imaturo, mudando o tempo todo e quebrando compatibilidade, falta documentação na forma de livros, cursos justamente porque ainda estão mudando muito. Esses não tem como ser adotados em larga escala. Só early adopters muito dedicados. Só empresas menores e com maior margem de assumir riscos, com um co-fundador ou diretor técnico muito bom pra liderar.</p>
<p>Mas tudo é uma questão econômica. Não me xinguem, eu sou só mensageiro, mas a realidade é que a enorme maioria dos programadores não tem capacidade de aprender a usar linguagens de baixo nível, no nível que seria necessário pra serem produtivos e produzir código bem feito. O fato da maioria das empresas não estar desesperadamente procurando esse perfil é a evidência. Sempre existem exceções, não comece com &ldquo;ah, mas eu conheço fulano ou ciclano&rdquo;. Exceção não define a regra.</p>
<p>Fica a dica: se tornar um programador avançado nas tecnologias mais difíceis, te torna uma mosca branca, que vale muito mais
que os outros. O que eu sempre falo? Tudo que é fácil pra você aprender, também é fácil pra qualquer outro. Quanto mais gente tiver fazendo a mesma coisa, mais barato todos vão ser. O valor está em ser bom naquilo que poucos tem capacidade de conseguir. Onde a barreira é mais alta. E isso vai ser Rust, Go, C++, pra resolver problemas muito mais complicados do que fazer Cruds ou APIs.</p>
<p>Reflitam sobre vocês mesmos, porque estão estudando Java ou C# ou Javascript? Porque são as posições que tem mais vagas. Rust não se vê vagas todos os dias. Estão vendo? É o problema do ovo e da galinha. Você não quer investir tempo pra aprender Rust porque não vê vagas todo dia. E as empresas não oferecem vagas porque não vêem trocentos cursos e estudantes aprendendo. Por isso que esse tipo de movimento, ou começa em startups arrojadas ou dentro de gigantes como uma Microsoft. As vagas existem, bem pouco, e pagam bem, só que são bem mais exigentes. Mas ninguém gosta de vagas difíceis, só querem as que acham fácil.</p>
<p>Não é prático nem realista manter aplicações web complexas e gigantes em linguagens assim. Imagine um Shopify, GitHub, MercadoLivre, Amazon ou iFood da vida, 100% em linguagens de baixo nível. Partes do sistema, que são mais sensíveis a performance, já adotam coisas como Go, mas é a menor parte. Pro uso do dia a dia, dashboard, relatórios, cruds e coisas assim, não compensam. Seria um enorme desperdício dar tarefas consideras commodity como fazer relatórios, pra um tipo de programador raro e caro, com de Go ou Rust.</p>
<p>Adotar uma tecnologia só porque é mais rápida, não é um bom argumento. Hoje em dia todas são rápidas o suficiente. Mesmo linguagens que um programador de Rust chamaria de lentas, como Javascript ou Python, conseguimos atingir patamares similares com um custo um pouco maior de máquina. Como demonstrei aqui, na maioria dos casos, é possível dobrar a performance, triplicar, tendo o conhecimento adequado, sem quase modificar código, nem precisar jogar tudo fora e reescrever tudo em Rust. É isso que programadores como eu fazemos.</p>
<p>No fim dia, dado a escassez de programadores de Rust ou Go e a dificuldade de achar talentos que consigam ser treinados nisso, em pouco tempo, sai bem mais barato compensar com servidores mais parrudos. Alguns pensam que pra dobrar a performance da sua aplicação em PHP, em Java, Dotnet, Python, Node, precisa jogar tudo fora e reescrever em Go. Tá errado.</p>
<p>Só vai garantir que os erros por ignorância que foram cometidos com um PHP, vão se repetir em Go. Programador ruim, vai fazer código ruim em PHP ou em Go. Não existe nenhuma garantia que vai ficar mais rápido se você continua não sabendo porque o anterior tava lento. Por outro lado, por experiência, já melhorei muito sistema de verdade mexendo nos 20% de problemas que dão 80% de retorno. É sempre assim, às vezes são besteiras de pouquíssimas linhas, como as que mostrei durante este video.</p>
<p>Como falei antes, são vários motivos, custo-benefício sendo o principal. No geral o maior problema é que é muito difícil formar bons programadores que consigam tirar vantagem dessas tecnologias. O custo-benefício não compensa. Não significa que essas linguagens não tem chance, significa que esse mercado sempre vai ser de nicho. Mas pra quem tem o talento e a capacidade, é um nicho que paga muito melhor. É só ser bom.</p>
<p>Enfim, hoje eu só quis resumir minha saga de como explorei 16 linguagens, claro que superficialmente, em 16 dias. Não deu pra detalhar cada truque, mas deixamos tudo anotado, e no próximo video vou explicar o truque por trás da mágica: como tudo isso funciona, por que funciona e, mais importante, quando usar e quando não usar. Se ficaram com dúvidas mande nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e não deixem de compartilhar o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>performance</category><category>linguagens-de-programacao</category><category>backend</category><category>akitando</category></item><item><title>[Akitando] #135 - ChatGPT Consegue te Substituir? | Entendendo Jobs Assíncronos</title><link>https://akitaonrails.github.io/2023/01/05/akitando-135-chatgpt-consegue-te-substituir-entendendo-jobs-assincronos/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/01/05/akitando-135-chatgpt-consegue-te-substituir-entendendo-jobs-assincronos/</guid><pubDate>Thu, 05 Jan 2023 11:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/Yl-hlwhj2B0"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Vamos entender mais sobre Jobs Assíncronos, a diferença de usar Promises ou Bull. E um pouco mais sobre o comportamento do famigerado ChatGPT. Vamos ver se ele realmente substitui programadores durante os códigos deste video.&lt;/p&gt;
&lt;h2&gt;Capítulos&lt;span class="hx:absolute hx:-mt-20" id="capítulos"&gt;&lt;/span&gt;
&lt;a href="#cap%c3%adtulos" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;01:42 - Cap 1 - Form de Upload com ChatGPT | Coisas que você não pensa&lt;/li&gt;
&lt;li&gt;04:55 - Cap 2 - Upload Direto pra Cloud | Porque não gravar local&lt;/li&gt;
&lt;li&gt;09:23 - Cap 3 - Ajustando o Front | ChatGPT é júnior?&lt;/li&gt;
&lt;li&gt;11:46 - Cap 4 - Jogando o Back todo dentro do Controller | Como avaliar Back pesado&lt;/li&gt;
&lt;li&gt;18:26 - Cap 5 - Promises, não: Jobs Async | Controlando seu back&lt;/li&gt;
&lt;li&gt;23:52 - Cap 6 - Exemplos Reais de Jobs Async | Google, Anchor&lt;/li&gt;
&lt;li&gt;26:00 - Cap 7 - Precisa de Jobs Async se Java tem Threads? | Apache Kafka&lt;/li&gt;
&lt;li&gt;29:51 - Cap 8 - Qual o nível do ChatGPT | Errando com Convicção&lt;/li&gt;
&lt;li&gt;33:27 - Cap 9 - Quanto custa um ChatGPT? | IA é inacessível&lt;/li&gt;
&lt;li&gt;36:40 - Cap 10 - ChatGPT vai te substituir? | Talvez?&lt;/li&gt;
&lt;li&gt;38:50 - Bloopers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Links&lt;span class="hx:absolute hx:-mt-20" id="links"&gt;&lt;/span&gt;
&lt;a href="#links" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.bullmq.io/guide/connections"target="_blank" rel="noopener"&gt;https://docs.bullmq.io/guide/connections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/bull-ui"target="_blank" rel="noopener"&gt;https://www.npmjs.com/package/bull-ui&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ShaneK/Matador"target="_blank" rel="noopener"&gt;https://github.com/ShaneK/Matador&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.baeldung.com/spring-kafka"target="_blank" rel="noopener"&gt;https://www.baeldung.com/spring-kafka&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/yE-XLLa2AME?t=642"target="_blank" rel="noopener"&gt;https://youtu.be/yE-XLLa2AME?t=642&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/0uQqMxXoNVs?t=551"target="_blank" rel="noopener"&gt;https://youtu.be/0uQqMxXoNVs?t=551&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jdhao.github.io/2022/08/10/http_benchmark_wrk/"target="_blank" rel="noopener"&gt;https://jdhao.github.io/2022/08/10/http_benchmark_wrk/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal, Fabio Akita&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/Yl-hlwhj2B0"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Vamos entender mais sobre Jobs Assíncronos, a diferença de usar Promises ou Bull. E um pouco mais sobre o comportamento do famigerado ChatGPT. Vamos ver se ele realmente substitui programadores durante os códigos deste video.</p>
<h2>Capítulos<span class="hx:absolute hx:-mt-20" id="capítulos"></span>
    <a href="#cap%c3%adtulos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>01:42 - Cap 1 - Form de Upload com ChatGPT | Coisas que você não pensa</li>
<li>04:55 - Cap 2 - Upload Direto pra Cloud | Porque não gravar local</li>
<li>09:23 - Cap 3 - Ajustando o Front | ChatGPT é júnior?</li>
<li>11:46 - Cap 4 - Jogando o Back todo dentro do Controller | Como avaliar Back pesado</li>
<li>18:26 - Cap 5 - Promises, não: Jobs Async | Controlando seu back</li>
<li>23:52 - Cap 6 - Exemplos Reais de Jobs Async | Google, Anchor</li>
<li>26:00 - Cap 7 - Precisa de Jobs Async se Java tem Threads? | Apache Kafka</li>
<li>29:51 - Cap 8 - Qual o nível do ChatGPT | Errando com Convicção</li>
<li>33:27 - Cap 9 - Quanto custa um ChatGPT? | IA é inacessível</li>
<li>36:40 - Cap 10 - ChatGPT vai te substituir? | Talvez?</li>
<li>38:50 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://docs.bullmq.io/guide/connections"target="_blank" rel="noopener">https://docs.bullmq.io/guide/connections</a></li>
<li><a href="https://www.npmjs.com/package/bull-ui"target="_blank" rel="noopener">https://www.npmjs.com/package/bull-ui</a></li>
<li><a href="https://github.com/ShaneK/Matador"target="_blank" rel="noopener">https://github.com/ShaneK/Matador</a></li>
<li><a href="https://www.baeldung.com/spring-kafka"target="_blank" rel="noopener">https://www.baeldung.com/spring-kafka</a></li>
<li><a href="https://youtu.be/yE-XLLa2AME?t=642"target="_blank" rel="noopener">https://youtu.be/yE-XLLa2AME?t=642</a></li>
<li><a href="https://youtu.be/0uQqMxXoNVs?t=551"target="_blank" rel="noopener">https://youtu.be/0uQqMxXoNVs?t=551</a></li>
<li><a href="https://jdhao.github.io/2022/08/10/http_benchmark_wrk/"target="_blank" rel="noopener">https://jdhao.github.io/2022/08/10/http_benchmark_wrk/</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Todo mundo parece que gostou do episódio de técnicas de otimização web, onde explico porque o aplicativo que você acabou de conseguir terminar seguindo um tutorial ou curso online não se parece nada com um projeto profissional de verdade. Não foi surpreendente como realmente, a maioria dos iniciantes nem imaginava que aquelas técnicas existiam. E quem já tem experiência também confirmou que tudo que falei é o arroz com feijão.</p>
<p>Na mesma semana, em dezembro de 2022, tava todo mundo super hypado com o ChatGPT. Acho que todos assistindo pelo menos ouviram falar dessa inteligência artificial, da OpenAI do Elon Musk, que consegue fazer sua lição de casa, até escrever uma matéria jornalística ou script pra videos de YouTube sem nenhum esforço e, pra nós programadores, o relevante é ele conseguir escrever código direitinho. Muitos YouTubers até prevêem o fim dos programadores. Será? Se você pensou assim também, hoje vou explicar porque está errado!</p>
<p>(&hellip; )</p>
<p>Recapitulando, no episódio de otimização expliquei rapidamente o conceito básico de balanceador de carga, tanto no nível de requisições HTTP com um NGINX ou entre sua aplicação e o banco de dados com pool de conexões. No final falei sobre como deferimos a execução de operações demoradas pra jobs assíncronos. Alguns iniciantes corretamente se questionaram: por que usar um sistema como o Bull, no caso de Node.js, se existem Promises, Async/Await, Child Processes e Worker Threads? Por que não simplesmente colocar a operação mais demorada numa Promise? Vai rodar em paralelo e devolver o HTML pro usuário rápido. Não é a mesma coisa?</p>
<p>Pra responder isso, eu queria mostrar um outro exemplo. E como escrevi este script tarde da noite, realmente tava morrendo de preguiça de escrever código Javascript então resolvi pedir pro meu copiloto, o ChatGPT, pra escrever pra mim.  Eu peço as coisas pra ele sempre tudo em inglês, porque o modelo em inglês é o mais completo e mais bem treinado e é quem dá as melhores respostas. E minha primeira requisição foi: &ldquo;escreve um form multipart pra upload de uma imagem&rdquo; (&ldquo;write a multipart form to upload an image&rdquo;).</p>
<p>Como era de se esperar, a resposta está correta, eis o código. E ele até dá uma explicação sobre o atributo <code>enctype</code> e tudo mais. Isso é bacana mas não exatamente impressionante. Pra uma lição de casa de curso ou faculdade é suficiente. Mas numa aplicação de verdade não fazemos mais assim.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;/upload&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;post&#34;</span> <span class="na">enctype</span><span class="o">=</span><span class="s">&#34;multipart/form-data&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;image&#34;</span><span class="p">&gt;</span>Select an image to upload:<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;file&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;image&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;image&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span><span class="p">&gt;</span>Upload Image<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É o seguinte: esse HTML vai gerar um formulário onde o usuário pode escolher uma imagem no seu computador ou smartphone. Quando clicar em &ldquo;enviar&rdquo; o navegador vai fazer aquele caminho que já expliquei nos episódios de rede: criar o pacote de HTTP request, conectar no servidor que responde com o endpoint &ldquo;/upload&rdquo;. E no servidor o processo que recebe essa requisição fica bloqueado esperando receber esse arquivo.</p>
<p>Qual o objetivo de um endpoint de uma aplicação web? É responder de volta pro usuário o mais rápido possível, de preferência na faixa abaixo de 100 milissegundos. Lembram? Quanto mais tempo sua aplicação leva pra responder deixando o usuário pendurado, menos requisições por segundo seu servidor consegue responder, daí somos obrigados a gastar mais, colocando mais servidores pra suportar todo mundo  acessando  simultaneamente. Portanto, sempre precisamos pensar se o que estamos fazendo contribui pra aumentar ou diminuir esse tempo.</p>
<p>O problema do form desse exemplo é que ele se conecta e vai deixar sua aplicação e o usuário parados esperando o upload da imagem completar. Num pior caso, digamos que o usuário é um fotógrafo e resolveu dar upload numa foto em 8K RAW, ou seja, descomprimido, que pode ocupar entre 60 a 100 megabytes! Digamos que seja 60 megabytes, e ele está fazendo upload do celular, numa conexão 4G ruinzinha que dá upload só a 10 megabits por segundo.</p>
<p>Isso vai custar 480 segundos, ou aproximadamente 8 minutos. Isso é terrível. Esse 1 upload fez um processo do seu servidor ficar simplesmente parado, esperando, e sem poder fazer mais nada por 8 minutos inteiros!</p>
<p>O Node tem capacidade de jogar essa espera pra uma thread separada que não bloqueia a habilidade de conseguir responder outras requisições enquanto isso. Porém, imagine que sua aplicação é feita pra fotógrafos que vão ficar o dia todo subindo imagens gigantes desse tipo, o tempo todo. Tudo tem um limite, e em breve seu servidor vai ficar bem pesado, porque não dá pra ficar criando threads infinitamente. E threads não são de graça.</p>
<p>Muitos não entendem isso. Cada nova thread ocupa espaço em RAM e tempo de processamento pra mudar o estado de uma thread pra outra. Um paliativo é justamente usar o NGINX na frente da aplicação Node, ou Spring Boot, ou Laravel ou seja lá qual framework esteja usando, porque o NGINX foi feito pra lidar com coisas como uploads demorados de forma mais eficiente do que o Node. O usuário primeiro se pendura no NGINX, faz o upload, e só quando termina o NGINX passa o arquivo direto pra sua aplicação.</p>
<p>Só que NGINX, apesar de ordens de grandeza mais rápido que seu Node, também não é infinito, e uma hora vai ficar pesado. Tem diversas formas de lidar com esse problema, mas hoje em dia o melhor custo-benefício é simplesmente não lidar com ele. Especialmente porque tem outro problema ruim que não tem a ver com a espera e sim com onde fica o arquivo: o storage, ou HD local do processo que recebeu o arquivo.</p>
<p>Lembra que numa infraestrutura de verdade subimos vários pequenos servidores embaixo de um balanceador de carga? Significa que se a aplicação receber um arquivo e gravar localmente, só os processos no mesmo servidor vão conseguir ler, os outros não vão ter acesso. Mas aí o usuário navega na sua aplicação e vai querer ver a imagem que acabou de subir, e não vai achar, porque o NGINX elegeu outro servidor pra receber sua requisição e nesse servidor não tem a imagem. Como faz?</p>
<p>Podemos usar um volume compartilhado na rede via NFS, que nem quando você se conecta num servidor de arquivos da sua empresa. É uma solução válida. Mas ao mesmo tempo gostaríamos de ter um backup, caso o servidor de arquivos dê algum pau, não queremos perder arquivos dos usuários. E no fim, vamos acabar mandando esses arquivos pra algum lugar como o serviço AWS S3. Por isso falei que o menos trabalhoso é simplesmente não lidar com arquivos localmente e sim já mandar direto pro AWS S3.</p>
<p>Eu já sabia disso então fiz outra requisição pro ChatGPT. Pedi pra ele: &ldquo;agora escreva outra versão desse formulário mas com suporte a upload direto pra um bucket com url pré-assinada do S3&rdquo;. E como esperado, devolveu um exemplo correto. (&ldquo;now write another version of this form but with support for amazon aws s3&rsquo;s pre-signed direct upload to a bucket&rdquo;)</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;{{ presignedUrl }}&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;post&#34;</span> <span class="na">enctype</span><span class="o">=</span><span class="s">&#34;multipart/form-data&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;key&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ objectKey }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;acl&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;private&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;X-Amz-Credential&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ xAmzCredential }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;X-Amz-Algorithm&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;AWS4-HMAC-SHA256&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;X-Amz-Date&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ xAmzDate }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;Policy&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ policy }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;X-Amz-Signature&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ xAmzSignature }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;image&#34;</span><span class="p">&gt;</span>Select an image to upload:<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;file&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;image&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;file&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span><span class="p">&gt;</span>Upload Image<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se você é iniciante e pelo menos já tentou programar um upload via web, provavelmente fez igual o ChatGPT na primeira vez: um form multipart dando POST pra um endpoint na sua aplicação. Só que nunca parou pra pensar em coisas como a demora do upload num cenário de verdade e nunca pensou que daria pra dar upload direto pro S3 sem passar pela sua aplicação. O grande truque é o atributo &ldquo;action&rdquo;. No exemplo anterior era um POST pra &ldquo;/upload&rdquo; na sua aplicação. Mas agora é pra URL que estiver nessa variável &ldquo;presignedUrl&rdquo;.</p>
<p>Em seguida tem vários campos escondidos que não chequei na documentação mais atualizada da AWS se é assim mesmo, mas se não for, é parecido. São coisas como o nome da chave onde vai ser gravado esse arquivo no bucket do S3, credenciais e outras configurações. E o resto do formulário é igual. Como falei antes, o  lance é que quando o usuário clicar em &ldquo;submeter&rdquo;, o arquivo não passa por nenhum dos servidores da sua aplicação. Vai dar upload direto pro bucket que você configurou na sua conta antes.</p>
<p>Mas como que gera essa tal de URL pré-assinada? Essa foi minha próxima requisição pro ChatGPT: &ldquo;agora escreva um examplo em Node.js Express de como conseguir a url pré-assinada antes de renderizar o HTML anterior&rdquo;. E ele fez o código de back-end que executa antes de mandar renderizar o HTML que acabei de mostrar. (&ldquo;now write a node.js express example of how to acquire the presignedUrl before rendering the previous HTML&rdquo;)</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;aws-sdk&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;express&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">s3</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">S3</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/upload&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">objectKey</span> <span class="o">=</span> <span class="sb">`uploads/</span><span class="si">${</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="si">}</span><span class="sb">/</span><span class="si">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">fileName</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Bucket</span><span class="o">:</span> <span class="s1">&#39;my-bucket&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Key</span><span class="o">:</span> <span class="nx">objectKey</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Expires</span><span class="o">:</span> <span class="mi">60</span><span class="p">,</span> <span class="c1">// URL expires in 60 seconds
</span></span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">s3</span><span class="p">.</span><span class="nx">getSignedUrl</span><span class="p">(</span><span class="s1">&#39;putObject&#39;</span><span class="p">,</span> <span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">presignedUrl</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">sendStatus</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Render form template with presigned URL and other required parameters
</span></span></span><span class="line"><span class="cl">    <span class="nx">res</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="s1">&#39;upload_form&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">presignedUrl</span><span class="p">,</span> <span class="nx">objectKey</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Nada mau. Ele importa bibliotecas como &ldquo;aws-sdk&rdquo;, o &ldquo;express&rdquo; e tudo mais. Note que só pedi esse trecho, mas pra funcionar, ou precisa saber fazer ou pedir pro ChatGPT configurar seu <code>package.json</code> o arquivo <code>app.js</code> e outras configurações. Só esse código sozinho não funciona. Se nunca usou Express, quando abrir o navegador apontando pra &ldquo;localhost:3000/upload&rdquo;, vai cair nesse <code>app.get('/upload')</code>.</p>
<p>Gerar a chave do objeto pode ser feito de diversas formas. Se for antes do usuário escolher qual arquivo vai mandar, vamos precisar criar um nome aleatório usando coisas como uma função de hash, tipo um SHA512 de um número aleatório. Se for via Javascript a partir do formulário, depois que o usuário escolher o arquivo, podemos usar o próprio nome do arquivo. O principal é escolher uma fórmula que gere um string que vai ser único dentro do bucket, pra não sobrescrever o arquivo de outra pessoa.</p>
<p>Então construímos um JSON simples indicando o bucket, a chave que geramos e por quanto tempo a URL de upload vai ficar disponível. Coisa como 60 segundos ou menos, porque se essa URL não tiver expiração, e vazar, qualquer um poderia ficar fazendo uploads arbitrários no seu bucket até acabar o espaço ou te fazer pagar uma fortuna à toa.</p>
<p>Finalmente, usamos o objeto de s3 que carregamos e chamamos essa função <code>getSignedUrl</code> passando os parâmetros que configuramos. A biblioteca do s3 vai chamar a API da AWS e se não der erro, vai devolver uma URL especial, a tal URL pré-assinada. Que passamos pra esse template &ldquo;upload_form&rdquo; que é o HTML que mostrei antes.</p>
<p>Aliás, faltou uma coisa né? Não tem nada mais ruim pro usuário do que dar upload de um arquivo gigante e não saber se tá fazendo upload devagar ou se travou tudo. Então pedi pro ChatGPT refazer o formulário anterior adicionando uma barra de progresso. &quot;<br>
rewrite the html form with pre-signed url for aws s3 but with a visual progress bar as well&quot;</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;upload-form&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;post&#34;</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;{{presignedUrl}}&#34;</span> <span class="na">enctype</span><span class="o">=</span><span class="s">&#34;multipart/form-data&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;key&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{objectKey}}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;x-amz-algorithm&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;AWS4-HMAC-SHA256&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;x-amz-credential&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{credential}}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;x-amz-date&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{date}}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;x-amz-signature&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{signature}}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;file&#34;</span><span class="p">&gt;</span>Choose file<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;file&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;file&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;file&#34;</span> <span class="na">required</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;progress-bar-container&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;progress-bar&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span><span class="p">&gt;</span>Upload<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">progress-bar-container</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">200</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#ccc</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">progress-bar</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">0</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">background-color</span><span class="p">:</span> <span class="mh">#4caf50</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;upload-form&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">progressBar</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;progress-bar&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;submit&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">formData</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">(</span><span class="nx">form</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">xhr</span><span class="p">.</span><span class="nx">upload</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;progress&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">lengthComputable</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">percentComplete</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">loaded</span> <span class="o">/</span> <span class="nx">event</span><span class="p">.</span><span class="nx">total</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">progressBar</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">percentComplete</span> <span class="o">*</span> <span class="mi">100</span><span class="si">}</span><span class="sb">%`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">xhr</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="s1">&#39;POST&#39;</span><span class="p">,</span> <span class="nx">form</span><span class="p">.</span><span class="nx">action</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">xhr</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">formData</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E ele fez de novo uma solução simples, mas correta. Primeiro adicionou um <code>div</code> chamado &ldquo;progress-bar-container&rdquo; com outro <code>div</code> dentro chamado &ldquo;progress-bar&rdquo;.  Em seguida criou um CSS simples pra cada um desses elementos. E no fim fez um Javascript que captura o evento de &ldquo;submit&rdquo; do form pra mandar o upload usando a classe XMLHttpRequest, que tem evento pra indicar o progresso do upload. Ele usa isso pra conseguir o percentual que já foi e atualizar o comprimento do <code>div</code>  da barra via CSS.</p>
<p>Honestamente não testei pra ver se funciona. Só de bater o olho parece que sim, mas de novo, é um código super simplificado. Como  não especifiquei que era pra usar Vue.js ou React ou outro framework, ele fez javascript puro. Se seu projeto usa React, não dê copy e paste desse código. Faça um componente seguindo o padrão da sua equipe. É o tipo de código que se encontraria numa resposta de stackoverflow e que não se deve copiar e colar sem ajustar. Note que pesquisei rapidamente no stackoverflow e achei uma resposta com código similar ao que o ChatGPT me deu. No geral não tá errado.</p>
<p>Estão entendendo? Digamos que você é um programador júnior iniciante num projeto e o gerente pedisse, &ldquo;fulano, faz um formulário de upload de imagens no nosso site?&rdquo;. Você faria a mesma coisa que o ChatGPT: aquele primeiro formulário HTML multipart mandando o arquivo direto pra aplicação e gravando no disco local do servidor e pronto. Se a mesma requisição viesse pra mim ou outro programador mais experiente, faríamos perguntas: &ldquo;é pra coisas pesadas? quem vai usar, só pessoal interno ou é pra todo usuário? onde quer gravar essas imagens? podemos mandar direto pro s3? quer mostrar uma barra de progresso pro usuário?&rdquo;</p>
<p>O ChatGPT sabe como fazer tudo isso, mas ele não questiona, faz o mais simples primeiro, e não tá errado. Precisamos saber de antemão que queremos mandar pro S3 via URL pré-assinada, daí ele faz exatamente o que foi pedido. Deixa eu dar outro exemplo pra ficar mais claro e aproveitar pra explicar mais conceitos que iniciantes não conhecem. Agora que temos a imagem no bucket do S3, digamos que nossa aplicação web é tipo um clone de Instagram. Eu gostaria que a imagem fosse cropada em formato quadrado e sempre fosse gravado em formato PNG pra padronizar tudo.</p>
<p>Então pedi exatamente isso pro ChatGPT: &ldquo;agora escreva um exemplo de ação de controller de Node.js Express pra puxar a imagem que deu upload na url pré-assinada do S3, daí use ImageMagick pra cropar num quadrado perfeito, salvar como um PNG e dar upload pra outro bucket do S3&rdquo;. (&ldquo;now write an example of a node.js express controller action to fetch the image that was uploaded to the presigned url in aws s3, then use imagemagick to crop it in a perfect square, save as a PNG and upload to another s3 bucket&rdquo;)</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;aws-sdk&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;express&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">s3</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">S3</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">im</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;imagemagick&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/process-image&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Download image from S3 bucket
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">objectKey</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">objectKey</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">Bucket</span><span class="o">:</span> <span class="s1">&#39;my-bucket&#39;</span><span class="p">,</span> <span class="nx">Key</span><span class="o">:</span> <span class="nx">objectKey</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">imageData</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">s3</span><span class="p">.</span><span class="nx">getObject</span><span class="p">(</span><span class="nx">params</span><span class="p">).</span><span class="nx">promise</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">imageBuffer</span> <span class="o">=</span> <span class="nx">imageData</span><span class="p">.</span><span class="nx">Body</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Use ImageMagick to crop the image into a square
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">croppedImageBuffer</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">im</span><span class="p">.</span><span class="nx">crop</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">        <span class="nx">srcData</span><span class="o">:</span> <span class="nx">imageBuffer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">width</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">height</span><span class="o">:</span> <span class="mi">200</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">gravity</span><span class="o">:</span> <span class="s2">&#34;Center&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">stdout</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="k">return</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">resolve</span><span class="p">(</span><span class="nx">stdout</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Save the cropped image as a PNG
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pngImageBuffer</span> <span class="o">=</span> <span class="kr">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">im</span><span class="p">.</span><span class="nx">convert</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s1">&#39;-strip&#39;</span><span class="p">,</span> <span class="s1">&#39;-interlace&#39;</span><span class="p">,</span> <span class="s1">&#39;Plane&#39;</span><span class="p">,</span> <span class="s1">&#39;-quality&#39;</span><span class="p">,</span> <span class="s1">&#39;100&#39;</span><span class="p">,</span> <span class="s1">&#39;-colorspace&#39;</span><span class="p">,</span> <span class="s1">&#39;sRGB&#39;</span><span class="p">,</span> <span class="s1">&#39;-define&#39;</span><span class="p">,</span> <span class="s1">&#39;png:compression-level=9&#39;</span><span class="p">,</span> <span class="s1">&#39;png:-&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="nx">croppedImageBuffer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">stdout</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="nx">resolve</span><span class="p">(</span><span class="nx">stdout</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Upload the cropped image to another S3 bucket
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">targetBucket</span> <span class="o">=</span> <span class="s1">&#39;cropped-images&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">targetObjectKey</span> <span class="o">=</span> <span class="nx">objectKey</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">&#39;uploads/&#39;</span><span class="p">,</span> <span class="s1">&#39;cropped/&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kr">await</span> <span class="nx">s3</span><span class="p">.</span><span class="nx">putObject</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Bucket</span><span class="o">:</span> <span class="nx">targetBucket</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Key</span><span class="o">:</span> <span class="nx">targetObjectKey</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Body</span><span class="o">:</span> <span class="nx">pngImageBuffer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">ContentType</span><span class="o">:</span> <span class="s1">&#39;image/png&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}).</span><span class="nx">promise</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">res</span><span class="p">.</span><span class="nx">sendStatus</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">res</span><span class="p">.</span><span class="nx">sendStatus</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>De novo, tá certinho. No começo ele carrega bibliotecas que precisa saber carregar no <code>package.json</code>. E cria um endpoint de POST chamado &ldquo;/process-image&rdquo; que poderia ser chamado pelo Javascript do formulário, pelo objeto de XMLHTTPRequest quando detectar que o upload terminou, por exemplo. Esse código faz o seguinte, primeiro tem que saber a URL completa do arquivo que acabou de subir no bucket do S3, e configurar os parâmetros pra esse bucket.</p>
<p>Ele começa chamando o método <code>getObject</code> do objeto do s3. Não estou familiarizado com a biblioteca <code>aws-sdk</code> pra Javascript, mas parece que  devolve uma Promise e corretamente dá <code>await</code>. Pra quem é de outras linguagens que não tem Promises, não é a mesma coisa, mas é como se estivesse colocando esse <code>s3.getObject</code> dentro de uma nova Thread e esse <code>await</code> é como se fosse <code>join</code>, ou seja, pra esperar a thread terminar antes de continuar.</p>
<p>Finalmente, puxamos o conteúdo binário do arquivo nessa variável <code>imageBuffer</code>. Relembrando, se a imagem tinha 60 megabytes, agora tem 60 megabytes na memória do interpretador V8 desse processo. Na sequência, ele faz o código pra fazer crop, ou seja, pra recortar a imagem num quadrado usando a biblioteca ImageMagick nesse &ldquo;im&rdquo; chamando o método &ldquo;crop&rdquo; direitinho.</p>
<p>Ele assumiu que eu queria uma imagem de 200 por 200 pixels. Tá ok, não tinha dito nenhum outro número, mas de novo, por isso que não pode copiar e colar às cegas. Se o amador só copiasse sem ver e deixasse isso passar, agora aquela imagem de altíssima qualidade em 8K RAW do fotógrafo, se transformou numa imagem porcaria minúscula. Se for pra ser fixo eu não cortaria nada menor que uns 1000 por 1000 pixels.</p>
<p>Esse pattern que ele faz de englobar tudo numa nova Promise e imeditamente chamar await, do ponto de vista só desse código, seria a mesma coisa que não ter isso. Desse jeito (muda imagem). Esse código vai esperar, await, até o crop terminar. Pela forma como o event loop do Node.js funciona, suponho que a vantagem é que fazendo assim, a única thread do event loop ficaria bloqueada e o Node não iria conseguir fazer mais nada até o crop do ImageMagick terminar.</p>
<p>Mas colocando numa Promise e dando await, ele delega pro sistema operacional a operação de esperar o I/O (ler arquivo, gravar arquivo), e libera o event loop pra fazer outra coisa, como responder outras requisições de outros usuário. Quando a operação cara de I/O retorna, a thread continua depois do await como se nada tivesse acontecido. Essa é a grande vantagem de se usar Node em primeiro lugar, o event loop e o suporte a Promises.</p>
<p>Vou voltar nisso mais pra frente. Em seguida temos um bloco de código que chama direto o programa &ldquo;convert&rdquo;. Mesma coisa, Promise com await. E no final, o último bloco é chamando a biblioteca do S3 pra fazer o upload do binário cropado e reformatado como PNG pro bucket do S3.</p>
<p>Agora vamos entender o problema. Quer dizer, não é nenhum problema se só você for usar essa aplicação, ou mesmo se for uma parte administrativa de um site onde só meia dúzia de pessoas for usar. Mas se for pra todo usuário do site e tiver centenas ou milhares de usuários, isso vai pesar rapidamente e exigir que se faça deploy de muitos servidores, tornando a infraestrutura super cara.</p>
<p>Primeiro de tudo. Quando fazemos download da imagem do S3 e guardamos nessa variável <code>imageData</code> e a imagem tem 60MB, de cara estamos consumindo 60MB de RAM do servidor. Digamos que 100 usuários estão fazendo upload de imagens mais ou menos ao mesmo tempo. Seria 6 GB de RAM sendo ocupado no servidor web. Pra processar imagens, realmente vai usar muita RAM, o problema é compartilhar isso no mesmo servidor que precisa responder requisições web o mais rápido possível.</p>
<p>Além disso temos duas operações de rede pra s3, de download e upload, baixando uns 60MB e subindo uma imagem menor, mas é um tráfego que não podemos ignorar. De novo, se tiver 100 usuários simultaneamente subindo imagens de alta qualidade, naquele minuto seu servidor tá precisando gastar uns 6 gigabytes de tráfego só pra baixar do S3. &ldquo;Ah, mas é porque você mandou fazer direct upload pro s3, se estivesse num storage local estaria ocupando menos banda&rdquo;</p>
<p>Sim, é verdade e eis um exemplo de que não tem resposta certa. Se você é um serviço grande onde uma das partes principais do que oferece aos usuários é armazenamento de arquivos grandes, talvez  queira ter essa infraestrutura sob seu controle. Vai ter gente dedicada, com servidores no data center dedicados só pra gerenciar armazenamento, daí não precisa usar tanto S3.</p>
<p>Por outro lado, pode ser uma empresa no começo, que ainda não tem  tantos servidores, rede e gente de infra dedicada só pra armazenamento. Talvez o S3 seja um custo-benefício melhor. Nenhuma das duas respostas é errada, depende de quem você é, qual seu nível de serviço, seu orçamento, quanto cobra e qual sua receita.</p>
<p>Digamos que somos uma empresa menor, e pra gente S3 faz sentido, então o código seria mais ou menos como nesse exemplo. Mas além do armazenamento temos esses dois blocos pesados de processamento, que é calcular o cropping e converter a image pra outro formato. Lembra que falei que na variável <code>imageData</code> tem 60 megabytes do binário da imagem original? Nessa parte aqui a variável <code>croppedImageBuffer</code> vai ter o binário da imagem recortada, digamos que seja uns 50 Megabytes.</p>
<p>E nessa outra parte aqui onde convertemos pra PNG faz de conta que o PNG ficou com uns 5 Megabytes de tamanho, porque é comprimido. Então, antes dessa função acabar, essas 3 variáveis estão ocupando 60 + 50 + 5, ou seja, 115 megabytes de memória. Se tiver 10 usuários pendurados fazendo upload de imagens semelhantes, estamos ocupando uns 11 giga e meio de RAM dos servidores só nesse processo.</p>
<p>Estão entendendo? O problema não é só conseguir rodar essas tarefas em paralelo pra liberar processamento pro Node conseguir atender mais requisições. O problema é que quanto mais gente usar o sistema, mais rápido vai acabar os recursos das máquinas web e em breve não vai ter RAM pra atender mais gente chegando. O objetivo é responder rápido, mas fazendo tudo isso, é impossível devolver a resposta HTML pro usuário na faixa de menos de 100 milissegundos.</p>
<p>Por isso que pra casos assim, o correto é jogar esse processamento pra fora da aplicação web. Isso não vai diminuir nem a carga de processamento e nem o uso de RAM, mas vamos conseguir segmentar, separar os recursos dedicados só pra web e os recursos dedicados pra processar as imagens. No caso do Node.js podemos usar a biblioteca Bull, como mencionei no último episódio.</p>
<p>Pedi pro ChatGPT: &ldquo;escreva o mesmo código mas sem usar Promises e na realidade deferir as tarefas de download da imagem, cropping, salvar em PNG e fazer upload de volta pra outro bucket como um job assíncrono implementado como um worker de Bull. (&ldquo;write the same code but without using Promises and actually deferring the tasks for downloading the image, cropping the image, saving as PNG and uploading back to a bucket to an asynchronous job implemented as a Bull worker&rdquo;)</p>
<p>Sabendo perguntar em detalhes exatamente o que queremos, o ChatGPT responde direitinho. Ele não tem como adivinhar qual é a melhor solução pra você, por isso sempre vai dar a resposta mais simples. Se eu não soubesse que Bull existe, o ChatGPT não ia me dizer. Mas agora o código começa com ele carregando a biblioteca do Bull e instanciando uma nova fila chamada &ldquo;image-processing&rdquo;. Em seguida já adiciona uma nova tarefa na fila com a chave do objeto no bucket, que é a imagem original que o usuário fez upload. Esse é o trecho que iria no controller do Express e vamos tirar fora.</p>
<p>Eu não pedi pro ChatGPT reescrever aquele trecho <code>app.post(&quot;/upload&quot;)</code> do Express, mas seria arrancar fora todo aquele código do processamento de imagens e simplesmente colocar essa linha de registrar uma nova tarefa na Fila e rapidamente devolver OK pro usuário parar de esperar na fila. Caso a página web que iria devolver tivesse um preview da imagem que subiu, não teríamos como mostrar porque ainda não processamos, mas aí é só colocar uma imagem genérica com uma mensagem de &ldquo;aguarde, estamos processando&rdquo;, e colocar um WebSocket ou algo assim pra puxar a imagem processada quando ficar disponível depois.</p>
<p>Voltando pro Worker de Bull, é a mesma coisa de antes, o código que acabamos de apagar do endpoint de <code>app.post</code>. Esse worker eu instalaria num servidor separado só pra workers. A vantagem é que  posso controlar quantos tarefas quero processar simultaneamente. Digamos que meu servidor não tenha 11 gigabytes de RAM que seria necessário pra rodar 100 tarefas simultâneas de imagens grandes. Só tenho 4 gigabytes, então só dá pra rodar umas 30 tarefas ou menos. Mas tudo bem. Vai demorar mais, mas como o usuário não tá pendurado esperando e nem estou congestionando os servidores web, não tem problema se demorar o triplo de tempo.</p>
<p>Além disso tem outra razão pra jogar essas coisas pesadas num job assíncrono em vez de tentar rodar com Promises ou Threads. Digamos que tenha um bug na biblioteca do ImageMagick ou do S3 e dê um crash na hora de processar por alguma razão. Como faço pra rodar de novo essa tarefa? Existem bibliotecas como o &ldquo;async-await-retry&rdquo;, mas imagina que seja um erro sistêmico, e tinha 100 usuários subindo imagens e por um erro qualquer, as 100 tarefas comecem a dar pau, e 100 vezes o sistema tente dar retry e fique retentando? Meu servidor vai morrer retentando à toa assim. Eu preciso ter controle sobre regras de retry.</p>
<p>Numa fila como Bull, se os workers começam a dar pau, posso configurar pra não dar retry e só marcar erro. Se descobrimos que foi algo sistêmico, podemos corrigir o bug e depois que estiver tudo corrigido, mandar as tarefas pendentes na fila rodar, tudo sob controle. Na documentação do Bull ele explica como podemos configurar retries automáticos, por exemplo, aqui ele diz pra tentar 3 vezes caso dê problema e com um tempo de backoff exponencial que começa em 1 segundo na primeira tentativa, 2 segundos pra segunda, 4 segundos pra terceira. Nunca é bom retentar imediatamente porque muito provavelmente vai dar problema igual.</p>
<p>E o Bull tem um monte de funcionalidades importantes como Gracefull shutdown. Digamos que modificamos o código dos workers e agora precisamos atualizar nos servidores. Não pode simplesmente desligar tudo e atualizar. Não queremos crashear jobs que estão rodando. Normalmente queremos que o worker saiba que tem uma atualização e reinicie só depois que terminar de processar o job que estava em andamento.</p>
<p>Se temos vários servidores de Bull, cada um com vários processos, ele vai desligando, atualizando e reiniciando um a um só depois que terminar os jobs. Assim nenhum job vai corromper. É que nem dar shutdown normal num Windows em vez de apertar o botão de força e desligar forçado, que nunca é bom.</p>
<p>E sendo um sistema com fila e workers separados, existem projetos como o Matador, que é uma interface web pra monitorar os trabalhos na fila do Bull. Assim um administrador ou devops pode checar visualmente se os workers estão se comportando como deveriam e facilitar conseguir identificar se tudo está correndo bem.</p>
<p>Entendam, colocar um Bull e jogar aquele processamento pesado de imagens em outros servidores não diminui a quantidade de recursos sendo gastos. Ele serve pra tornar esse processamento mais gerenciável e previsível. Tudo que roda no lado dos servidores web é pra ser leve e rápido. Tudo que é pesado, como nesse exemplo de imagens, jogamos em jobs em outros servidores que só rodam jobs.</p>
<p>E a comunicação entre os dois é feita via uma Fila. No caso do Bull ele usa um módulo chamado <code>ioredis</code> que usa Redis como servidor de fila. O correto na hora de instanciar a fila é indicar o servidor do Redis na conexão e quando subimos os workers, apontamos pro mesmo Redis pra escutarem quando chegar novos jobs, como estou mostrando neste exemplo que tem na própria documentação.</p>
<p>Se não ficou claro vou repetir: quando precisamos executar alguma tarefa mais pesada, faz de conta, mandar uma linha nova de log pra um servidor de log, enviar uma notificação, coisas que não duram muitos segundos ou até minutos, é possível que Promises no caso de Javascript, ou async/await num DotNet ou mesmo Threads como num Java, façam sentido. O problema é quando a duração desse processamento é longo e, pior, varia bastante. Nesse caso o tempo vai variar dependendo do tamanho das imagens. Fica imprevisível demais pra controlar misturado com a requisição web.</p>
<p>O certo é mandar pra jobs assíncronos. Isso me lembrou um caso. Esses dias eu estava configurando minha conta de Google Workspace, que são os planos pagos do Google pra serviços como Gmail. Eu queria habilitar a opção de conseguir mudar o tema do meu Gmail, isso tava desligado sei lá porque. Fui no admin, nessa tela e liguei. Mas mesmo recarregando meu Gmail, essa opção não aparecia. Olha só, em outra conta de Google Workspace que é da minha empresa, já tá ligado e em configurações aparece essa opção aqui tão vendo? Por que na outra conta não apareceu?</p>
<p>Porque quando habilitei no admin ele mandou isso pra uma fila que tem um tempo super longo pros workers pegarem. Pode ser porque tem tanta operação em fila dos milhões de usuários que usam a plataforma, que precisa de horas pra executar meu pedido. Você imaginaria que deveria ser imediato, mas eles escolheram separar assim porque deve ser pesado por N motivos que não temos acesso de saber.</p>
<p>Veja o YouTube. Toda vez que termino um episódio, faço upload do arquivo de video. É um arquivo de dezenas de gigabytes, leva vários minutos pra subir. E depois que sobe precisa fazer checagens de copyright, gerar versões com resoluções menores, sincronizar cópias em CDN e assim por diante. Enquanto isso não acontece, eu fico vendo essa mensagem indicando que ainda tá processando. Só depois que termina que consigo fazer coisas como adicionar cards.</p>
<p>Veja no anchor.fm, que é onde eu subo o arquivo mp3 pra publicar no Spotify ou Deezer. Eu faço upload e depois ele manda pra alguma fila, enquanto isso no meu front-end só indica que tá &ldquo;processando&rdquo;. Meu navegador não tá pendurado no servidor deles. Provavelmente tem um javascript que de tempos em tempos checa se já terminou, ou tem um web socket aberto.</p>
<p>Qual &ldquo;a&rdquo; melhor forma de fazer tudo isso? Não tem. Depende de quantos servidores, de quais tamanhos você pode pagar. O software tem que ser feito pra organizar os recursos disponíveis de forma que fique tudo menos difícil de gerenciar. Não quer dizer necessariamente mais performance ou rodar tudo em paralelo. Controle significa que quando alguma coisa dá erro eu tenha a oportunidade de pausar, checar, consertar e mandar rodar de novo, e pra isso soluções usando fila e workers como o Bull de Node.js faz sentido.</p>
<p>Muitos pensam que porque Node.js tem eventloop não precisa de jobs assíncronos, só Promises. Espero que tenham entendido que são funcionalidades diferentes pra casos diferentes. Mesma coisa pessoal de Java, que tem threads nativas e poderiam achar que não precisam de jobs assíncronos. Mas um dos sistemas de fila mais reconhecidos é justamente o Apache Kafka, que é feito em Java. Aproveitei pra pedir pro ChatGPT reescrever o worker de Bull em Spring Boot (&ldquo;To process an image that was uploaded to an Amazon S3 bucket using a pre-signed URL in a Spring Boot app, using a Kafka queue to defer the tasks to a worker, you can use the following code&rdquo;)</p>
<p>Tecnicamente, o boilerplate, a parte burocrática de configuração de Kafka exige adicionar o pacote org.springframework.kafka no arquivo <code>pom.xml</code> do projeto que é mais ou menos o equivalente ao <code>package.json</code> de Node. Daí teria que usar o script <code>kafka-topics.sh</code> pra criar os tópicos da fila. Finalmente, pra aplicação Spring Boot conseguir criar mensagens pra mandar pra fila, precisa configurar um <code>ProducerFactory</code> que é o responsável pela estratégia de como criar instâncias de Producers de Kafka.</p>
<p>O código de exemplo que o ChatGPT gerou instancia um KafkaTemplate mas ele deveria injetar um ProducerFactory dentro, que foi omitido. Como o video não é pra ser um tutorial de Spring Boot, depois leiam a documentação, mas só pra explicar que, de novo, o ChatGPT não me deu tudo mastigado. Eu sei que falta coisa porque já li a documentação. Se você nunca usou Kafka na vida e achou que o ChatGPT ia te dar tudo mastigado, pensou errado.</p>
<p>Em seguida ele cria um endpoint web chamado &ldquo;/process-image&rdquo; como tínhamos feito com o Express. No Spring Boot isso é configurado com uma annotation que é essa linha que começa com arroba pra decorar a função logo abaixo. E essa função responde super rápido porque a única coisa que faz é pegar a instância de KafkaTemplate e enviar a mensagem do job pra fila.</p>
<p>A classe de Worker abaixo também é configurado com outra annotation pra dizer que é o responsável por mensagens enviadas pro tópico &ldquo;image-processing&rdquo;. Nessa classe tem a função <code>processImage</code> que vai fazer a mesma coisa que o worker de Bull, só que em Java. Instanciar bibliotecas de S3 e ImageMagick nas versões de Java, download da imagem num ByteArray em memória, cropar e converter em PNG e fazer upload. Em termos de tempo de processamente, como o grosso depende da conexão remota com o S3 e de chamar o mesmo ImageMagick, tanto faz ser em Javascript ou em Java, vai levar mais ou menos o mesmo tempo pra rodar.</p>
<p>Pra quem gosta de perder tempo discutindo benchmarks de linguagens em subreddit, entenda esse cenário. Veja esses dois códigos lado a lado. A linguagem meio que não importa, porque o grosso do trabalho não é feito nem em Javascript e nem em Java. O grosso é o ImageMagick e o S3. Nas dus versões, vão fazer fork e subir um processo separado da ferramenta <code>convert</code> que vem no pacote do ImageMagick, que é feito em C.</p>
<p>Ele é quem vai fazer o trabalho sujo. E sim, eu não precisava ter usado ImageMagick, poderia ter escolhido uma biblioteca feita em puro Javascript e outra feita em puro Java. Mas não vejo porque, ImageMagick é a ferramenta mais madura e mais reconhecida pra trabalhar imagens. É literalmente um Photoshop em linha de comando que faz praticamente tudo. Pra que vou reinventar a roda? E mesmo se eu reinventasse a roda, não seria uma diferença muito grande de qualquer jeito.</p>
<p>Novamente, o objetivo de hoje foi demonstrar como um código feito como exercício simples, que só você vai usar sozinho na sua própria máquina é de um jeito. Mas como as mesmas funcionalidades num projeto de verdade ficam completamente diferentes. Na sua própria máquina, não precisa de Bull nem de Kafka. Basta subir uma Promise, ou child process que é fork de processos, ou worker thread que sobe uma thread no mesmo processo pra rodar em paralelo. Mesmo se a imagem for pesada, só você tá usando, mesmo se demorar alguns minutos e usar muita RAM e cair pro swap que é mais lento, não tem problema. O problema só aparece se estiver exposto na internet, aí fodeu.</p>
<p>E agora sobre coisas como o ChatGPT ou GitHub Copilot que não mostrei nesse video mas já usei. Acho excelentes. Eu sei exatamente o que quero, eu sei que não quero uma solução simples com upload direto na aplicação web. Eu sei que existe a técnica de URL pré-assinada, então o ChatGPT me obedece. Eu sei que o processamento de imagens, download e upload pro S3 é demorado, por isso eu soube pedir pro ChatGPT fazer o código levando isso em consideração e usar Bull e Kafka.</p>
<p>Além disso eu sei que nos exemplos de código que o ChatGPT me devolveu não tem a configuração de Redis do Bull nem do ProducerFactory de Kafka. Eu sei onde configurar e os detalhes que não lembro. Eu sei achar a documentação e completar. Também poderia pedir. &ldquo;Ow, ChatGPT, faltou a configuração de ProducerFactory no KafkaTemplate, pode completar pra mim?&rdquo;. Entenderam? Vocês assistindo que são iniciantes e ainda não trabalharam em projetos de verdade: já tinham ouvido falar dessas coisas?</p>
<p>Não estou dizendo que ninguém ensina, mas entenda: os cursos mais básicos querem que vocês tenham a falsa ilusão que programar é simples e qualquer um pode fazer. E te dão um passo a passo pra conseguir fazer um front-end de instagram, com upload de imagem, e quando conseguir ver a imagem postada na sua aplicação local, fica feliz. E realmente, a primeira vez que consegue é bacana mesmo. Mas acho que falta dizer que em cima disso tem esse tanto de coisas que mostrei nesse video e no anterior.</p>
<p>Uma inteligência artificial consegue facilmente substituir um programador júnior inexperiente como puderam ver aqui. Porém, ele ainda não consegue substituir um programador experiente. Eu vou ficar impressionado quando pedir pra ele fazer alguma coisa e em vez de me dar um código simples, começar a me devolver perguntas pertinentes, como quantos usuários eu espero que o sistema tenha que aguentar, ou o volume dos uploads. Um programador experiente não sai codando à toa de qualquer jeito.</p>
<p>O ChatGPT é um serviçal bem burrinho que dá respostas erradas ou incompletas com muita convicção. Por exemplo, num video que eu tava assistindo, do Joseph Carlson, ele pediu pro irmão dele, que é médico de verdade, fazer uma pergunta pro ChatGPT, no caso foi &ldquo;qual a condição mais comum que provavelmente se origina na coróida e que afeta homens jovens?&rdquo; E o ChatGPT faz um textão explicando que é neovascularização coroidal e segue explicando o que é isso. E o médico responde: &ldquo;nada mau, mas está incorreto. Seria correto se estivéssemos falando de pessoas idosas. A resposta correta seria retinopatia serosa, e é por isso que fazer auto diagnóstico via Google  não funciona&rdquo;.</p>
<p>Claro, esse caso em particular, com um pouco mais de treino o ChatGPT deve conseguir responder correto da próxima vez, mas o ponto é que as respostas deles não são garantidamente corretas o tempo todo e quando são erradas, ele não tem noção que tá errado. A resposta é sempre verbalizada de uma forma bem assertiva. Se você não for também especialista no assunto, vai acreditar e nesse caso poderia se auto-diagnosticar errado e acabar tendo problemas graves. Mas isso já acontece hoje com Google também.</p>
<p>Pra amadores, o ChatGPT pode ser usado como uma ferramenta de estudos, e nada do que ele soltar deve ser usado no mundo real sem consultar um especialista, porque você não tem o conhecimento e nem a experiência pra saber se veio errado ou incompleto. Pra especialistas de verdade, o ChatGPT é excelente pra fazer as tarefas mundanas que nós sabemos fazer, mas não vale nossa taxa hora, como os códigos que mostrei aqui. Eu sei avaliar se o código serve ou não, por isso pra mim o ChatGPT funciona bem. Quando ele não sabe responder, ou me responde errado, eu sei como consertar.</p>
<p>O banco de dados, o modelo dele, é realmente impressionante. O que mais me impressiona não é funcionar. Eu sei que funciona. As teorias pra isso existem e vem sendo evoluídas e desenvolvidas faz décadas. Mas pra fazer modelos desse tipo custa extremamente caro. Eu estava assistindo o canal Coldfusion que gosto muito e recomendo, e no video ele menciona o que o Sam Altman, presidente da OpenAI  falou.</p>
<p>Hoje temos milhões de pessoas usando o ChatGPT pela novidade. Mas ele não escala pra ordem de bilhões de pessoas, que seria a escala de redes sociais como Facebook ou a escala que Google, Apple ou Microsoft conseguem atingir com seus produtos. O custo seria inviável. Segundo o Altman, uma resposta do ChatGPT custa de 10 a 100 vezes o custo de pesquisa no Google. Ainda precisa otimizar 100 vezes essa tecnologia pra chegar num ponto onde vai ser economicamente viável todo mundo usar no dia-a-dia, como smartphones ou um Google.</p>
<p>E claro, em alguns anos isso vai acontecer. Hoje ainda não é esse dia. Pode levar 10 anos, 20 anos. E isso pra otimizar custos. Pra chegar num ponto onde posso confiar quase 100% a ponto de não precisar de especialistas humanos, isso realmente não vejo acontecendo tão cedo. Eu vejo sim, sendo um excelente auxiliar pra nós especialistas, e num espectro menor, ajudando não especialistas com perguntas simples do dia a dia, algo um pouco melhor que as porcarias de Alexa, Siri ou Cortana.</p>
<p>Pra nós programadores tem outro aspecto. Muitos iniciantes acham que deveriam estudar tópicos de inteligência artificial porque num futuro próximo todo mundo vai estar programando inteligências artificiais, mas sinto te informar que isso não vai acontecer como você pensa. Sim, se você tem nível e talento pra ser contratado pra times de pesquisa da OpenAI, da Microsoft, da Apple, do Google, aí você teria chances de trabalhar na construção dessas inteligências. Mas fora dessas empresas gigantes, não vai rolar.</p>
<p>O maior problema não são os algoritmos. O problema é o treinamento. Pra conseguir fazer um Dall-e 2, Midjourney ou Stable Diffusion gerar fotos e desenhos artísticos realistas, precisou treinar cada um desses modelos com milhões de imagens diferentes por muito e muito tempo. Estamos falando em escala de petabytes de dados, milhares ou milhões de horas em dezenas ou centenas de servidores super caros. Data centers dedicados com o supra sumo das placas de video NVIDIA. Ou seja, você, no seu PCzinho ou notebook, jamais vai conseguir produzir um modelo de verdade que vá além de um  brinquedo.</p>
<p>Modelos sérios do tamanho de um ChatGPT custam milhões de dólares pra produzir. Você não vai ter acesso a esse tipo de investimento e infraestrutura. Se alguém de uma tech startup desconhecida vier te convidar pra construir um modelo desses, já sabemos que ele mora em Nárnia, enxerga unicórnios e tem amigos imaginários. Não vai acontecer.</p>
<p>Na prática, quase todos nós, programadores, no máximo vamos poder usar modelos prontos dos outros, como da OpenAI. Se já tentou usar o ChatGPT todo dia, já deve ter visto como do nada uma resposta engasga e dá erro de conexão, ou como o site todo às vezes fica fora do ar por alguns segundos ou minutos. Exatamente porque nesta versão de agora, ainda não escala. Precisa sair uma versão 2.0 com mais garantias pra gente poder investir em produtos que tirem proveito desse modelo.</p>
<p>Mas a pergunta que todo mundo tem na cabeça é &ldquo;o ChatGPT vai substituir programadores?&rdquo; E a resposta é simples: sim, vai substituir os ruins, os preguiçosos, os falsos sêniors e infelizmente alguns dos iniciantes. Porque muitas tarefas mundanas de código que um júnior pegaria, eu como sênior posso usar o ChatGPT pra fazer mais rápido e, mais importante, com zero reclamação, mais rápido e com menos erros. Agora, substituir um bom sênior de verdade? Eu diria que precisa pelo menos de uns bons 50 anos antes de cogitar algo assim. Tá longe, bem longe.</p>
<p>Eu estou satisfeito o suficiente com ferramentas como o ChatGPT ou Copilot do GitHub. Eles são mais eficientes e mais rápidos do que dar alt-tab e procurar o que preciso no Google ou Stackoverflow. Eu sei quando ele me devolve bobagem e consigo ignorar. E sei quando tá incompleto. Ao mesmo tempo, é bom pra fazer coisas mudanas e repetitivas, como testes unitários por exemplo. Ou me devolver exemplos de códigos que estou com preguiça de procurar no Github. Realmente, é como se eu tivesse um serviçal que procura no Google pra mim.</p>
<p>Muitos YouTubers tem feito video dizendo &ldquo;olha só como o ChatGPT escreveu o script desse video&rdquo;. Eu tentei, mas tava dando tanto trabalho que foi mais r;apido eu mesmo escrever. Sinto muito, na atual versão, o ChatGPT é incapaz de escrever um script pra este canal sem muita intervenção. Eu concluo que os canais dos outros são bem superficiais e simplórios se um script gerado pelo ChatGPT é capaz de enganar a audiência. No meu caso não rolou. O ChatGPT é excelente pra encher linguiça de forma convincente, mas falar direto ao ponto, ele tem muita dificuldade.</p>
<p>No final a conclusão é simples: seu grau de empolgação com o ChatGPT é inversamente proporcional ao seu nível de conhecimento. Se for baixo, vai ficar exageradamente empolgado, se for alto, vai achar útil, com ressalvas. É a diferença de quem vê um truque de mágica pela primeira vez, e de quem já sabe como o truque funciona. Pra quem não quer ser substituído por uma IA mande dúvidas nos comentários abaixo. Se curtiram o video deixem um joinha, assinem o canal e não deixem de compartilhar o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>sistemas-distribuidos</category><category>backend</category><category>llms</category><category>akitando</category></item><item><title>[Akitando] #125 - Burlando Proxies e Firewalls | Introdução a Redes Parte 5 - SSH</title><link>https://akitaonrails.github.io/2022/08/17/akitando-125-burlando-proxies-e-firewalls-introducao-a-redes-parte-5-ssh/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/08/17/akitando-125-burlando-proxies-e-firewalls-introducao-a-redes-parte-5-ssh/</guid><pubDate>Wed, 17 Aug 2022 10:32:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/T-jHuFnxZ2k"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Ainda não é um episódio &amp;ldquo;de segurança&amp;rdquo; mas vou começar mostrando como é fácil injetar um bug de segurança tendo a melhor das intenções e depois como manipular tunelamento de SSH pra abrir buracos em firewalls e bypassar proxies. Coisas que podem ser muito úteis na vida real.&lt;/p&gt;
&lt;h2&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;00:57 - todo programa é como uma função&lt;/li&gt;
&lt;li&gt;02:52 - CAP 1 - fazendo um site com injection&lt;/li&gt;
&lt;li&gt;06:34 - One Liners&lt;/li&gt;
&lt;li&gt;08:25 - Sanitização&lt;/li&gt;
&lt;li&gt;09:57 - CAP 2 - furando firewall&lt;/li&gt;
&lt;li&gt;13:47 - criando um droplet na DigitalOcean&lt;/li&gt;
&lt;li&gt;15:41 - reconfigurando sshd&lt;/li&gt;
&lt;li&gt;18:06 - ssh remote forwarding&lt;/li&gt;
&lt;li&gt;21:26 - CAP 3 - furando proxies&lt;/li&gt;
&lt;li&gt;26:25 - ssh local forwarding&lt;/li&gt;
&lt;li&gt;30:45 - CAP 4 - consertando a vaca&lt;/li&gt;
&lt;li&gt;33:17 - conclusão&lt;/li&gt;
&lt;li&gt;34:27 - bloopers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal, Fabio Akita&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/T-jHuFnxZ2k"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Ainda não é um episódio &ldquo;de segurança&rdquo; mas vou começar mostrando como é fácil injetar um bug de segurança tendo a melhor das intenções e depois como manipular tunelamento de SSH pra abrir buracos em firewalls e bypassar proxies. Coisas que podem ser muito úteis na vida real.</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>00:57 - todo programa é como uma função</li>
<li>02:52 - CAP 1 - fazendo um site com injection</li>
<li>06:34 - One Liners</li>
<li>08:25 - Sanitização</li>
<li>09:57 - CAP 2 - furando firewall</li>
<li>13:47 - criando um droplet na DigitalOcean</li>
<li>15:41 - reconfigurando sshd</li>
<li>18:06 - ssh remote forwarding</li>
<li>21:26 - CAP 3 - furando proxies</li>
<li>26:25 - ssh local forwarding</li>
<li>30:45 - CAP 4 - consertando a vaca</li>
<li>33:17 - conclusão</li>
<li>34:27 - bloopers</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Finalmente, agora que posso assumir que assistiram meus últimos quatro videos sobre introdução a redes e internet, posso começar a falar de coisas mais interessantes. Vocês já sabem como informação trafega na rede, como processos se ligam a portas, sockets e tudo mais. Então vamos falar rapidinho sobre alguns conceitos de segurança em rede. Só pra esclarecer, o tema de hoje não é o geral de segurança, só algumas brincadeiras que eu acho interessante saber.</p>
<p>A primeira parte da história vai envolver um pouco de programação web e um dos buracos de segurança mais comum. E na segunda parte vou mostrar alguns truques que dá pra fazer com SSH em alguns cenários. A idéia não é ser um video completo sobre segurança, só mencionar dois aspectos que podem ser úteis. Eu já fiz um video inicial de segurança e em episódios futuros talvez eu entre em mais detalhes. Mas por hoje são coisas que programadores iniciantes ainda nem sabem que dá pra fazer.</p>
<p>(&hellip;)</p>
<p>Pense assim, um programa é como se fosse uma função. Ele pode receber algum input seu, do usuário, processar alguma coisa dentro, e cuspir alguma resposta. Sempre são essas três grandes partes, input, processamento e output. Mesmo no design de um chip de CPU temos as três grandes partes de fetch, decode e execute. Se for um programa long lived, que persiste e fica rodando em background, como expliquei no episódio passado, é um Big Loop esperando algum input seu na forma de coisas que digita ou que clica na tela. Daí processa alguma coisa, e cospe alguma coisa na sua tela. Em resumo, programas mastigam seu input e cospem um output. Também podemos pensar em programas como alguma coisa que transforma o input em um output. Por exemplo, um Spotify transforma os cliques de mouse na interface em música, que é seu output.</p>
<p>Antes de internet e antes de redes, você era a única pessoa que entregava algum input pro programa que ia rodar na própria máquina. E a maioria de nós não tem interesse em digitar comandos que destruam a própria máquina. Ninguém vai digitar <code>sudo rm -Rf /</code> e dar a senha. Esse meme é antigo mas deve ter gente que ainda não sabe o que é isso. Sério, se você não sabe vai assistir meu episódio de Ubuntu. Tudo que você manda sua máquina fazer é responsabilidade sua saber o que vai acontecer. Se não sabe, Google tá aí pra isso.</p>
<p>Eu gosto de pensar nas minhas máquinas como extensões de mim. Da mesma forma que seu corpo vai estragar se só consomir um monte de refrigerante, doces e pizza, sua máquina também vai estragar se só sair dando copy e paste de comandos e rodando no terminal. Fast food é fast food. Mas o ponto que quero chegar é que partindo da premissa que quem vai executar alguma coisa sabe o que tá fazendo, os programas antigamente eram bem mais ingênuos. Até porque não tinha tanto poder de processamento pra fazer coisas consideradas supérfluas, então o código sempre era o mais simples possível.</p>
<p>Vamos pegar o exemplo de hello world de node.js que mostrei no episódio anterior e adicionar algumas coisas. Acho que foi no episódio de aprendizado na beira do caos que mencionei de um programinha besta que tem em Linux chamado <code>cowsay</code> lembram? Você instala e no terminal executa assim <code>cowsay hello</code> e ele cospe uma vaquinha desenhada com caracteres ASCII mais a sua mensagem. Aí nos primórdios da Web, alguém poderia pensar, &ldquo;ulha, seria legal eu ter esse cowsay na web&rdquo;.</p>
<p>E por que não? Meu hello world do episódio passado é muito chato. Só imprime a mesma mensagem. Eu quero que o usuário consiga fazer a vaca dizer qualquer mensagem. Todo mundo precisa ver a carinha da vaca pra alegrar o dia. Eu quero poder digitar no navegador <code>http://localhost:3000?message=bom+dia</code> e ver a vaca me dar bom dia. Todo mundo precisa de um bom dia da vaca pra começar bem o dia.</p>
<p>A primeira coisa é conseguir pegar esse parâmetro de mensagem que vai na URL depois da interrogação. Em HTTP a URL não serve só pra passar o endereço do servidor, mas podem ir mais informações, como pares de chave e valor separado por &amp; comercial depois de uma interrogação. Todo mundo já deve ter visto isso, especialmente se já copiou e colou links de produtos em ecommerces como a Amazon, que notoriamente tem uns links sujos pra cacete. Pra pegar esses parâmetros, primeiro vou importar a biblioteca <code>url</code> pra processar a URL. E lá embaixo, na função que recebe a estrutura de request, faço o parsing da URL pra me devolver um objeto de query strings.</p>
<p>Pronto. Com isso tenho acesso àquele parâmetro que chamei de &ldquo;message&rdquo; no exemplo da URL. Agora quero conseguir executar o comando <code>cowsay</code> que instalei no meu Linux. Pra isso todas as linguagens tem a capacidade de instanciar um novo processo filho, literalmente conseguem rodar outro programa. No caso do Node carrego a biblioteca <code>child_process</code> e pego o comando <code>exec</code> dele. Já expliquei sobre forks, processos e coisas assim nos episódios de concorrência e paralelismo e no video de Ubuntu.</p>
<p>Agora vem o principal. Eu chamo a função <code>exec</code> com a query string &ldquo;message&rdquo; que tirei da URL e monto a string do comando de <code>cowsay</code> como fiz no terminal, usando interpolacão de strings. Toda linguagem tem o equivalente a interpolação de strings, se não sabem nem isso, estudem mais. Essa função <code>exec</code> vai executar o comando cowsay, e quando terminar chama essa outra função anônima que passei. Ela vai preencher pra mim o que o programa retornou, separado em objetos de erro, standard output ou stdout e standard error ou stderr.</p>
<p>Como é um exemplo besta, nem vou me preocupar com os casos de erro e ignorar completamente. Obviamente isso não é uma boa prática, mas é só pra simplificar neste exemplo. Quero só pegar o que o programa cuspiu no standard output e preencher a estrutura de response com ela. Pronto, agora salvo, volto pro terminal e executo o programa com o Node. Ele vai dar bind e ficar escutando na porta 3000 como antes. Podemos voltar pro navegador e digitar aquela URL que mostrei antes, localhost 3000 message igual bom dia.</p>
<p>E boom, olha que bonitinho, mostrou a vaquinha no meu navegador como eu queria. E era assim que muitos sites nos primórdios da Web faziam. Delegavam alguma coisa pra algum programa já instalado no seu sistema, pegavam o que cuspia no stdout, e devolvia no meio de um HTML pro usuário. No meu exemplo besta, nem me preocupei com HTML, só cuspi um texto puro mesmo. Iniciantes podem achar que acabou, mas agora que começam os problemas. Se você não viu nenhum problema no que eu fiz, sabemos que realmente ainda é um iniciante. Não se preocupe, vamos abrir seus olhos.</p>
<p>Vamos voltar pro terminal no Linux. Se não sabia disso, um shell como Bash ou ZSH ou todos eu acho, permite digitar múltiplos comandos numa mesma linha, um atrás do outro, basta separar por ponto e vírgula. Vamos tentar? Deixa eu digitar <code>cowsay hello; ls -la</code> e olha o que acontece. Ele executa o cowsay e printa na tela, mas logo em seguida executa o <code>ls -la</code> e printa embaixo. É um jeito de você fazer one-liners, que são programinhas que cabem numa única linha antes de dar enter. Pra scripts isso é muito útil. Todo mundo que mexe com Linux sabe disso.</p>
<p>Não precisa ser muito astuto pra pensar. “Hmm, será que eu consigo fazer isso do navegador?” Vamos tentar. Depois da mensagem posso digitar ponto e vírgula e algum comando. E olha só o que voltou!! O meu programa em Node executou o comando seguinte ao cowsay. E agora, eu consigo navegar por todos os arquivos do servidor e executar comandos arbitrários!!! E você pode pensar “ah, mas só dá pra listar coisas, por que isso seria um problema?”. Ok, vamos fazer outro exemplo <a href="http://localhost:3000?message=fodeu;cat"target="_blank" rel="noopener">http://localhost:3000?message=fodeu;cat</a> ~/.ssh/id_rsa.</p>
<p>Sabe o que eu fiz? Acabei de pegar a chave privada de SSH desse usuário, que é um arquivo que jamais deveria poder ser exposto fora desse diretório, por motivos de segurança. Com ela consigo invadir conta de GitHub, Amazon e tudo mais que essa chave conseguir usar. Eu posso até baixar arquivos binários, basta passar pelo comando <code>base64</code> pra transformar num texto, vai aparecer no navegador, copio e colo com Notepad em algum lugar e faço decode do base64 de volta pra string. Expliquei isso no video de detecção e correção de erros. Consigo imaginar várias coisas que dá pra fazer só com isso. Se dá pra executar qualquer comando, eu já tenho controle sobre essa máquina inteira. Eu ownei essa máquina. É game over.</p>
<p>Esse meu programa é exatamente o que um programador amador poderia pensar em fazer: o mínimo de código pra fazer a idéia funcionar. E de fato funcionou, a vaquinha aparece como deveria. E normamente esse mínimo nunca assume que alguém teria a idéia de tentar coisas que o programa não foi desenhado pra fazer. Mas é assim que pensa todo hacker: como eu posso explorar os programas de formas que o autor não previu? E um programador minimamente mais experiente já sabe: eu deveria ter sanitizado o que veio como query string.</p>
<p>Hoje não é o dia de falar sobre vulnerabilidades, mas isso que acabei de fazer se chama injection, ou injeção, porque estou injetando comandos arbitrários num parâmetro que não foi devidamente tratado, ou sanitizado. Sanitizar significa considerar que tudo que vem do mundo externo de um usuário, por definição, é podre, sujo, e precisamos sanitizar, antes de usar. Sanitizar significa remover caracteres especiais, como esse ponto e vírgula, ou escapar esses caracteres pra serem tratados como string e não como comandos. Existem dezenas de bibliotecas que fazem isso.</p>
<p>Frameworks como o Express, que é feito em cima do Node, justamente adiciona coisas como sanitização já sabendo que a maioria dos programadores não se preocupa com isso. Mas o objetivo desse exemplo besta foi só pra demonstrar como nós fazemos programas ingênuos quando somos iniciantes, e se for só pra nós mesmos, pra usar no nosso próprio computador, tudo bem. Mas numa aplicação web que vai pra produção e vai ter gente de verdade usando, todo cuidado é pouco.</p>
<p>Mas o ponto é que programas podem ser usados de formas pras quais eles não foram desenhados. No final vou voltar nesse código, mas esta próxima parte é voltando pros conceitos de rede que vim explicando nos últimos episódios. Em particular a idéia de sockets e portas. Como disse, antigamente a gente era bem mais ingênuo e não parava pra pensar que existiam pessoas maliciosas ou simplesmente só curiosas. É o oposto do que se aprende em cursos e tutoriais.</p>
<p>Agora imagina. Povo tá empolgado com o nascimento da internet e começa a colocar programinhas ingênuos como o que eu fiz aberto em portas de servidores com endereço IP público válido, como nas universidades da época. Servidores que agora qualquer pessoa poderia acessar. E no meio deles tá esse site de vaquinha. O objetivo nem era deixar público, só queria mostrar pros colegas da faculdade, mas sem querer agora deixei um buraco de segurança enorme aberto. Por isso que nunca um código de junior pode ir pra produção sem ter passado pela revisão de um ou mais sêniors, no mínimo.</p>
<p>Existem várias formas de um administrador de sistemas bloquear essas tentativas de ataque mesmo sem a colaboração do autor da vaquinha. Uma delas e a mais comum é criar políticas como, pra estar na porta 80 ou 443, precisa ser um site feito por alguém de confiança da faculdade ou instituição. Sites feitos alunos ou outras pessoas, até podem rodar internamente em portas diferentes, dentro do servidor do departamento, mas não vão ser expostos na internet pública. Todo mundo pode subir sites nesse servidor hipotetico, em portas como a 3000 que eu usei ou qualquer outra como 4000 ou 8080.</p>
<p>O site oficial roda na porta 80 desse servidor. Todas as outras portas deveriam estar fechadas pro público. E assim nasce o conceito de firewalls. Firewall é um programa, como qualquer outro, que roda com privilégios de administrador ou root do sistema. Isso porque ele precisa interceptar todos os pacotes que chegam ou saem pela placa de rede e filtrar pra saber se podem ou não prosseguir. Sem entrar em detalhes, existem dois tipos básicos de regras, uma pra permitir coisas e outra pra rejeitar coisas, allow ou deny.</p>
<p>Sem nenhum firewall é como se tivesse um firewall com nenhuma regra de deny e allow asterisco, ou seja, aceita tudo. Segurança completa é deny tudo e nenhum allow, mas aí o servidor seria meio inútil. No caso do servidor dessa faculdade de exemplo, a regra poderia ser deny tudo mas allow in, ou entrada, na porta 80. Assim, eu poderia ter meu site de vaquinha interno na porta 3000, mas se alguém de fora, da internet pública, tentasse acessar a porta 3000, a regra do firewall diz que é pra rejeitar, então rejeita todos os pacotes endereçados pra 3000 e nunca chega no meu sitezinho.</p>
<p>Mesmo numa rede interna de faculdade ou empresa, isso ainda não é suficiente. Porque em segurança a gente suspeita de tudo e de todos, incluindo pessoas internas. De novo, não é porque eu acho que todo mundo tem más intenções, mas porque justamente as pessoas de boa intenção, que não enxergam mau nos outros, são os mais fáceis de enganar com coisas como malwares. Você nunca sabe se um aluno instalou um malware que veio por email sem querer e agora no notebook dele tem um malware pendurado numa porta qualquer e vasculhando por portas abertas.</p>
<p>Por isso os firewalls também bloqueiam que um notebook numa rede consiga tentar se conectar numa porta de outro notebook na mesma rede. Ele dá deny de todas as portas acima de 1024, por exemplo. Assim, mesmo se eu subir a vaquinha na porta 3000 da minha máquina, outro usuário na mesma rede não vai conseguir acessar, mesmo sabendo meu endereco IP. O firewall vai bloquear. E é assim mesmo até hoje em muito ambiente corporativo. As regras costumam ser as mais fechadas possível pra justamente evitar o acidente de alguém largar uma vaquinha dando bandeira e alguém malicioso se aproveitar disso.</p>
<p>Agora faz de conta, sou um aluno dessa faculdade. Eu fiz lá a vaquinha e subi no servidor na porta 3000 e sei que tá bloqueada do lado de fora. Mas eu queria porque queria poder mostrar isso pros meus amigos lá fora. Só que o administrador da rede nunca que vai deixar eu pendurar meu site publicamente do servidor da faculdade. Por acaso eu tenho uma máquina virtual de grátis que ganhei pra testar da DigitalOcean, que é um serviço de VPS ou servidores virtuais de aluguel. Eu poderia instalar lá, mas sabe? Tô com preguiça, porque já tá funcionando aqui no servidor da faculdade e só depois descobri que as portas tavam fechadas. O que eu posso fazer? Tá tudo bloqueado, mas ao mesmo tempo eu sei que consigo navegar na web. Então nem tudo tá bloqueado, no mínimo portas 80 e 443 tão abertas pra sair. Eu posso usar isso a meu favor.</p>
<p>De novo vou repetir, portas são só números. Por convenção 80 é pra HTTP e 22 é pra SSH. Vamos rapidinho criar uma nova máquina na DigitalOcean. Vou escolher um Ubuntu numa máquina fraquinha, no data center dos Estados Unidos. Espero terminar. E copio o endereço IP público valido que ele me dá. Depois de ter criado uma máquina numa DigitalOcean da vida, consigo logar usando o comando <code>ssh root@ip da máquina</code> e como não indiquei nenhuma porta, o ssh vai tentar na porta 22 padrão. Coloco minha senha, e pronto, entrei na máquina remota. Estou mostrando o endereço IP de verdade porque depois de hoje vou destruir essa máquina, então não vai mais funcionar se você tentar conectar agora.</p>
<p>A DigitalOcean fornece endereços válidos na internet pra máquina que eu alugo. Numa máquina baratinha ele fica mudando o endereço pra outro. Se eu quiser ter sempre o mesmo, posso pagar a mais pra reservar pra mim. Lembra minha explicação que não existem endereços IPv4 pra todo mundo? É por isso. Pra ter um válido, precisa pagar mais caro. Mas o ponto é que qualquer um na internet consegue enxergar, ou seja, rotear pacotes pra essa máquina.</p>
<p>Enfim, o programa em si que tá rodando no servidor pendurado na porta 22 se chama <code>sshd</code> ou ssh daemon. Daemon é todo programa que o sistema operacional gerencia, ele inicializa depois do boot e fica checando se ele crashear, daí tem regras se tenta carregar de novo e assim por diante. Estude sobre systemd, OpenRC ou runit, que são gerenciadores de daemons que a maioria das distros Linux roda. Mas enfim, saiba que eles que são responsáveis por garantir que programas como o SSH carreguem sozinhos após cada boot. Graças a esse programa daemon aberto e pendurado na porta 22 que do meu notebook consigo executar o programa ssh apontando pro endereço de lá e abrir uma conexão segura de terminal remoto.</p>
<p>Agora vai ter alguns detalhezinhos mas não é importante decorar, só acompanhar o raciocínio. O que eu vou fazer é o seguinte, de dentro dessa máquina remota, vou editar o arquivo <code>/etc/ssh/sshd_config</code> e habilitar essas opções comentadas de Allow Tcp Forwarding e Gateway Ports pra yes, e lá embaixo vou mudar a porta de 22 pra 80. Viu, eu posso pendurar programas em qualquer porta e eu escolhi mudar o daemon de SSH pra porta 80. Agora salvo e uso o comando do systemd <code>systemctl restart sshd</code> pra reinicializar o daemon e carregar as novas configurações, agora posso sair do ssh com exit.</p>
<p>Se eu tentar o mesmo comando <code>ssh</code> pra conectar no servidor, vai falhar, porque como eu disse, vai tentar por padrão na porta 22. Só que mudei pra porta 80. E agora? Como faz? Normalmente esses programas de rede sempre tem uma opção pra manualmente escrever a porta, só colocar <code>-p 80</code> no final e bingo, olha só, conectou direitinho. Agora que a brincadeira começa.</p>
<p>Aquela minha vaquinha tá rodando localmente no tal servidor hipotético da faculdade na porta 3000. Faz de conta que meu PC aqui de desenvolvimento é esse servidor da faculdade. Por isso vou no navegador e digito 127.0.0.1 ou localhost, que são os endereços locais da minha máquina.</p>
<p>Não adianta eu passar esse endereço pra ninguém, porque significa &ldquo;minha máquina&rdquo; como expliquei no episódio anterior. Na máquina do seu vizinho, se ele digitar a mesma coisa, não vai carregar nada porque não tem nada pendurado na porta 3000 da máquina dele. Mas eu quero que meus amigos de fora consigam ver a vaquinha. Pra isso preciso de um endereço IP público válido e que tenha portas abertas sem bloqueio de um firewall.</p>
<p>Pra isso vou usar um dos super poderes do SSH, o recurso chamado Remote Forwarding. No terminal do servidor da faculdade, eu digito <code>ssh -R</code> pra remote forwarding e agora fica complicadinho <code>ip do servidor:7000</code> depois <code>127.0.0.1:3000</code>. Estou falando o seguinte, toda vez que alguém tentar se conectar no endereço do meu servidorzinho na DigitalOcean, nessa porta 7000, faça forwarding, redirecione o tráfego, pra minha máquina local 127.0.0.1 porta 3000, que é onde tá rodando a vaquinha.</p>
<p>Daí termino com as mesmas informações de antes pro login, usuario root arroba endereço e <code>-p</code> pra porta 80 que é onde o sshd tá ouvindo. Eu sei, ficou um comandão enorme, mas depois revejam com calma e entendam cada parâmetro. O que esse comando diz é: conecte no meu programa de sshd na porta 80 do servidor da DigitalOcean e se pendure na porta 7000 de lá e fique escutando. Toda vez que vier alguma requisição por lá, faça forwarding, redirecione todo o trafego, pra porta 3000 da minha máquina local.</p>
<p><code>ssh</code></p>
<p>Explicando fica complicado. Então vamos mostrar na prática. Deixa eu abrir aqui meu celular que tá conectado via 4G, ou seja, ele não teria acesso ao tal servidor hipotético da faculdade. Vamos digitar o endereco IP público do servidor da Digital Ocean, válido na internet e colocar dois pontos 7000. E voilá, olha a vaquinha aqui, aparecendo em público!  Como pode isso? O firewall não tava travando? O que aconteceu?</p>
<p>Vou repetir. No navegador do celular eu acessei um endereço IP válido da Digital Ocean. Essa máquina virtual tá vazia. É um Ubuntu recém instalado. A única modificação que eu fiz foi configurar o programa de sshd pra se pendurar na porta 80 em vez da 22 que seria padrão. Dessa forma, eu, pobre aluno da faculdade que está atrás de um firewall bem rígido que bloqueia portas como 22, consigo acessar via a porta 80, que está aberta porque a faculdade pelo menos permite as pessoas navegar na web. E pra isso, obrigatoriamente o firewall é obrigado a deixar passar saída na porta 80. Entenderam? O firewall bloqueia qualquer coisa de entrar na rede, mas precisa permitir pacotes saírem pras pessoas conseguirem navegar na Web, e pra isso no mínimo saída pela porta 80 estava aberta. Portanto, havia esse buraco que poderia ser explorado.</p>
<p>O firewall deixa pacotes sair, mas não deixa nada entrar de jeito nenhum, então mesmo o servidor da faculdade tendo um endereço IP valido, o firewall não deixaria ninguém se conectar na minha porta 3000. O que eu fiz com SSH é o que se chama de poking a hole, ou literalmente fazer um buraco no firewall, é assim que se fura um firewall do lado de dentro. O grande lance é que do servidor da DigitalOcean ele não consegue me achar na internet e se conectar em mim, mas eu consigo achar e me conectar nele, via a única porta aberta de saída, a 80. E uma vez aberta a conexão, daí meu servidor de fora consegue falar comigo via essa conexão que eu estabeleci com o programa SSH.</p>
<p>Se pra você isso é novidade, respire um segundo e reveja. O servidor da DigitalOcean bem como qualquer outra pessoa na internet não tem como se conectar no servidor porque o firewall bloqueia, mas se a conexão se inicia do lado de dentro, desse servidor, uma vez estabelecida a conexão pra fora, agora ambas as pontas conseguem se comunicar.</p>
<p>Esse é um truque que eu já usei inúmeras vezes ao longo dos anos. A primeira vez que aprendi a fazer isso acho que nem existia SSH. A gente usava um programinha chamado HTTP Tunnel, que faz o oposto do que acabamos de fazer, o recurso de Local Forwarding do SSH. Deixa eu dar um outro exemplo. Acontece que só fechar todas as portas mas manter a porta 80 e 443 de Web abertas, continua sendo um enorme risco de segurança, porque vai saber quem dentro da empresa que não tá entrando em site pornô, site de pirataria e uma hora esbarrando em sites com malwares. Pra evitar isso acho que até hoje tem empresas que obrigam todo mundo a usar um proxy.</p>
<p>Se você abrir um navegador recém instalado numa empresa mais segura, não vai conseguir navegar pra lugar nenhum. O administrador da rede da empresa vai te instruir a abrir a configuração de proxy do seu sistema operacional e colocar um endereço IP e uma porta que aponta pro programa de proxy. Isso pode ser no nível do sistema operacional ou nas configurações do navegador.</p>
<p>Lembra no episódio passado que fiz um exemplo de conectar num servidor web local usando telnet e digitando manualmente o cabeçalho de requisicão HTTP? Lembra? Comando GET seguido de barra e o nome da pagina html que quiser, terminando com HTTP 1.0? Um cabeçalho mais completo, que um navegador enviaria, seria parecido com isso aqui.</p>
<p><code>GET / HTTP/1.1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 DNT: 1 Accept-Encoding: gzip, deflate, sdch Accept-Language: pt-BR,en;q=0.8</code></p>
<p>Ele dá bem mais detalhes pro servidor, como se apresentar dizendo que navegador que é, dizendo que línguas que aceita e coisas assim. É uma das formas de como serviços como Google Analytics conseguem saber coisas como qual navegador mais acessa seu site. Enfim. Se tiver um proxy configurado, o navegador não vai mais se conectar direto com o site que você quer, em vez disso sempre vai mandar todas as requisições pro servidor de proxy. E o pacote vai ser um pouquinho diferente. Assim.</p>
<p><code>GET http://www.mega.io/ HTTP/1.1 Host: www.mega.io Proxy-Connection: keep-alive Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8</code>
DNT: 1
Accept-Encoding: gzip, deflate, sdch
Accept-Language: pt-BR,en;q=0.8`</p>
<p>Olha só, no comando GET vai a URL inteira, com o domínio, uma nova linha dizendo que host estamos querendo nos conectar e uma configuração específica de Proxy. O Proxy vai receber essa requisição e modificar pra ficar igual à original sem os detalhes de proxy. E é proxy quem vai se conectar com o servidor do Mega, no caso. O site do Mega vai gerar um HTML, o pacote de response e devolver pro Proxy. Daí o proxy redireciona a resposta de volta pro meu navegador. Um proxy atua como um intermediário entre eu e os sites que quero navegar. Do ponto de vista do usuário, estou navegando normamente. Mas do ponto de vista dos sites que estou acessando, ele só enxerga o proxy como usuário.</p>
<p>Mais ou menos o efeito é parecido com o que eu expliquei de NAT e CGNAT no terceiro episódio da mini-serie. Internamente temos um endereço IP 192.168 que não é válido na internet. Todo pacote passa pelo roteador do provedor e é convertido num endereço válido, que é o que os sites enxergam. Mas todo mundo atrás do NAT vai ser visto com o mesmo endereço IP. Pros sites, é difícil dizer quem são os usuários só baseado nisso.</p>
<p>Mesma coisa com proxy, só que restrito ao tráfego de Web. Em vez de NAT os pacotes passam pelo Proxy, e os sites só enxergam um endereço IP público da empresa, sem saber com certeza os usuarios atrás. A gente rastreia usuários com outros dados, como o cookie do navegador, que tem o login de cada usuário. Outra hora explico sobre cookies, mas foi só pra dizer que em termos de conceito, NAT e Proxy tem resultados semelhantes do ponto de vista dos sites.</p>
<p>A vantagem disso pra empresa é óbvia. Proxies tem filtros com whitelists e blacklists. Meio como um firewall ele pode dizer o seguinte: ninguém pode navegar em nenhum site da internet, com exceção dos domínios que o administrador configurar na whitelist. Assim ele pode restringir em quais sites os funcionários podem navegar. Sites pornô e de entretenimento provavelmente estão na blacklist da empresa. E adivinha, se o administrador não tiver colocado stackoverflow.com, você se fodeu.</p>
<p>Mais do que isso, ele pode monitorar o conteúdo de tudo que tá passando e registrar. Isso é mais difícil hoje porque usamos HTTPS e aí ele tem mais dificuldades de vigiar o conteúdo do que você tá navegando porque vai tudo criptografado. Mas como expliquei antes, na requisição fica aberto que site você tá tentando acessar, e o Proxy consegue registrar isso. Só de restringir onde pode navegar já é um pé no saco. É compreensível, muita empresa não gosta de ver os funcionários perdendo tempo em redes sociais, então bloqueiam coisas como YouTube ou Facebook. É um pensamento super retrógrado, que era comum no começo dos anos 2000 e eu sei que tem empresas que pensam assim até hoje, infelizmente.</p>
<p>Pois bem. No exemplo da faculdade eu dei um cenário hipotético onde o administrador fez um firewall que bloqueia a saída de pacotes pra 100% das portas com exceção das de web. Mas na prática acho que quase ninguém faz isso. Seria muito pouco produtivo. Você acabaria bloqueando portas úteis como as acima de 49 mil até 65 mil que servem pra coisas como Zoom e outros software de reunião online ou mesmo portas como 25, 193 que são pra clientes de email. Mas as portas de web ficam fechadas e te obrigam a configurar proxy. Então dessa vez não temos portaa 80 de saída pra trabalhar.</p>
<p>Não vou mostrar na prática, mas a solução é simples. Com o mesmo servidor na Digital Ocean que eu tenho, posso configurar aquele daemon sshd não pra porta 80 mas sim pra uma porta como 50000. Uma porta alta que provavelmente não vai estar bloqueado nos firewalls. O remote forwarding que eu fiz, com aquela opção <code>ssh -R</code>, permite alguém de fora acessar um site meu rodando numa máquina privada dentro da rede. Mas o oposto também é possível. Eu posso rodar o seguinte comando, um pouco mais complicadinho: <code>ssh -D 1337 -q -C -N root@ip do servidor de ssh -p 50000</code></p>
<p>Opções como <code>-q</code> é quiet mode, pra não ficar imprimindo nada no meu terminal, <code>-C</code>maiúsculo  é pra comprimir o conteúdo que passar por esse programa. <code>-N</code> é pra não abrir uma linha de comando pro servidor. Mas o mais importante é esse <code>-D 1337</code>. Isso vai pendurar esse programa, fazer bind, nessa porta 1337 na minha máquina local. Poderia ser qualquer outro número de porta. O resto é a mesma coisa pra conectar no sshd apontando pra porta 50000 do servidor, o que provavelmente vai conseguir furar o firewall.</p>
<p>Agora, no meu sistema operacional ou direto no navegador, eu procuro a configuração de proxy de novo, mas em vez de colocar o endereço do proxy da empresa, coloco localhost e porta 1337. E pronto, agora posso navegar normalmente por qualquer site que quiser, mesmo se a empresa achar que está me bloqueando. O que fiz foi criar um túnel via SSH, que fica exporto como um serviço SOCKS5, que é o protocolo de um Proxy Web, e pendurado na porta local 1337 da minha máquina. Eu fiz um proxy pessoal, que recebe pacotes HTTP normalmente, como um servidor web, só que ele dá forward, redireciona o tráfego, pro meu servidor da Digital Ocean via esse túnel criptografado via ssh.</p>
<p>O serviço sshd de lá recebe os pacotes que vieram pelo túnel e navega usando a internet da Digital Ocean, que tá toda aberta. O site que eu quis navegar devolve a resposta HTTP pro servidor, e o ssd redireciona o pacote de volta pra mim pelo mesmo túnel. E assim eu burlo toda tentativa de me restringir de navegar. Posso navegar onde quiser. Mesmo a saída da porta 80 na empresa estando fechada por firewall, mesmo sendo instruído a usar o servidor de proxy da empresa, eu criei o meu próprio servidor de proxy saindo por uma porta alta que provavelmente tá aberta no firewall e passei a navegar sem restrição nenhuma. Isso é um exemplo do que se chama de tunelamento.</p>
<p>Mais uma curiosidade. Antes, se eu fosse num site como what is my ip, ia mostrar o endereço IP do proxy da empresa. É a mesma coisa como no caso de CGNAT de provedor. Meu computador tem um endereço privado tipo 192.168 da vida, que não é válido pra rotear na internet. Então todo mundo da rede da empresa vai ter o mesmo endereço IP público, seja por causa de NAT, seja por causa de Proxy. Agora, se navego via o túnel pro servidor da Digital Ocean, o que o site what is my ip vai ser é o endereço IP do servidor. Não só isso, se ele tentar encontrar a posição geográfica desse IP vai ver que está em Nova Iorque, porque eu escolhi montar meu servidor no data center de Nova Iorque. Como programador é importante entender isso: os dados de usuário que sua aplicação web recebe normalmente não representa o computador do usuário, porque a maioria está escondido em redes privadas de NAT ou atrás de Proxy de empresas.</p>
<p>De qualquer forma, com esse truque de tunelamento de SSH, significa que não só burlei as restrições da tal empresa hipotética, como agora posso assistir Netflix como se estivesse nos Estados Unidos, tendo acesso a conteúdo que não tem no Brasil. Então eu burlei também a restrição de região do Netflix. E se o que eu acabei de falar pareceu com aquelas propagandas sobre VPN, é porque tem a ver.</p>
<p>Esse é só um exemplo de coisas que conseguimos fazer quando sabemos um pouco sobre redes. No próximo episódio vou falar um pouquinho de VPN. Por hoje queria mostrar um pouco mais de que tipo de coisas você está perdendo quando se recusa a entender um pouco mais sobre redes. A pessoa hipotética que fez o site de vaquinha e conseguiu disponibilizar na internet com túnel de SSH eu poderia dizer que é o oposto da maioria dos programadores: um péssimo programador, que fez um site cheio de buracos de segurança, mas um bom cara de redes que soube ultrapassar as restrições de firewall e abrir buracos no bloqueio.</p>
<p>Mas no geral ele ganha nota negativa, porque alguém colocar um site que não entende que é inseguro, à força na internet o torna um péssimo profissional. Eu sou o tipo que gosta de quebrar as regras e sempre fiz isso, mas quando faço eu sei os riscos que estou correndo. Não seja esse cara irresponsável, aprenda de tudo um pouco e saiba onde pode ou não pode usar cada coisa. Por exemplo, lá no começo eu disse que faltou sanitizar a mensagem do usuário pra impedir ele de tentar injetar comandos arbitrários pra rodar no servidor. Como que faz isso? Antes de terminar o episódio vamos pelo menos fazer isso.</p>
<p>Existem dezenas de bibliotecas e frameworks como Express já vem preparado pra isso. Mas nesse exemplo besta, vamos fazer nossa própria função. Veja esta função <code>sanitize</code>, ela recebe uma mensagem e passar por uma expressão regular, uma regex. Se não conhecem regex, precisa estudar, é obrigatório saber. Essa expressão procura na mensagem tudo que não é letra minúscula, letra maiúscula ou números e com a função <code>replace</code> ele vai trocar por uma string vazia. Ou seja, coisas com o ponto e vírgula vai ser substituída por string vazia. Agora lá embaixo eu passo a mensagem que peguei da URL por essa função e só depois concateno com o comando <code>cowsay</code>.</p>
<p>Vamos no terminal, rodar essa nova versão do programa e voltar pro navegador. Chamamos http localhost interrogação mensagem com ponto e virgula e um comando como ls e olha só, ele não executa mais o comando, porque eu quebrei a sintaxe. Se alguém achar esse site no ar e tentar executar comandos, não vai mais conseguir, porque só com letras e números, não tem como concatenar outros comandos pra executar e com isso bloqueamos todas as tentativas de injeção. Essa função não é a melhor, porque ela apaga todo caracter especial, então não consigo usar pontuação nas mensagens. Isso é só um exemplo, tem jeitos melhores de fazer isso, mas foi só pra mostrar como é o jeito mais simples e mais drástico de sanitizar.</p>
<p>O recado é simples: tudo que um usuário manda, seja como parâmetros na URL, seja como campos num formulário, tudo deve ser sempre considerado suspeito e deve passar por sanitização. Todo usuário é mau e malicioso até prova em contrário, é assim que devemos considerar todo mundo que acessa seu site. Todo mundo que vai usar seu programa vai tentar quebrar ele. Como programador você tem que estar ciente disso. E é simples: em algum lugar no seu programa você permite o usuário enviar alguma informação? Essa informação é material radioativo, trate ele como tal.</p>
<p>Não tem a ver com o episódio, mas já que falei isso vale outro aviso. Tem gente que fica paranóico demais e faz programação defensiva em excesso e sai colocando tratamento de parâmetros em todas as funções que faz. E está errado. Toda função que é usada internamente não precisa sanitizar tudo toda hora. Só funções que explicitamente recebem dados vindos diretamente de um usuário que precisam disso. Cuidado pra não sair dando copy e paste de sanitização em todo lugar que não precisa e ficar redundante. Ter cuidado não significa atirar pra todo lado. Um bom programador usa sniper, mira e atira com precisão só um tiro e acerta o alvo. Quem atira pra todo lado é claramente amador.</p>
<p>Eu quis fazer esse episódio porque muita gente usa SSH pra se conectar em servidor mas não sabe o que o SSH realmente consegue fazer. Espero que isso tenha servido pra mostrar que você usa ferramentas sem saber pra que elas realmente servem. Interesse-se mais em explorar o que você usa no seu dia a dia. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e não deixem de compartilhar o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>seguranca</category><category>redes</category><category>backend</category><category>akitando</category></item><item><title>[Akitando] #124 - Como Funciona Sockets, Cliente, Servidor e a Web? | Introdução a Redes Parte 4</title><link>https://akitaonrails.github.io/2022/08/04/akitando-124-como-funciona-sockets-cliente-servidor-e-a-web-introducao-a-redes-parte-4/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/08/04/akitando-124-como-funciona-sockets-cliente-servidor-e-a-web-introducao-a-redes-parte-4/</guid><pubDate>Thu, 04 Aug 2022 10:21:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/lc6U93P4Sxw"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Agora que todo mudno sabe como funciona o básico de rede, vamos ver como funciona o básico de aplicações de rede, o que são sockets, pra que eles servem, como disso chegamos em protocolos como HTTP ou FTP e os vários detalhezinhos que complicam a cabeça de um iniciante em programação Web.&lt;/p&gt;
&lt;h2&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;01:36 - Noção de Sockets&lt;/li&gt;
&lt;li&gt;02:26 - Entendendo Comunicação Entre Processos&lt;/li&gt;
&lt;li&gt;03:18 - Memória Protegida/Isolada Impede Comunicação&lt;/li&gt;
&lt;li&gt;05:30 - Comunicação via Pipes&lt;/li&gt;
&lt;li&gt;07:18 - Comunicação via Arquivos&lt;/li&gt;
&lt;li&gt;09:56 - BSD Sockets&lt;/li&gt;
&lt;li&gt;12:43 - Fluxo de conexão&lt;/li&gt;
&lt;li&gt;16:54 - Portas&lt;/li&gt;
&lt;li&gt;19:55 - Recapitulando navegador/DNS&lt;/li&gt;
&lt;li&gt;21:15 - Exemplo de server web com Node&lt;/li&gt;
&lt;li&gt;22:17 - localhost é diferente de 0.0.0.0&lt;/li&gt;
&lt;li&gt;23:43 - Continuando Exemplo de Node&lt;/li&gt;
&lt;li&gt;24:19 - Portas Efêmeras&lt;/li&gt;
&lt;li&gt;25:47 - Continuando Exemplo de Node&lt;/li&gt;
&lt;li&gt;29:00 - Debugando com Telnet e Developer Tools&lt;/li&gt;
&lt;li&gt;31:57 - Camadas de Segurança&lt;/li&gt;
&lt;li&gt;34:07 - Big Loop&lt;/li&gt;
&lt;li&gt;37:24 - OSI, TCP, HTTP&lt;/li&gt;
&lt;li&gt;39:28 - Mostrando FTP&lt;/li&gt;
&lt;li&gt;42:25 - De redes distribuídas a &amp;ldquo;walled gardens&amp;rdquo;&lt;/li&gt;
&lt;li&gt;45:06 - Bloopers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal, Fabio Akita&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/lc6U93P4Sxw"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Agora que todo mudno sabe como funciona o básico de rede, vamos ver como funciona o básico de aplicações de rede, o que são sockets, pra que eles servem, como disso chegamos em protocolos como HTTP ou FTP e os vários detalhezinhos que complicam a cabeça de um iniciante em programação Web.</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>01:36 - Noção de Sockets</li>
<li>02:26 - Entendendo Comunicação Entre Processos</li>
<li>03:18 - Memória Protegida/Isolada Impede Comunicação</li>
<li>05:30 - Comunicação via Pipes</li>
<li>07:18 - Comunicação via Arquivos</li>
<li>09:56 - BSD Sockets</li>
<li>12:43 - Fluxo de conexão</li>
<li>16:54 - Portas</li>
<li>19:55 - Recapitulando navegador/DNS</li>
<li>21:15 - Exemplo de server web com Node</li>
<li>22:17 - localhost é diferente de 0.0.0.0</li>
<li>23:43 - Continuando Exemplo de Node</li>
<li>24:19 - Portas Efêmeras</li>
<li>25:47 - Continuando Exemplo de Node</li>
<li>29:00 - Debugando com Telnet e Developer Tools</li>
<li>31:57 - Camadas de Segurança</li>
<li>34:07 - Big Loop</li>
<li>37:24 - OSI, TCP, HTTP</li>
<li>39:28 - Mostrando FTP</li>
<li>42:25 - De redes distribuídas a &ldquo;walled gardens&rdquo;</li>
<li>45:06 - Bloopers</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No começo dessa mini-série eu ia só até o episódio passado, mas depois que escrevi, resolvi estender mais três episódios. Estou pulando toda uma parte sobre rede física, MLPS, OLDM, Wimax, 802.11. Vale estudar o livro de redes do Tanenbaum pra esses detalhes, o objetivo é focar mais em assuntos que ajudem programadores e não quem está interessado em seguir na carreira de infraestrutura, por isso eu sei que pra quem é de telecom ou infra tem bastante coisa simplificada. Agora quero ir direto pra camada de transporte e aplicação e nos próximos episódios falar assuntos que o Tanenbaum não chega a explicar no livro.</p>
<p>Só antes de continuar, tem uma errata que esqueci de falar nos videos anteriores, sobre o primeiro video de redes. Lá eu tava tentando simplificar com metáforas pra falar que ondas como de rádio e internet wireless se parecem com ondas na água, mas do jeito que falei ficou parecendo que wireless precisa de um meio como ar pra se propagar, mas claro que não, ondas na água são diferentes de ondas eletromagnéticas, que se propagam no vácuo, diferente de som. Por isso que no espaço não tem barulho, mas satélites conseguem se comunicar. Já tá na errata do episódio mas achei melhor esclarecer.</p>
<p>Todo programador vai ter que lidar com coisas como endereços IP, que já expliquei, e também com as tais portas. Toda vez que você roda um <code>npm start</code> da vida, sobe uma aplicação web nas tais portas 3000 ou 4000. Todo tutorial só menciona isso: magicamente funciona. Mas não explica o que está acontecendo de verdade. É hora de entender a mágica. O episódio de hoje é sobre sockets.</p>
<p>(&hellip;)</p>
<p>Em resumo bem resumido, pense em sockets literalmente como tomada da sua casa. Diferentes portas tendo diferentes padrões de buracos, como o padrão brasileiro sendo uma porta, padrão europeu sendo outra, padrão americano outra e assim vai. E só plugues corretos encaixam em determinadas tomadas. Por isso seu notebook importado não encaixa na porcaria da tomada do Brasil. +</p>
<p>Na prática o conjunto de endereço IP e porta de saída, digamos, do seu navegador conectando num site, e o endereço IP e porta de entrada do tal site, essas quatro informações é o mínimo pra definir um socket. É o plug e a tomada conectada. Não se atenha tanto ao nome mas a esse conjunto de informações, é o que se precisa pra formar uma conexão de rede entre dois processos, que é o caminho que os pacotes que falamos antes vão trafegar.</p>
<p>Sockets, na verdade, é uma forma de comunicação entre processos. Todo sistema operacional existe pra gerenciar processos. Expliquei isso nos videos de Programação pra Iniciantes, em particular nos de concorrência e paralelismo e gerenciamento de memória. Processo é todo programa que tem rodando no seu computador, seja seu spotify ou banco de dados. No nível mais básico, um processo é representado por um número, um process ID ou PID que o sistema operacional dá pro seu programa. Quando do terminal de um Linux se digita <code>ps -ax</code> dá pra ver os processos e seus PIDs. O sistema operacional aloca uma certa quantidade de memória e internamente, seu processo acha que tem acesso a toda memória teórica de 64-bits, mas na realidade o sistema operacional é quem intermedia o acesso à RAM ou Swap de disco.</p>
<p>Isso é importante. Um processo é como uma criança rica mimada gastando cartão de crédito dos pais. Ele não tem noção que tem limite, e só vai gastando, e os pais, que é o sistema operacional, vai sempre só pagando, então essa criança tem a ilusão que tem dinheiro infinito. É mais ou menos assim. Seu PC tem só 8 giga de RAM real, mas seu processo acha que tem do endereço 0 até 2 elevado a 64 de memória, que seria o máximo teórico de 16 exabytes. Nenhum computador de verdade tem exabytes de RAM ainda. Isso é importante de entender então vou insistir com outra metáfora.</p>
<p>Cada processo rodando na máquina enxerga uma memória virtual, de 0 até 16 exabytes. Quando falamos que se consegue endereçar memória, na realidade pense assim: Imagine um livro com uma parte de índice no começo. O índice tem espaço pra 16 exabytes de linhas, mas o livro em si não tem 16 exabytes de páginas, saca? Todos os processos estão compartilhando a mesma memória física de 8 gigabytes reais, mas ele só enxerga o índice.</p>
<p>O sistema operacional intermedia e garante que um processo não consiga acessar a memória real inteira, as páginas do livro, de outro processo. Por isso falamos que rodam isolados, sem um sobrescrever a memória do outro. Eu explico sobre isso nos episódios sobre gerenciamento de memória. Mas a parte importante é entender que um processo não enxerga nunca a memória de outro processo. Ele acha que está sozinho na máquina.</p>
<p>Antigamente, nos primeiros computadores sem isolamento de memória, um processo poderia escrever num endereço de memoria real e um outro processo poderia ler essa informação diretamente desse endereço. Um livros sem índices. Na época dos computadores de 8 e 16 bits era mais ou menos assim. Até a Intel inventar memória protegida. Todo processo enxergava todas as páginas e poderia escrever em qualquer lugar. Endereçar memória diretamente era mais rápido mas levava a dezenas de bugs de um processo escrevendo em cima de endereços que outro processo tava usando, levando desde a comportamento não-determinístico até a crashes. Isso era menos ruim quando a gente só rodada um programa de cada vez, na era do MS-DOS. Mas nos primeiros Windows isso já era um problemão.</p>
<p>Desde que passamos a isolar os processos e demos endereços virtuais em vez de reais, pelo menos essa categoria de bugs foi extinta, já na época do Windows 3.1 e foi melhorando a cada nova versão. Também significa que se um processo quiser se comunicar com outro processo, não tem como, porque não tem como gravar algo na memória que outro processo possa ler diretamente. As únicas duas formas de dois processos se comunicarem ou é via pipes ou via arquivos. Vamos entender.</p>
<p>Pipes de Linux é simplesmente ligar a saída padrão de um programa na entrada padrão de outro programa, o stdout ligado ao stdin via um pipe, ou cano. Que nem quando fazemos <code>cat algum_arquivo</code> pipe <code>grep alguma coisa</code>. Expliquei um pouco disso no episódio de Ubuntu se quiser relembrar. Isso significa que um programa roda primeiro, cospe algum resultado no stdout, e esse resultado é passado pro stdin, pra entrada, do próximo programa. É uma forma de dois programas diferentes se comunicar, mas é uma forma serial: um programa precisa rodar primeiro até o fim, terminar, e só depois começa o outro.</p>
<p>Portanto é mais complicado se eu quero os dois programas rodando em paralelo, sem um esperar o outro terminar. A única outra solução é um programa ficar cuspindo coisas num arquivo e o outro programa ficar lendo desse arquivo. Esse arquivo serve de ponte, de stream de entrada e saída pra ambos, criando um tipo de conexão. Isso resolve o problema deles conseguirem rodar em paralelo, ao mesmo tempo. Como exemplo, pense num programa que vai gravando coisas num arquivo de log e pense outro programa como o Prometheus, que vai lendo esse log e organizando a informação. Arquivos é uma área compartilhada que dois processos podem usar pra se comunicar, já que memória é isolada.</p>
<p>Todo iniciante precisa se acostumar a lidar com arquivos. Sem entrar em detalhes, na maioria das linguagens, você manda abrir um arquivo a partir de um path ou caminho, que é a sequência de diretórios e sub-diretórios separados por uma barra e o nome do arquivo com extensão. Isso abre o que chamamos de um stream, pense como se fosse um cano de bits bidirecional, um rio onde flui bits. Agora podemos ou ler ou escrever nesse stream.</p>
<p>Podemos ler bit a bit, ou pedir pra ir enchendo tipo um balde antes, um buffer, um acumulador, pra pegar linha a linha de cada vez, por exemplo. Mesma coisa pra escrever, podemos jogar bit a bit nesse stream, ou preencher um buffer antes e mandar vários bits de uma só vez, como uma linha inteira. Podemos programar pra deixar encher esse balde, e quando tiver uma certa quantidade de bits ou quando vier algum caracter especial como uma quebra de linha, pedir pra notificar o programa pra fazer alguma coisa com o que veio. De novo, a maioria das bibliotecas de linguagens modernas já cuidam disso e você como programador só lida com coisas como linha a linha. Mas por baixo existem coisas como buffers.</p>
<p>Vale lembrar que se você programar numa linguagem um pouco mais baixo nível, tipo C, Rust ou até Java, vai lidar mais com esses conceitos de buffers. Java tem classes como Buffered Stream, por exemplo. Numa linguagem mais alto nível como Javascript, ele cuida disso pra gente por baixo dos panos, e você só fala &ldquo;vai lendo de linha a linha e vai me dando&rdquo;. Bibliotecas de linguagens de alto nível foram feitas pra oferecer produtividade ao custo de dar menos controle. Por outro lado é o velho 80/20, pra 80% das coisas que precisa, o 20% que oferece por padrão costuma ser suficiente. Pros outros 20%, aí precisa descer pra um C da vida.</p>
<p>Só pra ter de referência aqui, como que em Javascript com Node dá pra escrever e ler de um arquivo? Primeiro carrega a biblioteca &ldquo;fs&rdquo; de filesystem e ele tem várias funções como esse <code>writeFile</code> onde passo o nome do arquivo e o conteúdo como String. Se não der nenhum erro, podemos usar <code>readFile</code> pra ler o conteúdo que acabamos de escrever e mostrar na tela com o bom e velho <code>console.log</code>. Mas o principal é entender que a semântica são funções como essas, read alguma coisa, write alguma coisa.</p>
<p>`var fs = require(&ldquo;fs&rdquo;);
fs.writeFile(&lsquo;hello.txt&rsquo;, &lsquo;Hello World&rsquo;, function(err) {
if (err) {
return console.error(err);
}</p>
<p>fs.readFile(&lsquo;hello.txt&rsquo;, function (err, data) {
if (err) {
return console.error(err);
}
console.log(&ldquo;read: &quot; + data.toString());
});
});`</p>
<p>Enfim. Com pipes e arquivos, você tem o mínimo suficiente pra fazer dois processos comunicarem entre si mesmo sem compartilhar memória RAM. Esse tipo de técnica fica embaixo de um guarda chuva de assuntos que chamamos de IPC ou inter process communication. Existem outras formas de comunicação como sinais, mas só isso é suficiente pra hoje. Se você só tem um computador, isso é suficiente. Mas aí lá nos anos 60 inventaram um troço que fodeu esse esquema. Essa tal de rede.</p>
<p>Com redes, temos mais de um computador rodando em paralelo e seria bacana se um processo rodando em um computador pudesse falar com um processo rodando em outro computador. E seria mais legal ainda se pudéssemos usar uma semântica parecida com lidar com arquivos, sem de fato usar arquivos. Ter comandos parecidos como &ldquo;read&rdquo; ou &ldquo;write&rdquo;, ler ou escrever num stream de bits. Pensando assim que em 1983 surge a implementação de Berkeley Sockets no UNIX original BSD versão 4.2.</p>
<p>Por isso são chamados de BSD sockets, de Berkeley Software Distribution ou POSIX sockets. POSIX que é o conjunto de APIs e protocolos que define o que é um UNIX. Pra entender um pouco mais sobre UNIX e Linux e licenças de software livre, não deixe de assistir o episódio de Apple e GPL que fiz faz tempo. BSD Sockets se tornou meio que o padrão de como lidamos com comunicação entre processos de forma mais geral.</p>
<p>Já vimos nos episódios anteriores dessa mini-série de redes, como que informação é fragmentada em pacotes, e como esses pacotes são transferidos numa rede de um computador até outro, seja numa rede local ou seja na internet em geral. Vamos assumir que tudo isso funciona e que os sistemas operacionais sabem como produzir pacotes e enviar na rede e consumir pacotes que chegam da rede, seja via fibra ótica, cabo de cobre ou rede sem fio.</p>
<p>Não por acaso venho me repetindo bastante sobre sistemas de armazenamento: como HDs são dispositivos de blocos e não dispositivos de arquivos. No baixo nível do hardware, sistemas operacionais lidam com blocos de bits num HD ou blocos de bits em pacotes de rede. Tudo são blocos, toda informação é fragmentada e transferida num stream. Podemos fazer buffers de blocos. Se blocos de bits forem arquivos num HD, podemos pensar em blocos de bits que vem pela rede meio como arquivos também. As abstrações são parecidas embora no meio físico sejam diferentes. No fim do dia você está lendo bits de algum lugar e escrevendo bits em algum lugar, esse é o conceito principal.</p>
<p>Não é a definição exata mas a grosso modo poderíamos pensar em sockets como uma generalização das primitivas de arquivos. Um Linux entrega um file descriptor ou file handle pro seu programa, tanto se for um arquivo local no seu HD ou um socket de rede. Pense em um handle ou descriptor como uma caixa preta que representa um arquivo, um intermediário pra quem você pede pra receber bits ou enviar bits. E essa caixa preta poderia ser um arquivo em disco, mas poderia ser uma conexão remota em rede. Tem uma preparação extra que precisa fazer pra se comunicar na rede e em toda linguagem de programação vai ser parecido. O protocolo que os Berkeley Sockets definiram funciona mais ou menos assim.</p>
<p>`const http = require(&lsquo;http&rsquo;);</p>
<p>const hostname = &lsquo;127.0.0.1&rsquo;;
const port = 3000;</p>
<p>const prepareResponse = (req, res) =&gt; {
res.statusCode = 200;
res.setHeader(&lsquo;Content-Type&rsquo;, &rsquo;text/plain&rsquo;);
res.end(&lsquo;Hello World\n&rsquo;);
}</p>
<p>const server = http.createServer(prepareResponse);</p>
<p>server.listen(port, hostname, () =&gt; {
console.log(<code>Server running at http://${hostname}:${port}/</code>);
});`</p>
<p>Primeiro seu programa pede um bind pro sistema operacional. Bind é literalmente ligação, a ligação de um endereço IP com um outro número de 16 bits que chamamos de porta. Bind é um pedido de registro. Funciona assim. Todo processo quando inicia, o sistema operacional oferece um número de identificação, o tal PID. Só que esse PID muda o tempo todo. Hoje seu Spotify inicia com PID 10. Mas amanhã pode já ter outro programa com PID 10, então o sistema atribui PID 11 ou qualquer outro número. PIDs identificam programas rodando naquele momento, mas não servem pra identificar um programa individualmente a qualquer momento. Então precisamos de outro número que seja fixo que o programa possa pedir pra se registrar.</p>
<p>Digamos que seu programa peça pro sistema registrar ele com o número 3000. O sistema operacional mantém uma tabela em memória e poderia associar o PID atual do seu programa, digamos que seja 11, com esse número 3000, que ele chama de porta, ligados ou bound num endereço IP. Porta é um número que identifica um processo que o sistema operacional mantém numa tabela de controle na memória dele. Se ninguém antes pediu pra registrar essa porta, o sistema te associa nela e nenhum outro programa pode pedir pra se ligar na mesma porta. Porta é só isso, um identificador do programa rodando ligado ao endereço IP do computador.</p>
<p>Quando o sistema consegue te associar nessa porta 3000, devolve um ok pro programa e devolve o equivalente a um stream de bits, o cano aberto, como se tivesse acabado de conseguir abrir um arquivo. E agora seu programa pode entrar em modo de escuta, ou listen. Nesse momento ele fica bloqueado esperando o sistema te mandar alguma coisa por esse cano e não precisa fazer mais nada enquanto isso. Normalmente isso fica numa thread bloqueada ou é usado I/O assíncrono, que não é escopo explicar hoje. Só entenda que depois do bind, seu programa ficaria parado esperando e escutando o que vier através dessa porta 3000. O processo de bind e listen de sockets é similar ao processo de dar open e abrir um arquivo.</p>
<p>Daí alguém na rede resolve mandar alguma informação, pro endereço IP do computador onde ele tá rodando, e mandando junto que quer falar com seja lá quem está ligado nessa porta 3000. O sistema operacional recebe isso e vê, &ldquo;hum, quem tá escutando na porta 3000, vamos ver na tabela, ah é o processo de PID 11, vou mandar no cano aberto dele&rdquo;. Agora seu programa, que estava só escutando, começa a receber bits. Nesse momento ele pode escolher aceitar ou rejeitar, normalmente só se aceita, isso é o passo de accept.</p>
<p>E é no accept que fecha o que se chama de conexão entre este computador e o outro que pediu a conexão. O socket serve pra estabelecer essa conexão, esse stream de bits, entre dois programas rodando, mesmo se for em máquinas diferentes. E agora os dois programas precisam enviar comandos que ambos entendam. Se seu programa receber bits que formam um comando válido, pode começar ou a receber ou a enviar bits de volta pela mesma conexão. Aqui é a metáfora das tomadas de padrão diferente.</p>
<p>Pra comunicação acontecer, ambos precisam concordar que comandos aceitam trocar. Esse acordo entre as duas pontas é o tal de protocolo. Pense em protocolo como sendo os pinos nas posições corretas que encaixam certo nos buracos da tomada. Mesmo tendo um processo pendurado numa porta, se ele não for um servidor web, não adianta um navegador web tentar se comunicar nele, eles não vão se entender e só vai dar erro. Por exemplo, se um navegador web tentar se conectar com um servidor de banco de dados, só vai dar erro, porque o banco de dados não entende comandos de HTTP que o navegador manda.</p>
<p>Falando bem resumido, esse é o básico do básico do básico de programação distribuída em rede. Toda linguagem vai te dar bibliotecas e funções que tem nomes parecidos com esses passos, bind, listen, accept, send. Apesar da sintaxe de cada linguagem ser diferente, podem ver que em todas é o mesmo processo, porque as primitivas no sistema operacional são as mesmas e as tecnologias de rede são as mesmas. Independe da linguagem, o jeito de falar em rede é sempre a mesma. Eu sempre falo que é mais importante saber a fundação do que uma linguagem só, porque se você aprendeu isso em C, facilmente vai conseguir fazer a mesma coisa em Java ou Python.</p>
<p>Do lado do sistema operacional, entenda que portas é só uma tabelona com identificadores numéricos de 16-bits, onde é possível associar 2 elevado a 16 portas no total, de 0 até 65 mil 536. Por isso você nunca viu número de portas acima disso. Essa tabelona diz que se receber bits endereçados pra um IP e porta, ele sabe pra qual cano de qual processo rodando enviar. Agora vamos entender isso a partir do programa mais comum pra se comunicar em rede hoje, seu navegador web, como Chrome ou Firefox.</p>
<p>Seu navegador é um exemplo desse tal programa que pede uma conexão, um cliente. Ele não precisa fazer bind, porque não precisa que ninguém se conecte nele. Ele só quer se conectar na porta dos outros, de um servidor web. E é isso que diferencia um cliente de um servidor. Um servidor faz bind e listen numa porta e fica esperando, escutando. Um cliente envia pacotes pro endereço IP do servidor e pede pra se conectar num programa rodando lá que está escutando numa determinada porta. Pra isso funcionar, o cliente precisa saber não só o endereço IP como em qual porta o servidor está escutando. E por isso se estabeleceu que certas portas são exclusivas pra um determinado uso.</p>
<p>Mas não se enganem, portas são só números numa tabela que o sistema operacional gerencia. Os usos de cada número de porta são completamente arbitrários, determinados por consenso, e pro programa em si não faz nenhuma diferença se ligar numa porta 80 ou 3000 ou 8080. Não existe nenhum tratamento especial diferente. Portas são só números. E por isso você sobe uma aplicação web feita em Node numa porta 3000 e ela funciona igual quando subimos na porta 80 num servidor na Amazon.</p>
<p>Por convenção, estabeleceu-se que as primeiras 1024 portas, de 0 até 1023 são portas de sistema, e num Linux precisa ter permissão de root pra fazer um programa se registrar numa delas. Essa distinção existe porque vários usuários podem abrir sessões num mesmo servidor UNIX ou Windows Server, mas só o administrador pode subir processos em portas abaixo de 1024. Usuários nesse servidor podem subir processos em outras portas menos nessas. É nesse intervalo que se encontram as duas portas mais famosas de todas, a 80 de HTTP e 443 de HTTPS. Temos outras famosas, como porta 21 de FTP, porta 22 de SSH, porta 25 de SMTP pra envio de emails, porta 37 que usamos pra sincronia de horário via NTP, porta 53 que é DNS, porta 194 é de IRC, 993 pra IMAP de cliente de email e assim por diante.</p>
<p>Existe uma organização que registra o uso dessas portas baixas, a IANA ou Internet Assigned Number Authority. Dá pra ligar qualquer programa numa porta baixa, mas normalmente essas portas costumam ter serviços específicos como servidor web ou DNS. Por isso que num navegador quando digitamos endereço, podemos omitir a porta, porque o navegador preenche a porta pra gente usando essas convenções. Quando digitamos <a href="http://www.google.com"target="_blank" rel="noopener">http://www.google.com</a> primeiro ele precisa resolver esse nome de domínio, pra isso resolve esse nome num endereço IP e pra isso precisa de um name server ou DNS.</p>
<p>Já mostrei no episódio anterior como seu sistema operacional pergunta quem é esse nome pra um DNS, como o 1.1.1.1 da CloudFlare ou 8.8.8.8 do Google. O DNS vai te devolver um grupo de endereços IP e seu computador escolhe um deles. A partir desse ponto faz um cache disso localmente. Se no navegador pedir de novo o &lt;www.google.com&gt;, nem precisa pedir pro DNS, porque já tem gravado num cache local. Toda resposta de DNS tem uma coisa chamada TTL ou time to live, que é tipo uma expiração. Quando expirar, aí sim seu computador vai no DNS e pergunta de novo qual é o endereço do nome &lt;www.google.com&gt;. Em outro video vou explicar DNS com um pouco mais de detalhe mas se acostumem com esse conceito primeiro.</p>
<p>Enfim, graças ao DNS ou o cache local de DNS, seu navegador agora tem um endereço IP. Ele pode começar a se conectar. Mas como disse antes, precisa saber em que porta nesse endereço vai estar o programa que vai te devolver os HTMLs. Esse programa chamamos de servidor web, e como falei antes, por convenção, vai estar escutando nas portas 80 ou 443. Se no navegador você digitou http:// significa que quer conectar na porta 80. Se digitar https:// quer dizer que quer conectar na porta 443. O que vem antes dos dois pontos é com qual protocolo quer se comunicar com esse servidor, no caso HTTP ou HTTP com TLS.</p>
<p>Protocolos é quem definem os tais comandos e valores que ambos o navegador e o programa servidor web conseguem entender, os pinos certos pros buracos certos da tomada. No protocolo HTTP temos comandos em texto como GET ou POST pra pedir ou enviar coisas do navegador pro servidor. Vamos fazer um exemplo simples só pra consolidar o conceito. Vou fazer rapidinho um servidor web em Node.js estilo hello world, só pra ter um programa escutando numa porta. Isso você encontra quinhentos tutoriais na internet não vou detalhar muito.</p>
<p>Olha esse do tutorial do próprio site oficial do Node.js. Primeiro ele carrega a biblioteca &ldquo;http&rdquo;. Em seguida declara duas variaveis, uma pra guardar o endereco 127.0.0.1. Esse é o endereço que chamamos de loopback, ele representa o endereço local de rede do seu computador. Todo pacote enviado pra esse endereço não vai sair do seu computador, ele vai devolver pra ele mesmo. Por isso chamamos de loopback. Vale entender isso porque muita gente acha que 127.0.0.1 é a mesma coisa que outro endereço, o 0.0.0.0 e não sabe a diferença.</p>
<p>Normalmente um endereço IP é associado com uma placa de rede instalado na sua máquina, mas 127.0.0.1 é como se fosse uma placa de rede virtual, de mentira. Ele recebe pacotes e devolve pra ele mesmo. 127.0.0.1 é a mesma coisa que localhost. Quando damos bind de um processo nesse endereço, dá pra abrir o navegador e conectar por ele com <a href="http://localhost"target="_blank" rel="noopener">http://localhost</a>. Mas se alguém de fora tentar se conectar no mesmo serviço, na mesma porta, não vai conseguir, porque na placa de rede de verdade não tem bind nessa porta. Localhost só é acessível de dentro do próprio computador, não de fora.</p>
<p>Tem também o endereço 0.0.0.0 que significa “qualquer placa de rede do computador”. Quando queremos pendurar um servidor que vai receber bits independente de por qual placa de rede do seu computador vier, penduramos ele no 0.0.0.0. E no navegador podemos digitar localhost porque 127.0.0.1 é um desses “qualquer placa de rede”, mas também um navegador em outro computador na rede, vai conseguir se conectar no seu programa. Todo computador responde nesses endereços locais. Muita gente pensa que são a mesma coisa, mas só localhost e 127.0.0.1 são a mesma coisa. Pra desenvolvimento a gente usa localhost, porque normalmente não queremos outra pessoa na mesma rede acessando enquanto estamos no meio do desenvolvimento. Mas num servidor de verdade queremos dar bind em 0.0.0.0 pra deixar o programa acessível a qualquer pessoa.</p>
<p>A segunda variável define que quer se ligar na porta 3000. Eu expliquei que portas de 0 até 1023 são reservados e o sistema operacional exige permissão de administrador pra ligar um programa nessas portas baixas. Por isso que em máquina de desenvolvimento, preferimos usar portas de 1025 até 49 mil 151. Porque com suas permissões de usuário normal, pode pedir pro sistema ligar seu programa numa porta, sem privilégios especiais, sem precisar ficar usando o comando <code>sudo</code> toda hora. Por isso que pra subir uma aplicação de Node.js ou Django ou Laravel, numa porta como 3000 ou 4000, não precisa executar com <code>sudo</code>.</p>
<p>Pra complementar, de 49 mil 152 até 65 mil 535 são o que chamamos de portas dinâmicas ou privadas. Em particular 49 mil 152, que é um número difícil de decorar em decimal, mas é a mesma coisa que 2 elevado a 15 menos 2 elevado a 14, que é mais fácil de lembrar. Também são chamadas de portas efêmeras. Você até pode mas normalmente não se registra nenhum programa servidor que vai ficar rodando permanentemente nesse segmento. Essas são as portas usadas, por exemplo, por comunicadores como o Zoom ou Skype pra abrir conexão com outra pessoa na internet. Pode ser usado por games pra sessões multiplayer. São portas temporárias. Programas de comunicação tentam diversas dessas portas e usam a primeira livre que acharem. Não é regra, mas eu acho que programas que usam UDP em vez de TCP usam mais essas portas.</p>
<p>Por isso que nós programadores web, quando fazemos uma aplicação usando Node.js ou Spring ou Laravel ou qualquer coisa assim usamos portas como 3000, 4000, 8000, 8080 como era o Tomcat de Java e assim por diante. Qualquer uma depois de 1024 e antes daqueles 49 mil e tanto servem. Sem interferir com calls de Zoom e sem precisar de privilégios de administrador e ter que ficar digitando <code>sudo</code>. De novo, portas são só números. Se você achava que porta 8080 ou 3000 tinha algum significado, não tem, foram escolhidas arbitrariamente por alguém e todo mundo só usa a mesma porta por convenção.</p>
<p>Enfim, agora é específico de cada linguagem e framework, mas no caso do Node criamos uma instância do que ele chama de &ldquo;server&rdquo;. Eu modifiquei um pouco o exemplo do tutorial porque quando criamos o servidor já registramos como queremos que ele responda quando vier algum comando de HTTP. Já volto nisso. Na sequência, fazemos essa instância de server pausar e ficar escutando, esperando alguma coisa, que o sistema operacional vai mandar pra gente no cano dessa porta 3000. Se ninguém mais estava pendurado na porta 3000, e ele conseguir se ligar com sucesso, daí escreve essa mensagem de servidor rodando. E pronto, agora está escutando na porta 3000 desse computador.</p>
<p>Vamos voltar pro navegador. Lá no campo de endereço escrevo que quero usar o protocolo &ldquo;http&rdquo;, dois pontos, barra, barra, localhost que é outra forma de dizer &ldquo;127.0.0.1&rdquo;, dois pontos, porta 3000. Se eu não colocasse explicitamente esse dois pontos 3000, ele ia assumir que seria porta 80. Tenta aí, em qualquer site como google.com, digita no final dois pontos 80. Vai ver que dá no mesmo.</p>
<p>E pronto, veio uma resposta Hello World. Vamos voltar pro código do servidor. Lembra que quando criei a instância de server registrei uma função de resposta? O framework do Node esconde os detalhes. O Linux recebeu os pacotes de bits do navegador endereçados pra porta 3000, viu que tinha meu programa pendurado nele e começou a reenviar os pacotes pro cano da porta 3000.</p>
<p>O meu programa em Node começa a receber pacote e dá accept. E aí vê o que fazer com esses pacotes. Daí tinha essa minha função registrada no server. Ele encapsula esses pacotes numa estrutura chamada Request, ou HTTP Request. É isso que faz essa biblioteca &ldquo;http&rdquo; que estamos usando. Ele se encarrega de receber os bits e organizar tudo bonitinho nesse objeto de Request pra gente. A partir desse ponto eu posso fazer lógica de código pra fazer coisas diferentes dependendo do que vier nessa request, como carregar um arquivo HTML. No caso, como é um exemplo besta, eu só mando responder a mensagem de Hello.</p>
<p>Mas não é só a mensagem. Isso é um protocolo, tem algumas regrinhas. O mínimo do mínimo que se espera de resposta é montar uma segunda estrutura chamada HTTP Response. Nesse response registro um status code 200, que é o código do protocolo HTTP pra dizer que tá tudo ok, daí por boa etiqueta, registro também um cabeçalho com um mime type, no caso pra indicar que não vou responder um HTML e sim só um texto puro. É com esse mime type que o navegador sabe se é pra interpretar como HTML ou se é um binário e é pra fazer download, por exemplo. E finalmente, posso escrever minha mensagem de hello no response como um simples string.</p>
<p>O Node vai pegar esse pacote de response que preenchi e mandar pro sistema operacional devolver pela rede pro navegador pela mesma conexão. E o sistema operacional por baixo vai fazer tudo que já expliquei antes, vai quebrar esse response em datagrams, vai encontrar a rota pra enviar, etc. E no final o navegador vai receber o pacote de resposta e interpretar isso visualmente na tela, desenhando o hello na tela pro usuário. Esse é o fluxo mais básico de web. Se não entender nem isso, não vai entender nada mais complicado, então garanta que esse fluxo está super claro na sua cabeça.</p>
<p>Tem outra forma de ver como isso funciona. Antigamente todo sistema operacional vinha com um programa de internet bem simples chamado telnet. É um programa que se conecta em qualquer porta que tenha algum outro programa escutando e com ele posso manualmente digitar comandos pra enviar, caso eles aceitem comandos em texto em vez de binário. No caso do Arch Linux hoje ele fica no pacote opcional chamado de inetutils. Em nenhuma distro se instala mais por padrão porque acho que é considerado um risco de segurança. São ferramentas que precedem o conceito de encriptação. Em vez de telnet hoje se usa SSH pra abrir conexões encriptadas, mas como não quero ter que lidar com criptografia neste episódio, deixa eu mostrar o bom e velho telnet.</p>
<p>Aquele meu programa de hello de Node continua rodando. Ele tá pendurado na porta 3000 e mesmo quando processa a requisição de algum navegador, continua aceitando novas requisições de outros navegadores. Por isso que um servidor web consegue atender várias pessoas ao mesmo tempo. Vamos abrir um outro terminal e conectar direto nele usando telnet, passando o endereço e a porta.</p>
<p>Pronto, estou conectado. Agora posso enviar uma requisição HTTP manualmente. O protocolo HTTP diz que posso digitar o comando GET seguido de uma URI que é o barra alguma coisa e no final escrever HTTP/1.0 denotando a versão do protocolo que estou querendo usar. Isso é tudo texto. Nas linhas seguintes posso mandar outros detalhes desse comando, mas se der dois enters, duas quebras de linha, isso indica pro servidor que terminei minha requisição.</p>
<p>E olha só, quando dei dois enters, o Node entendeu que podia começar a responder, e essa é a resposta que ele envia. Olha só o que tinha mandado ele preencher na tal estrutura de resposta. O status code 200 veio na primeira linha, seguido do content type que é texto puro, mais uma data e horário que o framework preencheu pra mim e o fechamento da conexão seguido do corpo da resposta que é o string da mensagem hello.</p>
<p>Claro, hoje em dia não preciso de telnet pra ver isso. Todo navegador agora tem o tal Developer Tools, que se  acessa com o atalho control shift J no Windows ou control option J no Mac. Ele tem essa aba Network ou rede, onde podemos ver os pacotes de request e response e os cabeçalhos. Mesma coisa que fizemos no telnet, olha como no request ele mostra que o navegador fez um comando GET e embaixo temos a mesma resposta que recebemos via telnet, status code 200, content type text plain, a data e horário. Antes de existir Dev Tools a gente usava coisas como Telnet pra debugar.</p>
<p>Em linhas gerais é assim que um navegador, um cliente de APIs e tudo mais que fazemos na web funciona. Com o navegador conectando na porta 80 ou 443 ou numa porta de usuário como a 3000 do meu Node.js. O navegador monta uma estrutura em texto chamada request, um servidor feito em qualquer framework como Node da vida recebe, decide o que fazer baseado no que foi pedido, monta uma outra estrutura chamada response e devolve. Esse é o básico do básico de web.</p>
<p>A porta 443 e TLS é uma camada a parte. É um processo que acontece entre seu navegador e o servidor web, independente da aplicação por baixo. Esse meu hello em Node não precisa estar ciente que a conexão estava ou não criptografada. O navegador pede um handshake, um aperto de mão, com o servidor que, se estiver devidamente configurado com certificados SSL, vão abrir uma conexão segura usando chaves assimétrica, trocar uma chave simétrica, e a partir daí todo pacote que o servidor web envia vai criptografado e o navegador consegue descriptografar e vice versa. Eu explico isso na parte 2 dos meus videos sobre criptografia.</p>
<p>Mas eu digo que tanto faz porque o programa Node por baixo vai receber a request já descriptografado direitinho, como se nada tivesse acontecido. E em redes existe isso de cada camada na arquitetura trabalhar os pacotes trafegando e as camadas de cima, como minha aplicação, não precisar estar cientes e funcionar normalmente. Se não sabia o que é uma boa arquitetura, a arquitetura OSI de camadas de rede é uma boa inspiração, onde cada camada tem sua responsabiilidade, cada camada não precisa estar ciente se uma camada embaixo foi adicionada ou trocada, contanto que realizem as mesmas tarefas. Programação de software pra rede costuma ter essa metáfora de camadas de uma cebola. Quem já brincou de Tor e coisas assim já deve ter ouvido falar de Onion, por isso. Outro dia falo de coisas como Tor.</p>
<p>Um navegador web como Chrome é um dos softwares mais complexos que você tem instalado no seu computador depois do próprio sistema operacional, especialmente pra renderizar a parte gráfica corretamente. Mas pra se conectar num servidor web e mandar comandos simples de GET, um mero telnet como mostrei já serve. Vários outros programas simulam um navegador simples como os programas wget ou curl de Linux, que a gente usa mais pra fazer download, mas tecnicamente ele se comporta como um navegador web. Ou bibliotecas que usamos pra fazer clientes pra APIs como o Axios de Javascript, ou a classe HttpClient de Java ou C#, ou a classe http.client de Python e assim por diante. São todos mini navegadores, sem a parte gráfica.</p>
<p>Se nunca pensou assim, um servidor é todo programa que pede um bind de uma porta pro sistema operacional e fica em modo listen de escuta, esperando pra dar accept e abrir uma conexão com outro computador remoto. Aliás, vale outro conceito básico. Eu categorizo programas em dois tipos, short lived e long lived. Ou em bom português, programas de tiro curto e programas persistentes. Programa de tiro curto são muitos dos que você roda via linha de comando, comandos como o grep, sed, ou awk de Linux, programas que recebem um input, processam alguma coisa, cospem um resultado e saem fora. Servidores ou programas persistentes são loops.</p>
<p>Vamos desconsiderar por um segundo que existem threads, I/O assincrono, e que tudo seja como era antigamente, quando só dava pra rodar um comando bloqueante e síncrono por vez. Se eu fosse escrever numa pseudo-linguagem, ou seja, uma linguagem que não existe, a estrutura mais básica de um servidor de rede seria assim:</p>
<p>`while(true) {
listen(:3000)</p>
<p>stream = accept()
while(not stream.eof) {
request += stream.read_line
}
response = processa_alguma_coisa(request)
send(response)
}`</p>
<p>Todo servidor fica num loop infinito até receber sinal do sistema operacional pra morrer, o tal SIGTERM ou sinal de término, que se você já usou Linux, já enviou com o comando <code>kill</code>. Ou no Windows via powershell com o comando <code>taskkill</code>. Enfim, entenda que se executar um programa e ele tiver o comportamento de esperar um evento, reagir, e voltar a esperar, normalmente é um loop, equivalente a um while true.</p>
<p>Dentro desse while, ele manda comando pro sistema operacional, o <code>listen</code>, pra pedir pra ficar esperando o que vier numa porta, como 3000. Nesse ponto que eu falei pra ignorar que existe threads e concorrência. Faz de conta que quando ele dá listen, fica parado nessa linha até receber alguma coisa. Uma hora alguém conecta e começa a enviar bits. Daí sai dessa linha e cai no <code>accept</code> onde recebe o stream de bits, o tal cano de bits. Agora eu fiz um pseudo código arbitrário que é ficar lendo linha a linha até sinalizar <code>eof</code> que, se nunca viu, costuma ser sigla pra end of file, que terminou o envio ou acabou o arquivo.</p>
<p>Agora, o programa faz alguma coisa com esses bits que vieram, que chamei de request, e monta alguma resposta, que é a estrutura de response. No final envia de volta ou dá <code>send</code> desse response, pedindo pro sistema operacional devolver, pela mesma conexão fechada no accept, e o loop reinicia esperando outra requisição. Volta pro estado de listen, até aparecer mais bits. No mundo real existem threads e na realidade uma das coisas que pode acontecer dependendo da linguagem é o accept abrir uma nova thread pra processar o request e response enquanto o loop de listen continua ativo esperando mais conexões. Possibilitando lidar com dezenas ou centenas de conexões simultaneamente.</p>
<p>Aproveitando o conceito. Todo programa gráfico é um grande loop também. Ele fica esperando você digitar alguma coisa ou clicar alguma coisa com o mouse, é parecido com esse <code>listen</code>. E fica no loop infinito até receber um sinal pra fechar como o menu de Quit ou alt-f4 no Windows. Um game também é um grande loop, esperando seus comandos e reagindo. Essa é a fundação pro que muitos chamam de programação orientada a eventos original. O programa reage a eventos, como input de mouse e teclado ou algo assim. Não vou entrar em paradigmas, mas só pra lembrar que esse é um dos patterns mais comuns em programação, o que eu pessoalmemnte chamo de Big Loop.</p>
<p>Normalmente, em web, a gente fica muito preso ao protocolo HTTP, onde as duas peças principais são a tal estrutura texto de HTTP Request e HTTP Response e comandos básicos como GET, POST ou PUT. Todo tutorial de web começa direto neles. E por causa disso muita gente faz o que não devia, tentando embutir tudo que pode dentro dessas estruturas. Mas a internet não é feita só de HTTP, ela só é a mais popular. Muitos falam “internet” pensando em redes, roteadores, pontos de acesso. E chama o resto de Web. Mas Web, que é o nome mais curto pra World Wide Web, que se refere à capacidade de hipertexto de páginas HTML. A idéia que uma página pode ter links pra outras páginas formando uma teia, ou Web.</p>
<p>Mas internet na realidade é um conjunto de tecnologias, que envolve coisas como ethernet, wifi, fibra, o TCP/IP e todos os outros protocolos de aplicação, incluindo o HTTP de Web. E chamamos assim porque como expliquei antes, o modelo OSI de internet define sete camadas. A camada física que foi o episódio que expliquei sobre ondas, cabos de cobre e wireless. A camada de data link responsavel entre outras coisas como checar erros, como eu expliquei sobre Hamming Code e tudo mais. A terceira camada, de rede é que lida com coisas como roteamento, endereços IP e tudo mais.</p>
<p>Eu pulei as camadas seguintes, a camada 4 de transporte, a camada 5 de sessão, a camada 6 de apresentação e a camada 7 de aplicação. Isso porque em TCP/IP as funções da camadas acima da 5 acabam sendo acumuladas na camada de aplicação, que é onde mora protocolos como HTTP de Web.</p>
<p>Em OSI existe uma camada pra lidar com sessões, mas em Web controlamos sessão direto no HTTP usando cabeçalhos como de cookies. Sessões servem pra literalmente identificar uma sessão individual de cada usuário e no modelo OSI esse controle ficaria na camada 5, mas a gente acaba achando mais fácil acumular isso direto na camada de aplicação. Por isso em vez de ter um jeito único de lidar com sessões, protocolos como HTTP, FTP, IMAP e tudo mais, cada um controla sessões do seu jeito. Tem vantagens e desvantagens de se fazer isso.</p>
<p>Exemplo de outro protocolo era o antigo FTP ou File Transfer Protocol que hoje em dia ninguém mais usa pelo mesmo motivo que não se usa Telnet, ambos precedem conceitos mais modernos de segurança. Muitos antigos servidores de FTP estão expostos atrás de um servidor web, como o FTP da Unicamp que eu usava muito na faculdade e hoje hospeda espelhos de arquivos de Linux. Se digitar ftp.unicamp.br no seu navegador, vai ver que ele coloca http:// na frente, indicando que não está usando o protocolo FTP.</p>
<p>Num terminal Linux, se instalar o antigo pacote inetutils, além do telnet, vai ganhar o programa ftp. Basta digitar <code>ftp ftp.unicamp.br</code> no terminal. Ele vai pedir um usuário. Como antigamente, muitos servidores públicos aceitavam o usuário <code>anonymous</code>. E felizmente esse ainda aceita. A senha pode ser qualquer coisa ou vazia. E pronto, abriu uma conexão de FTP onde ele passa a esperar comandos que são parecidos com os comandos pra navegar no seu HD via terminal como <code>cd</code> pra mudar de diretórios ou <code>ls</code> pra listar o conteúdo do diretório. Se digitar <code>help</code> mostra os comandos que o protocolo FTP suporta. Lembra que HTTP suporta comandos como GET? Em FTP ele suporta comandos diferente. Get em FTP é pra fazer download de algum arquivo.</p>
<p>Eu até consigo mudar de diretório fazendo <code>cd /pub</code> pra ir pro diretório público, mas não sei porque, comandos como <code>ls</code> não devolvem nada e dão timeout. HTTP versão 1.0 era ótimo pra baixar páginas HTML e até mesmo fazer download de arquivos, como agora. Mas era chatinho enviar arquivos pro servidor. Hoje em dia é fácil, mas nos primórdios, a gente ainda não entendia tudo que dava pra fazer, nem lembro se HTML 1.0 tinha form multipart. Pra enviar arquivos pra um servidor, o certo era usar FTP.</p>
<p>Acumulamos um monte de funções em cima de HTTP que antes eram de outros protocolos. Hoje em dia é muito normal abrir um Gmail e anexar um arquivo. Ou abrir um Dropbox e subir um arquivo. E mesmo se quisermos transferir arquivos num equivalente FTP, não se usa FTP pelo mesmo motivo que hoje usamos HTTPS em vez de HTTP, por segurança. HTTPS é HTTP mais encriptação com certificado TLS. O equivalente seria FTPS. Assim como em vez de Telnet hoje usamos SSH. E SSH é mais versátil porque permite não só abrir um terminal remoto seguro, como também trafegar arquivos, via SFTP que é FTP sobre SSH. Vou falar mais de SSH no próximo episódio.</p>
<p>Outro protocolo que até existe ainda mas num nicho muito pequeno é o que falei 2 episódios atrás quando expliquei correção de erros e parchives. O protocolo NNTP de rede Usenet. A idéia era ter um servidor de artigos de texto, como fóruns e sincronizar diferentes servidores usando esse protocolo NNTP. De novo, acumulamos essas funções dentro de HTTP e o equivalente seria sites como StackOverflow, HackerNews, Reddit.</p>
<p>No começo da internet não usávamos navegador web pra tudo. Usávamos programas separados como FTP pra transferir arquivos. News pra ler notícias e discussões da Usenet, literalmente newsgroups. Telnet pra abrir sessões remotas de terminal. E Gopher, que eu quase nunca usei porque meio que tinha o mesmo propósito de HTTP e perdeu espaço quando a Web ficou mais popular. Mas todos são exemplos dos primeiros protocolos de aplicação no começo da internet comercial. Na prática, Usenet ainda é usado até hoje pra facilitar pirataria, algumas coisas que talvez seja mais difícil em BitTorrent ainda se acha na Usenet.</p>
<p>Todos foram substituídos. HTTPS se tornou o protocolo mais popular. SSH é a única boa alternativa tanto pra telnet quanto ftp. E protocolos específicos como NNTP, IRC de chat, Gopher e outros, tiveram suas funções acumuladas em HTTPS. E o S de segurança se tornou importante nos primórdios do comércio eletrônico, onde encriptação se tornou essencial pra permitir trafegar coisas como número de cartão de crédito. Sem isso seria impossível ter comércio online.</p>
<p>Tivemos primeiro o SSL da Netscape, e hoje é o TLS ou Transport Layer Security. Isso possibilitou escalar a economia na Web mas também dificultou pra hobistas e amadores que queriam só fuçar a rede com comandos simples no terminal, porque agora temos que lidar com uma camada de encriptação no meio.. Antigamente era só plugar telnet em qualquer porta que já dava pra fuçar, hoje já não é tão simples e como isso torna as coisas mais indiretas e abstratas, imagino que deve dificultar o aprendizado de muita gente.</p>
<p>Sem contar que se eu quisesse contribuir na rede e participar de sub-redes como servidores de chat IRC, era só montar meu próprio servidor IRC e plugar na internet. Ou se quisesse ter meu próprio servidor de newsgroups era só montar um servidor de NNTP e plugar na Usenet e assim a gente podia fazer parte do mesmo serviço distribuído na rede. No momento onde centralizamos tudo em HTTP, também perdemos a característica de serviços distribuídos e entramos na era de walled gardens ou jardins emparedados. Acho que os únicos exemplos de rede verdadeiramente distribuída onde qualquer um pode entrar e participar é BitTorrent e Bitcoin da vida. Sim, eu sei, como falei antes Usenet e IRC ainda existem, mas são nichos bem pequenos e desconhecidos. Tem muito mais gente usando Discord do que IRC.</p>
<p>E falando nisso, eu acho que preciso explicar um pouquinho sobre segurança de rede e vou deixar isso pro próximo episódio. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>redes</category><category>backend</category><category>javascript</category><category>akitando</category></item><item><title>[Akitando] #44 - Concorrência e Paralelismo (Parte 2) | Entendendo Back-end para Iniciantes (Parte 4)</title><link>https://akitaonrails.github.io/2019/03/20/akitando-44-concorrencia-e-paralelismo-parte-2-entendendo-back-end-para-iniciantes-parte-4/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/03/20/akitando-44-concorrencia-e-paralelismo-parte-2-entendendo-back-end-para-iniciantes-parte-4/</guid><pubDate>Wed, 20 Mar 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/gYJSWs-gp1g"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente! Chegamos ao FIM do assunto sobre Concorrência e Paralelismo! Desta vez vou finalizar o que faltou falar sobre processos, threads, como eles se coordenam, quanto isso custa pro sistema operacional. E então vamos vamos sobre Green-Threads e como as linguagens modernas lidam com elas.&lt;/p&gt;
&lt;p&gt;Então finalmente posso dar minha perspectiva sobre as linguagens interpretadas como Python, Ruby, mais sobre Erlang/Elixir, Go, e como Scala, e outras linguagens se comportam em termos de concorrência e como se comparam com coisas como Javascript.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/gYJSWs-gp1g"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente! Chegamos ao FIM do assunto sobre Concorrência e Paralelismo! Desta vez vou finalizar o que faltou falar sobre processos, threads, como eles se coordenam, quanto isso custa pro sistema operacional. E então vamos vamos sobre Green-Threads e como as linguagens modernas lidam com elas.</p>
<p>Então finalmente posso dar minha perspectiva sobre as linguagens interpretadas como Python, Ruby, mais sobre Erlang/Elixir, Go, e como Scala, e outras linguagens se comportam em termos de concorrência e como se comparam com coisas como Javascript.</p>
<p>Este é o episódio mais longo da série até agora, e se você conseguir chegar até o final deve ter uma visão bem mais completa quando for discutir sobre linguagens. Então tenha um pouco de paciência.</p>
<p>Se você ainda não assistiu os vídeos anteriores, agora é a hora porque este vídeo vai convergir todos os assuntos que venho explicando desde que comecei a falar sobre Back-end.</p>
<p>Links:</p>
<ul>
<li>Named Pipe or FIFO with example C program (<a href="https://www.geeksforgeeks.org/named-pipe-fifo-example-c-program/"target="_blank" rel="noopener">https://www.geeksforgeeks.org/named-pipe-fifo-example-c-program/</a>)</li>
<li>Understand promises before you start using async/await (<a href="https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8"target="_blank" rel="noopener">https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8</a>)</li>
<li>Futures, cancellation and coroutines (<a href="https://medium.com/@elizarov/futures-cancellation-and-coroutines-b5ce9c3ede3a"target="_blank" rel="noopener">https://medium.com/@elizarov/futures-cancellation-and-coroutines-b5ce9c3ede3a</a>)</li>
<li>Futures and Promises (<a href="http://dist-prog-book.com/chapter/2/futures.html"target="_blank" rel="noopener">http://dist-prog-book.com/chapter/2/futures.html</a>)</li>
<li>Go concurrency considered harmful (<a href="https://medium.com/@sargun/go-concurrency-considered-harmful-26499a422830"target="_blank" rel="noopener">https://medium.com/@sargun/go-concurrency-considered-harmful-26499a422830</a>)</li>
<li>Go: the Good, the Bad and the Ugly (<a href="https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/"target="_blank" rel="noopener">https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/</a>)</li>
</ul>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>O episódio da semana passada foi bem pesado em conceitos. Estamos no nono episódio da série Começando aos 40, episódio quatro do tema de Back-end, e a segunda parte especificamente sobre Concorrência e Paralelismo. Pra variar, eu tentei encurtar esse episódio mas ele vai acabar virando um dos mais longos da série de novo. Se você ainda não assistiu meus episódios anteriores do tema de Back-end, eu recomendo que pause agora e vá assistir, porque tudo que eu falei antes vai convergir hoje.</p>
<p>Como eu já disse, é um tema bem cabeludo e vamos tentar desembaraçar os últimos conceitos que vocês vão precisar saber pra conseguir seguir com seus estudos sozinhos. A pergunta que todo mundo fica na cabeça é: qual das linguagens é a mais performática? Ou, qual vai me dar a escalabilidade do Whatsapp e ao mesmo tempo ser mais fácil de programar?</p>
<p>Vários números podem parecer impressionantes. Muita gente adora repetir os X milhões de conexões simultâneas que um servidor do Whatsapp consegue suportar e chegam à conclusão que não existe nada melhor do que Erlang. Esse número é bacana, mas não é impressionante. Parem de se impressionar com números sem contexto. E parem de ficar escolhendo tecnologia baseado em blog post ou palestra de engenheiro do Netflix ou Facebook. É um absurdo sem sentido pra pessoas que deveriam ter como profissão raciocinar com lógica.</p>
<p>Repitam comigo: você não é o Netflix. Seu produto que ainda nem saiu do papel, não tem nem um usuário, você não precisa nem pensar na escalabilidade massiva de um Netflix, que em horas de pico pode representar ⅓ do tráfego total da internet dos Estados Unidos. Quando alguém me pergunta: Akita, queria fazer tal coisa igual do Facebook, será que dá? Minha resposta é a mais óbvia: claro que dá, você está disposto a PAGAR o mesmo que o Facebook pagou? Se sim, então claro, dá pra fazer igual ou melhor. Guardem esse pensamento, eu ainda vou falar sobre isso em mais detalhes em outro episódio, mas por agora só entendam esta verdade: foda-se mil conexões simultâneas ou 10 milhões de conexões, sua preocupação como iniciante tá a milhares de quilômetros de distância disso. Atenha-se a entender as tecnologias sem virar fã de marcas e ter metas fora da realidade.</p>
<p>(&hellip;)</p>
<p>Já entendemos os mecanismos básicos de como conseguimos ter concorrência e paralelismo. Mas na minha opinião o real problema é a coordenação. Não adianta nada você ter silício suficiente pra executar milhares de threads em paralelo se você não consegue coordenar o trabalho dessas threads. Então se tiver uma única coisa que você precisa levar dessa série toda é que a coisa mais importante nesse tipo de programação é conseguir a maneira mais simples e “barata” de coordenar tarefas concorrentes.</p>
<p>Vamos voltar ao que interessa. Você é um iniciante, finalmente entendeu os conceitos mais básicos de processos e threads. Aprendeu rapidamente sobre forks, sobre copy on write que ajuda a economizar memória. Mas você precisa entender alguns conceitos adicionais sobre coordenação agora. A unidade mais simples numa CPU é uma instrução. Uma instrução em Assembly, que usa como argumentos certos valores que você preenche em determinados registradores. Daí a instrução executa e o resultado é escrito pela CPU em algum acumulador ou outro registrador. Ele incrementa o contador de programa e vai pra próxima instrução.</p>
<p>É assim que a CPU começa a transformar o mundo ao seu redor. Tudo que você programa serve para pegar alguma informação que o mundo exterior vai fornecer, por exemplo o que o usuário digita ao programa, e você vai usar essa e outras informações para gerar algum resultado. Programação é basicamente isso: transformação de dados. Uma instrução simples de soma, pega dois números que você armazena um em cada registrador, a instrução executa o cálculo e transforma esses dois números num único que representa a soma deles, e grava em outro registrador. Essa instrução é basicamente o que você chamaria de uma função. Toda função recebe parâmetros ou argumentos de entrada, processa alguma coisa e retorna algum valor de saída. E você pode usar o resultado de uma função como parâmetro ou argumento de outra função, e então vai  encadeando uma função atrás da outra.</p>
<p>Quando você manda o sistema operacional executar o binário do seu programa, digamos um programa como wget ou curl no Linux, que você pode pensar como se fossem navegadores Web muito simples que rodam na linha de comando, você normalmente passa uma parâmetro, no caso uma URL de algum arquivo que você quer baixar. Você já viu ou vai ver um monte de tutoriais que usam um desses dois comandos pra baixar alguma coisa. O OS vai fazer o que já expliquei antes: alocar memória, criar um processo isolado, carregar seu binário, e passar esse parâmetro pro processo. Esse processo vai executar, no caso conectar no servidor que você mandou e baixar o tal arquivo. E quando terminar, o OS vai descarregar e liberar os recursos e o resultado, se tudo deu certo, pode ser um arquivo gravado em algum lugar no seu sistema de arquivos. Nesse caso o programa funciona como uma “função” também, ele tem uma entrada que é a URL que você passou de parâmetro e tem um retorno que foi o arquivo gravado. Ele transformou uma URL em um arquivo.</p>
<p>Em sistemas UNIX ou Linux, os shells como BASH ou ZSH, onde você roda na linha de comando, permitem encadear esses programas como se fossem funções. Quando seu programa carrega e vira um processo o sistema cria três canais unidirecionais: um canal padrão de entrada ou Standard In ou STDIN, um canal padrão de saída ou Standard Out ou STDOUT e também um outro pra erros, o STDERR.
Na prática pense que todo processo pode iniciar conectando alguma coisa no STDIN e alguma outra coisa no STDOUT. Por exemplo, podemos usar o tal programa CURL pra baixar um arquivo da Web mas em vez de gravar em um arquivo local podemos redirecionar o conteúdo baixado pra esse STDOUT e conectar no STDIN de outro programa, por exemplo, um programa como o famoso grep que filtra texto pra fazer pesquisa. (curl -s <a href="http://somepage.com"target="_blank" rel="noopener">http://somepage.com</a> | grep whatever)</p>
<p>Quando ensinei sobre processos já expliquei que processos sobem em espaços isolados de memória e o código interno do processo não interfere no espaço de memória de outro processo. O que eu acabei de falar sobre encadear processos redirecionando o STDOUT de um ao STDIN do outro, usando o que chamamos de Pipes é a primeira forma mais simples de comunicação e, portanto, coordenação entre processos, mas não é o único e nem necessariamente o mais útil, porque você só liga o final de um programa ao começo de outro programa e esses canais só tem uma direção, ou seja, o primeiro programa é quem manda coisas pro segundo programa, mas não o oposto.</p>
<p>Sem entrar em muitos detalhes hoje, existem outras formas de um processo se comunicar com outro. Como já disse antes existem os sinais como SIGINT. E você pode codificar seu programa pra ignorar isso ou fazer alguma coisa quando recebe esses sinais ou eventos. Novamente é mais uma forma rudimentar de comunicação. Um terceira forma rudimentar é que dois processos abram um mesmo arquivo, daí um deles pode ir escrevendo uma linha nova de cada vez nesse arquivo e o outro processo pode ir lendo essas linhas novas.</p>
<p>Expandindo nessa idéia de um arquivo comum entre dois processos, o sistema operacional oferece outra opção, os FIFOs que se você estudou alguma coisa de estruturas de dados, funcionam como uma fila, First In, First Out, ou seja, o primeiro elemento que entra é o primeiro elemento que sai. Você pode abrir um FIFO e dar um nome e abrir o mesmo FIFO a partir de outro processo, é quase como se fosse um arquivo. Agora os processos podem se comunicar e a essa estrutura damos o nome de Named Pipe. E chamamos de pipe porque é como se fosse um cano mesmo, passando informação de um lado para o outro. Eles são limitados como os pipes simples ou arquivos. Um dos processos abre o named pipe pra ler e o outro pra escrever e por isso também a comunicação só flui numa única direção.</p>
<p>Em todos os exemplos, um dos processos serve de produtor de alguma informação e o outro processo serve de consumidor. E o mecanismo no meio, seja um arquivo ou um named pipe, serve meio como uma fila. Eu acho que o mecanismo mais simples de comunicação entre dois processos é uma fila e o sistema operacional já oferece esses mecanismos faz décadas. Porém, no nosso mundo de Web, internet e programas que funcionam como servidores - que seriam os produtores - e programas que funcionam como clientes - que seriam consumidores, esses mecanismos não funcionam, porque não se comunicam pela rede. Mas mesmo na mesma máquina, sem depender de rede, podemos simular a comunicação entre um servidor e múltiplos clientes usando named pipes ou arquivos. Porém, cada novo cliente conectando no servidor exige um novo named pipe ou arquivo. Por isso existem os sockets.</p>
<p>No caso da internet falamos sobre sockets de IP que trafegam pacotes no formato TCP. Uma das vantagens de usar sockets é que o programa servidor se liga a uma porta no sistema como já expliquei e aceita conexões de múltiplos clientes. Mas diferente de arquivos ou named pipes, o servidor só precisa se comunicar através de um único socket, a partir de onde vai aceitando as conexões de múltiplos clientes. Além de só precisar de um único socket pra abrir esse canal, a comunicação é bi-direcional, então o cliente também pode enviar dados pro servidor e vice-versa, pelo mesmo socket.</p>
<p>No caso de Unix e Linux além de um socket IP existe o que chamamos de Unix Sockets. Ele tem propriedades parecidas com sockets de IP mas só servem para processos rodando na mesma máquina já que não é um socket de rede. É a maneira preferida de fazer dois processos se comunicarem de forma eficiente porque você só precisa de um socket no processo servidor pra atender múltiplos outros processos clientes, que normalmente chamamos de workers, e ele abre um canal bi-direcional, diferente de named pipes. No episódio anterior eu expliquei como o servidor web NGINX tem um processo master, que serve como um servidor, e ele cria diversos processos workers e, claro, eles se comunicam entre si via esses Unix Sockets, canais versáteis bi-direcionais. É como outros servidores que geram forks e workers fazem para seus processos coordenarem entre si, como um Postgres ou MySQL.</p>
<p>No mundo web, onde hoje temos os tais micro-serviços, eles são basicamente processos, programas servidores, que se ligam a uma porta de rede e abrem sockets IP aceitando conexões TCP e em particular oferecendo comunicação via o protocolo HTTP. Mas na prática são processos que se coordenam via sockets. Basta vocês entenderem que sockets, em particular sockets IP, obrigam um processo a se ligar a uma porta; e são canais bi-direcionais de comunicação. Em UNIX como BSD ou clones como Linux, podem ser Unix sockets para comunicação de processos na mesma máquina ou sockets IP para comunicação de múltiplas máquinas numa rede TCP/IP. E no nosso mundo Web essa comunicação normalmente se dá através do protocolo HTTP mas não é uma obrigação, podemos ter outros protocolos mais eficientes como o Protobuf ou Protocol Buffers que é um protocolo em formato binário, que é mais eficiente que o HTTP que é um protocolo texto, inventado pelo Google.</p>
<p>Aliás, lembrem que protocolos binários sempre são mais eficientes que protocolos em texto pelo simples fato que texto ocupa muito mais espaço. A explicação é muito simples. Num computador, representamos tudo no formato binário, 1 ou 0. Você já deve ter aprendido isso então vou resumir muito rapidamente. 1 byte são 8-bits. Em 1 byte você pode representar números inteiros de 0 até 255. Por que 256? Porque 2 elevado a 8 é 256. Uma palavra double byte ou seja 2 elevado a 16 representa números até 65 mil 536.
Um número como 1,000, cabe perfeitamente em 2 bytes, se for representado binariamente. Mas, se você preferir mandar o número 1,000 como um texto, isso seria um texto de 4 caracteres. Em um sistema moderno que reconhece Unicode ou UTF-8, cada letra precisa de 2 bytes ou mais. Existe uma tabelona que mapeia cada caracter de cada alfabeto do mundo a um código. Em alfabetos ocidentais, 1 byte já resolve, mas kanjis de japonês precisa de pelo menos 2 bytes por exemplo. Mas no caso do texto 1,000 que é um, zero, zero, zero, são pelo menos 4 bytes. Em resumo, o número puro sozinho, em binário, cabe em 2 bytes, mas sua representação em texto precisa de 4 bytes, ou seja, o dobro do tamanho. Iniciantes tem dificuldade de entender isso mas comece decorando, o número inteiro 1000 é um elemento totalmente diferente do texto de 4 caracteres “1000”.</p>
<p>Isso fazia mais diferença numa época onde as redes eram extremamente lentas, e hoje você não pensa muito nisso porque qualquer um pode ligar nas porcarias de uma Net ou Vivo e pedir internet de 50 Megabits ou mais. Mas se você tem um sistema que trafega milhões de gigas por hora, todo byte faz diferença, por isso pra muita coisa o Google usa o Protobuf em vez de HTTP em seus sistemas. Pro seu dia a dia, não se preocupe tanto, HTTP tá de bom tamanho mesmo porque hoje sabemos comprimir texto com algoritmos como GZIP que podem derrubar o tamanho de um texto longo até menos de um décimo do seu tamanho. Mas isso é assunto pra outro dia.</p>
<p>Eu expliquei tudo isso porque a comunicação entre dois processos, usando um protocolo pesado como HTTP, sobre TCP/IP, passando pros sockets que seu programa se conecta, vai precisar fazer uma coisa chamada marshalling ou serialização. Ou seja, internamente no seu programa o dado, digamos o tal número 1000 puro, está numa variável que provavelmente é um inteiro de 2 bytes. Mas quando você vai enviar pra fora do seu processo, como a exigência é o protocolo HTTP, você vai serializar ou seja, transformar o inteiro binário pra um texto e cuspir pelo socket. Do outro lado um outro processo cliente vai receber esse texto e vai ter que converter de volta pra um inteiro, ou seja, desserializar o dado. Muitos dos bugs modernos em micro-serviços, ou seja, comunicação de processos através de HTTP, vem de erros nessa conversão e desconversão. Fique esperto com isso.</p>
<p>Mas eu estou me adiantando. Tudo isso foi pra explicar as diversas opções que existem para comunicação entre processos, você tem coisas muito simples como pipes, named pipes, arquivos, unix sockets e até sockets IP. Pra fazer um programinha besta, que só roda localmente, falar com outro programinha, que também roda só localmente na mesma máquina, obviamente usar um protocolo como HTTP é dar um tiro de canhão pra matar uma mosca. É descomunal e ineficiente. Na pior das hipóteses você vai querer usar Unix Sockets. Se você programa em C existem bibliotecas como OpenMPI e OpenMP para facilitar programar comunicação inter processos na mesma máquina ou mesmo entre diversas máquinas.</p>
<p>Entendido como processos se comunicam, vamos entrar dentro de um processo e falar de Threads. Eu já expliquei nos episódios anteriores que uma Thread tem acesso à toda a memória interna do processo. Então se uma thread precisa se comunicar com outra thread, elas podem basicamente compartilhar uma mesma estrutura de dados. No caso de C digamos um Array. Ou uma lista ligada. Como fica tudo no mesmo espaço de memória, não existe a necessidade de você usar mecanismos fora do processo como um named pipe ou unix sockets, porque você acessa o dado diretamente no endereço da memória virtual do processo. Mecanismos externos que eu falei antes existem porque um processo não enxerga nenhum endereço de memória externo ao seu espaço virtual alocado pelo sistema operacional. Por isso comunicação entre threads é ordens de grandeza mais eficiente, mas com essa eficiência vem todos os problemas de mutex, semáforos, bugs de race condition e dead-locks que eu já expliquei.</p>
<p>Programar com threads é um processo que tem que ser extremamente cuidadoso da parte do programador. O sistema operacional não tem como te proteger e você precisa garantir que uma thread não vai escrever em cima do mesmo endereço onde já tem outra thread escrevendo ao mesmo tempo. Num sistema que só opera com uma thread real, esse problema meio que não existe porque o scheduler do sistema operacional vai deixar só uma thread realmente rodar e a outra precisa esperar a vez, o tal do time-slice que eu já expliquei. Mas em máquinas com 2 cores ou mais, realmente 2 threads podem estar rodando exatamente no mesmo instante, com paralelismo real, e é aí que os bugs que eu falei podem acontecer.</p>
<p>Como eu disse antes, o problema todo é coordenação. Pra piorar, vocês vão notar que existe a CPU com seus cores reais, o hardware de silício, que rodam threads reais, uma por core. E existem as threads que o sistema operacional organiza e gerencia. O tal scheduler do sistema operacional como o CFS do Linux, tem acesso a esses cores físicos, mas os programas podem mandar criar 100 Linux threads. Então numa máquina Intel i5 da vida, com quad-core e hyper-threading, existem 8 threads reais que podem rodar em paralelo em cada determinado instante, mas o sistema operacional pode gerenciar centenas ao mesmo tempo, fazendo seu scheduler ir dando um pouco de tempo pra cada uma, fazendo o tal context-switching que eu expliquei, tirando as coisas da mesa de uma thread, recolocando as coisas na mesa pra outra thread e dando vez pra ela.</p>
<p>Para gerenciar essas threads, fazer o tal context switching, o sistema operacional precisa guardar o tal contexto em memória pra cada thread. E eu já disse que uma thread vai gastar pelo menos 1 MB de memória. Então quanto mais threads você criar mais memória vai gastar. Pior, quanto mais threads você criar, maior o trabalho de context switching. E pior ainda, quanto mais threads você criar maiores as chances de seu programa ter aqueles bugs por não ser thread-safe. Existe ainda outro agravante, criar threads é uma atribuição do sistema, mais especificamente da kernel. Para seu programa pedir pra kernel criar threads você precisa fazer system calls, as syscalls que eu falei, e isso também não é barato.</p>
<p>Aliás, se você não sabia, vale definir. Você pode pensar que a kernel do Linux, drivers, os programas que vem numa distribuição, e os próprios programas que você escreveu, é tudo a mesma coisa. São todos binários que rodam na máquina. Se você fez o que eu recomendei e estudou um pouco de Linux, você sabe que existe o comando <code>sudo</code>, o comando que escala seu programa pra rodar com permissões de root ou administrador da máquina. Nesse caso você sabe no mínimo que existem diferenças entre rodar como um usuário restrito normal ou rodar como um root que tem acesso a tudo.</p>
<p>Mas na verdade o root não é a coisa mais poderosa do sistema. O Kernel é que é. No final das contas é o kernel que decide o que o root pode ou não pode fazer. Então se você só chegou até o root, vamos entender outra coisa. A única coisa que tem acesso irrestrito a tudo da máquina, todos os dispositivos, toda a memória e pode executar qualquer coisa, é a kernel. Na arquitetura de processadores Intel existem 4 anéis de permissão. A Kernel boota e roda no chamado Ring 0, ou anel 0, dentro desse anel qualquer código tem acesso a tudo. Todos os seus programas, inclusive os que rodam via sudo, rodam no Anel 3, o mais restrito de todos. É onde rodam os seus programas. Nos Anéis 1 e 2 rodam os drivers e coisas de virtualização, por exemplo o Virtual Box carrega a kernel do OS que ele vai emular no Anel 1. No Anel 3 várias instruções de Assembly da máquina não funcionam, como a instrução que muda de anel, obviamente, ou a instrução HLT que dá Halt e pára a máquina. O sudo simplesmente libera mais system calls pra kernel, mas o programa ainda vai ter que fazer syscalls e pedir coisas pra kernel.</p>
<p>Num processador ARM também existem Rings mas a numeração é ao contrário, o que ele chama de EL0 é o equivalente ao user land, ou Ring 3 da Intel. O EL1 é onde roda a kernel, eu não tenho certeza mas acho que seria mais equivalente ao Ring 1 da Intel, porque no EL3 roda o Hypervisor que vai ser tema talvez do próximo episódio. Daí você pode rodar múltiplos kernels no EL1 isolados um do outro.</p>
<p>O mais importante é entender que existe User land que é onde rodam todos os seus programas e Kernel space, onde roda a Kernel, a única entidade que tem real acesso a tudo do sistema. Chamar uma função dentro do seu próprio processo é extremamente barato. Chamar uma syscall é mais ou menos como chamar uma função em outro processo, então você pode imaginar que vai acontecer algo parecido com um context switch toda vez. Fora isso, existe um salto em privilégios do Ring 3 pro Ring 0. Tudo isso custa. Ok, são micro-segundos a mais toda vez. Mas pense um servidor web com threads, com milhares de usuários pedindo requisições HTTP e seu servidor criando threads pra cada uma e matando todas que vão acabando. Milhares por segundo, milhares de syscalls por segundo, agora as coisas começam a ficar caras.</p>
<p>Não tem nada mais rápido do que código que roda dentro do seu processo. Por isso você quer evitar fazer syscalls o máximo que puder. Criar milhares de processos ou milhares de threads custa memória, custa context switch, custa escalar privilégios entre Rings. Parece que o povo do Node.js estava correto em usar I/O assíncrono, certo? Calma, I/O assíncrono e event loops ou reactors é só “Uma” forma rudimentar de concorrência em sistemas que não suportam outros mecanismos como threads. E justamente, Javascript não suporta threads. Ele é mono-thread, então a sua única opção é usar o intervalo entre atividades de I/O como arquivos ou sockets pra tentar fazer alguma coisa. Mas isso é muito pouco.</p>
<p>Eu já expliquei uma das soluções no episódio anterior. É fazer o que o NGINX faz: múltiplos processos, na média um pra cada core da sua máquina, ou seja, um processo pra cada thread e cada uma delas com um reactor pra aproveitar que chamadas de I/O costumam gastar tempo pra completar. A grande vantagem num sistema como Node.js que é um processo de um único-thread é que você tem alguma concorrência graças às syscalls de I/O assíncrono do sistema operacional, e pelo menos não tem que lidar com as dores de cabeça de mutex e bugs de sincronia como race conditions ou deadlocks. Imagine o inferno extra que ia ser se Javascript além de tudo ainda tivesse que lidar com mais esse tipo de categoria de bugs.</p>
<p>Interpretadores que dependem bastante do sistema operacional por baixo, como Python ou Ruby possuem Threads reais. Porém existe um grande problema. Para ter boa performance, eles delegam muita coisa pra módulos escritos em C. O Scipy, uma das bibliotecas científicas mais famosas do mundo Python é quase inteira escrita em Fortran e C++, por isso ela é veloz, só uma casca é escrita em Python. Essas extensões ganham acesso às estruturas de dados internas dos interpretadores, é como se fossem parte do interpretador.</p>
<p>Agora vem o grande problema: elas não são necessariamente thread-safe, na verdade a maioria não é, e você não tem como saber.  Por causa disso, as Threads de Python e Ruby, apesar de mapearem para threads reais do sistema operacional e terem a capacidade de rodar em paralelo, acabam bloqueadas por um lock gigante do interpretador, que justamente ganha o nome de Global Interpreter Lock, ou GIL.</p>
<p>Com exceção de chamadas de I/O que conseguem rodar realmente em paralelo, as threads do interpretador são bloqueadas por causa do tal GIL. Elas ainda são úteis, mas não tanto quanto numa linguagem que não tenha o GIL. Na prática, assim como Javascript, tanto Python quanto Ruby acabam sendo essencialmente single-threaded, com algumas exceções como operações de I/O. Clones deles como Pypi ou JRuby, como não precisam manter compatibilidade com extensões em C, podem desligar o GIL e usufruir de threads reais.</p>
<p>Assim como Node.js em 2009, eu já expliquei como o Twisted em 2002 implementou a mesma solução de colocar um event loop numa thread pra atender milhares de requisições de I/O assíncronas e usar o intervalo enquanto espera essas chamadas completarem pra executar alguma computação e assim ter um jeito rudimentar de concorrência. Obviamente isso tem duas desvantagens, a primeira é que basta você fazer uma computação que demore demais e o event loop inteiro é bloqueado mesmo que a chamada de I/O já tenha terminado, já que roda tudo na mesma thread.</p>
<p>E a segunda desvantagem é que sua aplicação precisa ter muitas requisições de I/O pra compensar, ou seja, é melhor se usado em servidores de rede, como um servidor de aplicações Web. Esse modelo não funciona pra programas que são pesados em computação só. Por isso Node.js ou Twisted são indicados pra aplicações web. No mundo Ruby temos opções similares como Eventmachine ou o mais recente Async. No mundo Python, depois do Twisted também foi criado o Tornado e o Gevent que tem APIs mais modernas. Na prática é a mesma solução em todos eles: um event loop rudimentar que chamamos de reactor, que é gerenciado numa única thread, intercalando syscalls assíncronas de I/O e executando algum código nos intervalos.</p>
<p>Do ponto de vista da programação do código temos um problema. A forma como se programa um event loop é fazendo uma chamada de função assíncrona e passando como parâmetro uma segunda função que deve ser chamada quando o I/O acabar. Por exemplo, uma função de ler um arquivo de template de HTML na máquina recebe de parâmetro uma segunda função pra preencher esse template e gerar o HTML final. Num sistema single-thread, com tudo bloqueante, você escreveria essas chamadas de função uma embaixo da outra. Mas num sistema orientado a eventos, você precisa passar as funções subsequentes uma como parâmetro da outra. E isso cria o horroroso padrão de cascata ou o que eu gosto de chamar, de hadouken de funções. Se você ver algo assim hoje em dia, é um jeito antigo de escrever, é hediondo e não há ser vivo no planeta que veja algo nesse formato e não sinta imediatamente vontade de vomitar um pouco na boca.</p>
<p>No C já tínhamos algo parecido mas não necessariamente só pra concorrência, é o conceito de passar o ponteiro ou referência de uma função como parâmetro de outra função, chamamos isso de callbacks. Interfaces gráficas em VB ou Delphi ou Java Swing também tinham esse conceito com Listeners por exemplo. É um jeito rudimentar de se encadear funções onde uma depende do resultado da outra dentro de um loop de eventos. Toda interface gráfica é basicamente um loop também. Na prática, você tem um callback pra quando tudo dá certo e outro callback pra quando dá algum erro.</p>
<p>O Twisted do Python introduziu um conceito de código, os Deferreds. Em vez de chamar uma função, você cria um objeto que pode ser encadeado. Esse objeto basicamente engloba a tal chamada assíncrona, por exemplo, e você pode configurar esse objeto chamando métodos como addCallback. É quase a mesma coisa que antes, mas como estamos encapsulando a função num objeto, ele fica um pouco mais maleável de se trabalhar no seu código sem precisar virar um callback hell ou hadouken que falei antes. No mundo Web o framework JQuery adicionou a mesma construção com o mesmo nome, Deferreds. Assim você podia configurar uma chamada Ajax e conectar callbacks.</p>
<p>Na prática você encapsula uma chamada assíncrona num objeto, que você vai “deferir” a execução pra algum ponto no futuro, normalmente depois de ter a oportunidade de configurar esse objeto com callbacks ou esperar outros deferreds ficarem prontos e executar tudo de uma vez só, por exemplo. De qualquer forma é uma forma melhor do que ter hadouken de callbacks.</p>
<p>Retomando, fazer syscalls é caro. Context Switching é caro. Chamar coisas, funções, dentro do próprio processo é rápido e barato. Dentro do seu processo existe seu código transformado em binário. E você tem suas funções lá. Funções que demoram demais pra executar bloqueiam todo o seu processo ou thread. E se fosse possível pausar uma função antes dela acabar, deixar ela de lado, e dar chance de outra função rodar dentro do mesmo processo? Não estou falando de criar 2 threads, estou falando de na mesma thread a função ser pausada, deixar outra executar e daí voltar a executar a primeira. É como se fosse uma thread mas como é tudo dentro do mesmo processo, sem syscalls, sem context switching. É pra isso que serve uma corrotina, pense uma função com múltiplos pontos de suspensão, que você pode continuar ou resumir depois.</p>
<p>Em particular, um caso especial de corrotina se chama Fiber ou Generator. Se você já brincou com classes de Enumerators e Iterators em Python ou Ruby, sabe pra que elas servem. Elas são basicamente funções ou métodos como qualquer outro. Mas de dentro desse método faça de conta que existe um loop gigante, indo até infinito. Significa que esse método vai bloquear tudo e você não vai rodar mais nada pra sempre. Mas de dentro desse loop você pode chamar uma função, normalmente nomeada como yield, que vai devolver o controle pro código anterior que chamou o método. Agora esse código anterior pode executar outra coisa e quando quiser pode chamar o método resume, pra resumir a execução desse método com loop gigante, e ele vai dar yield de novo e assim por diante. Se você lembra de quando eu falei do Windows 3.1, isso seria algo parecido com uma multitarefa cooperativa. Fibers são funções, ou objetos de execução, onde você codifica pontos de suspensão ou pausa da execução. A maioria das linguagens atuais têm algo assim, os nomes vão variar mas se você ouvir corotina, fiber ou generator é basicamente a mesma coisa. Python tem, Ruby tem, Javascript tem, Swift tem, Kotlin tem, todo mundo tem algo assim.</p>
<p>De qualquer forma, pra que servem essas corotinas que pausam e podem ser resumidas se elas não rodam em paralelo? Em princípio elas se parecem com threads: elas estão dentro do processo, portanto tem acesso a todas as estruturas de dados do processo. Porém, como elas não rodam em paralelo, não existe a situação de uma querer escrever em cima dos dados da outra. Você que tem que manualmente pausar e manualmente resumir uma fiber, portanto você tem controle. Por isso elas são mais simples de se trabalhar do que threads. Mas elas não rodam em paralelo, então se parecem mais com as threads de Python ou Ruby, que apesar de terem a capacidade de rodar em paralelo já que mapeia pra threads reais do sistema operacional, existe o GIL que vai bloqueá-las. Mas diferente de threads, não precisamos de chamadas pra Kernel, elas são simples abstrações dentro do seu programa, em user-land.</p>
<p>A existência dessa construção na linguagem facilita a programação de concorrência. ?Eu não disse que a forma rudimentar é uma função recebendo callbacks pra chamar outras funções quando vier o evento que determinado I/O finalizou? Depois inventamos Deferreds que já ajuda. Porém, podemos usar Fibers pra escrever o mesmo código que seria um hadouken de callbacks quase como se fosse programação imperativa tradicional, só que o yield bloqueia até a chamada voltar e daí ele resume daquele ponto. Então veja no exemplo como é um código hadouken
Agora veja o mesmo código usando fibers e a chamada de yield, e você vai ver como de repente do ponto de vista do código tudo ficou mais simples e sequencial. Então só por isso já temos um ganho absurdo em legibilidade e, por consequência, mantenabilidade do código.</p>
<p>Do ponto de vista do código, se somarmos um event loop com I/O assíncrono, conseguimos ter algum nível de concorrência e com fibers conseguimos ter um código que não é tosco. Estamos chegando em algum lugar aqui. Com isso Javascript com Node.js, Python com Twisted, Tornado ou GEvent, Ruby com Eventmachine ou Async, todos tem a possibilidade de usar essa arquitetura de reactors, porque no fundo quem realmente faz a mágica é a Kernel e suas syscalls assíncronas como epoll, kqueue ou IOCP. Porém, todas as bibliotecas que fazem chamadas síncronas, ou seja, do jeito bloqueante, precisam ser reescritas para usar esse recurso de fibers ou generators. Daí você entende porque em Python ou Ruby esse suporte é mais complicado e porque no Node.js é mais fácil: porque Node.js já nasceu com suas bibliotecas sendo criadas pra serem assíncronas.</p>
<p>Isso ajuda em linguagens que tem dificuldades com paralelismo de threads reais. Vamos falar das que tem acesso a threads reais sem o problema de um GIL ou lock global, como Java, ou C#. Pra elas existem Thread Pools. Em vez de ficar criando quinhentas threads a torto e a direito, sem controle, você limita quantas threads vão de fato existir simultaneamente. Como já disse antes uma média de uma thread por core da máquina. Tanto em Java quanto C#. Elas costumam ter alguma coisas como uma classe ThreadPool, literalmente um tanque de threads, e melhor ainda, quando tem outra abstração chamada Task ou Tarefa. A grande vantagem você vê num loop em uma lista gigante, onde você quer processar cada elemento dessa lista em paralelo. Só que se você ficar criando uma thread pra cada elemento, e a lista tiver quinhentos elementos, você vai acabar tento uns quinhentos threads pro sistema gerenciar. Ao passo que se você usar Tasks, que é só uma abstração, e configurar a ThreadPool pra ter só 5 threads, mesmo a lista tendo 500 elementos, você nunca vai ter mais que 5 threads rodando ao mesmo tempo.</p>
<p>Uma Thread Pool funciona como se fosse um load balancer de threads, vamos dizer. Nós usamos o conceito de Pool em tudo que é recurso caro. Por exemplo, bancos de dados nós usamos connection pools. Mesmo se tivermos 500 navegadores simultaneamente conectando no servidor de aplicação, não vamos criar 500 conexões no banco, vamos configurar um pool de conexões pra, digamos 100 conexões, e é só isso que vai existir, os primeiros 100 vão usar essas conexões, quando o primeiro acabar devolve a conexão pro pool e agora o próximo esperando vai pegar e usar e assim sucessivamente. A mesma coisa em ThreadPools. E esse é um dos principais conceitos que você tem que saber em arquitetura de concorrência e paralelismo: pools e filas. É como numa agência de banco. É dia de pagamento, 100 pessoas aparecem na hora do almoço, mas só vai ter 10 caixas. As 10 caixas representam o pool. E assim vamos tendo 10 pessoas sendo atendidas simultaneamente e as outras esperam na fila. É assim que o scheduler de threads funciona também. E é assim que podemos ter algum controle sobre as threads do sistema operacional.</p>
<p>No mundo .NET você tem classes como Parallel e Task que abstraem esses conceitos. Em Ruby temos também uma blblioteca chamada Parallel que vai abstrair pools de forks ou pools de threads. No mundo de Mac e BSD existe o Grand Central Dispatch e seus DispatchQueue. Enfim, cada linguagem ou sistema operacional vai oferecer alguma coisa que tem essa arquitetura. Alguma combinação de queues ou filas e pools. Preste atenção: você raramente quer usar Threads reais diretamente, especialmente em grandes quantidades. Se precisar de threads sempre use Pools, Queues e Tasks.</p>
<p>Esse conceito também se estende pra computação distribuída. Até agora sempre falamos de um código que, na hora que precisa, carrega alguma forma de executar uma tarefa em paralelo. Ou um fork pra gerar um novo processo. Ou criando uma nova Thread. Ou agora criando Pools. Mas existe a opção de já ter essa segunda entidade pré-carregada e esperando. Um segundo processo, ou uma thread ou algo assim.</p>
<p>Em Java sempre tivemos no Enterprise Edition os JMS ou Java Message Service. No mundo Ruby on Rails usamos coisas como Sidekick, a aplicação conecta a uma fila representada num banco de dados Redis e existe um ou mais processos que ficam pré-carregados e ouvindo nessa mesma fila. Chamamos esses processos secundários de Workers. Temos isso em quase toda linguagem. Uma fila e vários workers, e como é um número limitado de workers é como se fosse um Pool também.</p>
<p>No mundo .NET temos opções como o projeto Hangfire. No mundo Python temos projetos como Celery ou RQ. No mundo Node.js temos coisas como o Kue. No mundo Java temos Message Queues no JEE ou Background Jobs no Spring. O conceito é sempre o mesmo: algum tipo de fila como RabbitMQ, Redis ou qualquer coisa assim, e diversos workers consumindo dessas filas. É uma solução de paralelismo que funciona tanto na mesma máquina quanto em diferentes máquinas, representando um tipo de computação distribuída. Outro nome que você vai ouvir associado a esses mesmos conceitos é Background Jobs.</p>
<p>Esse tipo de solução resolve a grande maioria dos problemas reais que temos hoje. Mesmo numa aplicação mono-thread, que tenha um reactor, ainda temos operações que custam tempo e processamento, que eu já expliquei que vão bloquear o event loop. Em vez de executar isso no processo principal, podemos enviar uma mensagem a uma fila e deixar outro processo, ou worker, fazer esse trabalho pesado, e assim deixar o reactor principal mais leve e mais rápido.</p>
<p>Mas ainda assim gostaríamos de alguma coisa mais eficiente dentro de um mesmo processo. E até agora não chegamos lá ainda. Mas se você prestou atenção aos episódios anteriores, existe sim uma construção pra definir concorrência em sistemas operacionais e hardware que não tem mais do que uma thread real. Uma green thread. Ou seja, uma thread que só existe em User land e não em kernel space. É o que o protótipo do Java original tinha no começo quando foi desenhado pra set-top-boxes, tipo Tivo como eu expliquei em outro episódio. Um conceito de hardware barato do começo dos anos 90 que não rodaria nada muito mais potente que um DOS. Se quiséssemos programar concorrência, não haveria threads reais, mas mesmo assim podemos programar concorrência com green threads, só que elas nunca vão rodar em paralelo. Lembram? Uma coisa é ter concorrência, outra coisa é ter paralelismo. Ter paralismo implica ter concorrência, mas não o oposto.</p>
<p>Corotinas, fibers ou generators que expliquei acima, seriam mecanismos para chegar em green-threads: funções com pontos de suspensão, onde podemos gerenciar várias delas ao mesmo tempo, e ir executando um pouco de cada uma de cada vez. À primeira vista, hoje que temos máquinas multi-core com múltiplas threads paralelas reais, pra que serviriam green-threads? Pra muita coisa. Eu já repeti várias vezes e vou repetir de novo: threads custam memória, talvez 1 MB, tem context switching caro, executado pelo scheduler da kernel, tem syscalls que precisam pular de user-land pra kernel space. Green threads são baratas, dependendo da linguagem pode custar talvez na ordem de 2 quilobytes, ou seja umas 500 vezes mais leve que uma thread. Elas estão dentro do mesmo processo, em user land, e não dependem de syscalls nem de context switching da kernel. Ou seja, comparado com threads, elas são realmente muito mais baratas. Mas tem um problema: só com o que sabemos até agora, elas não rodam em paralelo.</p>
<p>Aí que você se engana. E se, usássemos o conceito de thread pools e fizéssemos um código que pegasse milhares de green-threads e mandássemos executar num pool de threads reais? Basta construirmos um scheduler em user-land que se encarregaria de fazer esse load balancing de green-threads entre threads num pool. E muito bem chegamos à base de um Scala, Clojure, Go ou Erlang e, por consequencia, Elixir.</p>
<p>Erlang, nos anos 80, fez o seguinte: quando sua VM Beam sobe, ele sequestra os recursos da máquina pra ele: o I/O, a memória e principalmente as threads reais do sistema operacional. Ele roda um scheduler em user land pra cada thread real. As funções em Erlang não são só fibers, elas são corotinas, e a VM tem o poder de arbitrariamente pausar uma função quando bem entender, ou seja, ela não depende que você tenha programado um yield na função pra devolver o controle, ela vai pausar quer você queira ou não, exatamente como a Kernel do Linux faz com seus programas. Ou seja, o Erlang implementa o equivalente a um scheduler preemptivo de green threads em user land, e em vez de fibers ela tem corotinas de verdade.</p>
<p>Mas o Erlang tem algo mais importante ainda. Um dos problemas de uma thread é que se ela tem problemas, exceções, erros e outros bugs, ela pode corromper toda a memória do processo. Então basta uma thread fazer bobagem e seu processo inteiro pode crashear. Pra evitar isso o Erlang resolveu que cada uma dessas suas corotinas não compartilhasse nenhuma memória com outras corotinas. Na verdade, pra dificultar nosso vocabulário, dentro do Erlang ele chama essas corotinas que estão executando de “processos”, não confunda com processos de verdade de um Linux, apesar do comportamento ser similar. Existem funções normais como de qualquer outra linguagem, mas existem esses processos de erlang. Como esses processos não enxergam memória de outros processos, se você quiser fazer um processo se comunicar com outro temos o equivalente a Unix Sockets se fossem processos de Linux, canais bi-direcionais por onde eu posso enviar mensagens.</p>
<p>Essas mensagens são estruturas de dados, de novo, não compartilhadas. A VM do Erlang, assim como um OS Linux, aloca espaços de memória protegidos pra cada processo. Mas diferente do kernel Linux, que aloca 1 MB pras estruturas de um processo ou thread, o Erlang não precisa alocar mais que 2 quilobytes pra cada processo. O processo vai recebendo mensagens num mailbox, um tipo de fila e escolhe o que fazer com essas mensagens quando quiser. Quando um processo termina ou crasheia, existe um garbage collector pra cada processo individualmente. E isso pra mim é o que diferencia Erlang de todo o resto, um processo, que é um green-thread, com bug não afeta e nunca corrompe o resto do sistema já que ele não compartilha nada.</p>
<p>Eles não compartilham ponteiros nem nada e por isso nunca tem a capacidade de desestabilizar o sistema. Por isso eu costumo dizer que a VM Beam do Erlang é quase como se fosse um mini-Linux. Pra ficar mais similar ainda, os Linux tem um processo master que dependendo da distro pode ser um initd ou systemd ou outro que sobe daemons, que já expliquei antes, são processos especiais, serviços do sistema. No Erlang temos a mesma coisas só que se chamam Supervisors. Eles se encarregam de quando você subir sua aplicação, no boot da VM do Erlang, vai subir os supervisors que vão iniciar os processos necessários e garantir que se um deles crashear ele vai subir um novo processo no lugar.</p>
<p>Eu expliquei em 2 minutos o Erlang, mas pense em Erlang mais como um Java: uma máquina virtual que te oferece diversos serviços, incluindo coisas que o Java ou .NET não tem: schedulers e processos. Esses processos são green-threads. Tudo roda em user-land, e por isso é extremamente barato. É tão barato de fato que podemos fazer o que eu expliquei que fazíamos antigamente: toda nova conexão de rede antes a gente gerava uma thread real pra lidar com ela. Mas com o C10K descobrimos como é caro e não escala ficar criando threads reais o tempo todo pra toda conexão. 10 mil threads consumiria 10 gigabytes de RAM. Mas 10 mil processos Erlang consome só 19 Megabytes. Portanto, como essa green-thread é ordens de grandeza mais barato, podemos voltar a criar uma green-thread por conexão se quisermos. Entenderam a diferença? Green-threads são ordens de grandeza mais eficientes que uma thread real, e se a máquina virtual tem seu próprio scheduler pra gerenciar a thread-real pra rodar as green-threads temos paralelismo de verdade.</p>
<p>No Java normal não temos essas construções, mas outras linguagens por cima do Java como Scala e Clojure implementam estruturas similares de green-threads e schedulers. Em particular o Scala introduziu o framework Akka por volta de 2009, que implementa a arquitetura de Actors. Actors, é como eu chamo essa função especial no Erlang, os processos, que tem características de corotinas com múltiplos pontos de suspensão, com mailboxes que servem como filas de comunicação, e onde as mensagens trocadas entre processos não compartilham memória, nunca trafegam coisas como ponteiros, mantendo as green-threads isoladas entre si. Isso é um Actor, basicamente. Por isso tanto Erlang quanto Scala compartilham de arquiteturas similares, apesar das linguagens em si serem bem diferentes. O Jonas Bonér, criador do Akka, se inspirou diretamente no Erlang pra fazer o framework Akka do Scala, por isso eles tem conceitos similares.</p>
<p>Lembram quando eu expliquei que os schedulers de thread de Linux e o próprio suporte de pthreads eram ruins no Linux e que só depois de 2007, na kernel 2.6, que ganhamos o NPTL e o CFS do Igor Molnár? Eu expliquei que o sistema operacional mapeia uma estrutura de thread pra cada core real do hardware. O tal modelo um pra um. Mas se você adicionar um scheduler e múltiplos green-threads em user land, voltamos ao modelo M pra N que eu disse que a IBM tentou implementar no Linux com o projeto NGPT. Lembram que eu falei pra vocês guardarem essa informação? Estamos revisitando o tema agora.</p>
<p>O Go faz mais ou menos a mesma coisa. O que o Erlang chama de processo, em Go se chama goroutine. Só que eu diria que as goroutines são mais baixo nível do que um Actor de Scala. Num Erlang, assim como num Linux, cada processo tem um PID ou Process ID, um número que identifica unicamente cada processo rodando. Esse PID serve como um e-mail, e um processo pode mandar mensagens pra outro processo através desse PID. O Erlang é tão versátil que podemos mandar mensagens pra um PID de um processo em outra máquina.</p>
<p>O Go tem uma outra abstração: as channels, literalmente canais. Duas gorotinas podem compartilhar uma mesma channel. Uma gorotina escreve num channel e outra gotina pode ficar escutando do mesmo channel. Lembram de algo similar que expliquei hoje? Na prática não me parece nada muito diferente de um named pipe entre processos Unix, um canal unidirecional bloqueante. E eu disse que gorotinas e channels são mais baixo nível que Actors de Erlang ou Scala porque eles permitem trafegar qualquer coisa, incluindo ponteiros de memória, ou seja, eles compartilham memória entre si e, portanto, tem os mesmos problemas que programar com threads reais: se você escolher trafegar ponteiros, vai precisar lidar com mutexes e locks e os temidos bugs de race condition e deadlocks.</p>
<p>Diferente de Erlang ou Java, o Go não é uma máquina virtual, cheia de serviços. A filosofia e os usos são diferentes. Ele tem um runtime por causa do controle das gorotinas que podem rodar em paralelo. Diferente do scheduler de Erlang que é preemptivo, o scheduler de Go é cooperativo. Ele espera determinados eventos numa gorotina como syscalls pra receber o controle de volta e dar a vez pra outra gorotina. É menos sofisticado do que Erlang nesse caso mas é mais leve e mais previsível também. E obviamente, por causa disso ele é automaticamente mais poderoso do que Node.js e mais simples de programar aplicações complexas. Assim como Erlang ou Scala, um reactor é desnecessário, porque eles tem schedulers que podem pausar uma corotina quando alguma coisa como I/O bloqueante é executado. Na minha opinião, schedulers em user-land pra corotinas são melhores do que reactors. Reactors são interessantes em linguagens que não tem como ter schedulers em user-land. A premissa pra isso é acesso irrestrito às threads reais, sem locks globais que Python ou Ruby tem.</p>
<p>Do ponto de vista puramente dos interpretadores ou máquinas virtuais. Temos PHP, Python, Ruby, Perl e Javascript que são todos interpretadores. Alguns tem acesso a threads reais, mas na prática, todos são essencialmente single-threaded, com locks globais que chamamos de GILs por conta das antigas extensões em C. Em todos eles, a opção de um Reactor com a ajuda de Fibers pra não complicar a programação, é a melhor solução pra concorrência. Erlang e Java são máquinas virtuais, no caso de Java significa que Scala, Clojure ou mesmo Kotlin, todos rodam sobre a mesma máquina virtual. Essas VMs são excepcionais trabalhos de engenharia e foram feitas pra sequestrar todos os recursos da máquina. O melhor caso de uso é seu programa e a VM rodarem sozinhas na máquina e elas gerenciarem tudo. E mais do que isso: o melhor caso de uso é que uma vez iniciadas elas não fiquem rebootando toda hora. Guardem essa informação pro episódio de devops.</p>
<p>Erlang, na minha opinião, é quem tem as melhores abstrações de concorrência, e por consequência Elixir também já que ele basicamente gera bytecodes pra mesma VM, usando os mesmos frameworks. Em Java, Scala e depois Clojure e Kotlin adicionaram abstrações de linguagem e bibliotecas pra facilitar concorrência, inclusive no caso de Scala e Clojure implementando schedulers user-land e passando a adotar o conceito de green-threads, que o Java nativo não tem. O C# veio copiando cada uma dessas coisas de outras linguagens como Fibers, ThreadPools e tudo mais, mas que eu me lembre ele também não tem schedulers em user-land nem green-threads. Kotlin, Swift, C# tem sim classes chamadas schedulers que não é a mesma coisa que eu falei de Scala ou Go. Eles não são serviços do runtime, eles são classes pra agendar coisas pro futuro, como um cron num Linux, onde vc diz pra determinada tarefa rodar numa thread num determinado horário ou numa determinada frequência. Os nomes confundem, eu sei, mas schedulers de Kotlin, por exemplo, é como se fosse um cron.</p>
<p>O Go é uma linguagem que compila binário nativo, assim como Rust ou C. Mas o Go possui um runtime, pense um pedaço grande de código que gruda no seu pra gerar o binariozão final. Ele tem um scheduler real cooperativo e oferece green-threads com coordenação via channels, que como eu disse, parece com named pipes como eu expliquei no começo do episódio. Rust e outras linguagens como Crystal ainda Não tem o mesmo nível de concorrência e paralelismo. Mesmo em Rust o suporte a I/O assíncrono demorou pra chegar. Crystal está agora ganhando suporte experimental a rodar fibers em paralelo, mas nenhum dos dois possui schedulers em user-land que vem pré-embutido no runtime da linguagem.</p>
<p>Se não ficou claro, Javascript não tem paralelismo. Ele é a mais simples de todas as linguagens, e muito do investimento em engenharia no Google se focou em tornar a V8 muito boa em cuspir código nativo, portanto ele é um Just In Time Compiler, um JIT muito bom. Ele não tem sintaxe complicada, não tem bibliotecas padrão pra manter, e obviamente não oferece scheduler user-land também. Não tem um GIL como Python ou Ruby, mas também não tem acesso a threads reais. Sua única forma de concorrência são generators ou fibers, em multitarefa cooperativa, e na falta de um scheduler, essa multitarefa é controlada num event loop reactor. No máximo dá pra fazer fork pra outros processos. Porém, Javascript troca performance por uso de memória, então Copy on Write do Linux Não ajuda muito porque ele faz JIT, então muita coisa não pode ser reaproveitada entre processos. Por isso eu tendo a dizer que apesar de ser rápido, é uma das linguagens mais rudimentares que temos.</p>
<p>Eu ia falar sobre a parte da programação, mas como já tá comprido pra caramba de novo vou só resumir. Eu expliquei até agora sobre como as coisas funcionam por baixo. Agora como tudo isso fica no seu código? Se lidarmos com threads reais, já expliquei que precisamos ficar programando mutexes pra lá e pra cá. Em linguagens antigas como Java ou C#, que expõe Threads reais, você ainda precisa se preocupar com isso. Em Go também você precisa se preocupar com isso, porque ele transporta ponteiros nos channels entre gorotinas. Ou seja, o oposto de um Erlang ou Elixir que não compartilha nada entre seus processos. Portanto ficou na sua mão como desenvolvedor se virar com sincronização e coordenação. Experimente criar um deadlock no seu channel de Go: seu programa crasheia. É um dos motivos de porque eu fico decepcionado com Go em 2019 que já poderia ter oferecido abstrações melhores direto na linguagem. Mas é uma opinião pessoal, o mesmo problema acontece num C ou C++ onde eu tenho acesso a tudo, incluindo ponteiros. Esconder ponteiros é uma decisão de design da linguagem. Mas assim como GOTO desapareceu, Null é considerado ruim hoje em dia, Ponteiros também já haviam sido aposentados quando Java foi criado, não é nenhuma novidade.</p>
<p>De qualquer forma, Node.js no começo, por 2010, tornou famoso o estilo porco de programação com callbacks. Twisted e JQuery tornaram famosos a abstração de Deffereds. E Scala, não lembro se foi o primeiro, mas foi um dos que ajudou a popularizar o termo Future. Não deixa de ser uma pequena evolução sobre Deferreds. Na prática é mais uma forma de encapsular uma execução futura num objeto, um tipo de placeholder. Em vez de fazer sua variável receber direto o resultado de uma chamada assíncrona ou mesmo uma chamada externa tipo uma chamada HTTP, que não se tem certeza se vai completar, ou uma query num banco, você faz a variavel receber um Future e o Future encapsula essa chamada incerta. Quando o Future executar e resolver, ele é substituído pelo resultado.</p>
<p>Ninguém mais usa os nomes Deferred ou Futures ou mesmo Delay, o próximo passo nessa evolução são Promises, em particular o famoso spec A+. Se você está em Javascript faz alguns anos, já deve estar de saco cheio de ouvir falar de Promises. Dois ou três anos atrás eu dizia que a ironia de Promises é que eles permaneciam sendo promessas. Enfim, na prática, Promise é como eu já falei antes de Deferred e Futures, um objeto que encapsula alguma chamada incerta. Esse objeto é encadeável e customizável e costuma ter métodos como then e catch, que é onde você adiciona callbacks, como o addCallback do Deferred de Twisted. Java 8 tem CompletableFuture, frameworks como Guava tem SettableFuture. Javascript tem Promise/A+.</p>
<p>Deffereds, Delays, Futures, Promises, são termos que acabam sendo usados um no lugar do outro, mas hoje em dia as pessoas falam mais de Promises. Sobre os objetos de Promises podemos adicionar o que chamamos de syntactic sugar, ou literalmente um açúcar na sintaxe da linguagem pra não ter que lidar com objetos de Promise diretamente nem essas configurações via then ou catch. Se não me engano a Microsoft desenvolveu a sintaxe de async/await pro C# e várias outras linguagens implementaram algo similar.</p>
<p>Voltando um pouco, em linguagens que tem Threads costuma também existir a função de join. Por exemplo, você pode criar um array ou lista com 10 Threads. Ao criar todas elas, o código pai que está criando pode chamar Join em todas elas. Assim o programa principal vai ficar bloqueado até todas elas executarem e retornarem. Daí ele continua a execução com o resultado de todas as threads, que garantidamente já acabaram de rodar. Async e Await é quase a mesma coisa, funções marcadas como async costumam devolver um objeto Promise e o await é como se fosse um join numa thread, ou um yield num Fiber, é tipo um join numa promise, esperando que ela resolva pra depois continuar.</p>
<p>A diferença é que async/await é uma semântica que pode ser usada em threads, green-threads ou corrotinas ou fibers. Então é como se fosse Join pra coisas que não são threads. Todo programa que você escreve são trechos de código que chamam outros trechos de código, uma função chamando outra. Pra explicar um exemplo, vamos chamar a primeira função de Maria e a segunda função de Joao. Maria chama Joao, Joao executa, e devolve controle pra Maria. Se Joao for uma Fiber, Maria chama Joao, Joao começa a executar e dá yield, suspende e devolve controle pra Maria. Agora Maria pode fazer outra coisa e, se quiser, pedir pro Joao resumir de onde parou.</p>
<p>Com Promises é assim: Maria chama Joao, Joao não faz nada e devolve uma promessa. Maria pode escolher quando cobrar a promessa. Daí ela cobra, e Joao executa o prometido e quando termina “resolve” devolvendo o resultado. Agora Maria pode escolher o que fazer com o resultado. Ela tem duas opções, ou usar a sintaxe padrão de Promises e dizer ao Joao: Joao quando você terminar chama a Camila. Ou, a Maria pode cobrar a promessa do Joao, esperar ele terminar que é o await, e quando terminar ela mesma pode chamar a Camila. Entenderam, é uma questão de estilo de código, ambos vão chegar no mesmo resultado, mas hoje em dia se tornou consenso que se precisar fazer uma chamada externa, por exemplo uma requisição de rede, que não sabemos quando vai ficar pronto ou mesmo se vai completar, então englobamos num Promise. Daí escolhemos se esperamos já ele terminar ou se só atrelamos nele um callback e vamos fazer outra coisa.</p>
<p>Processos, threads, user-land e kernel-space, context switching, schedulers de user-land, corrotinas, fibers, generators, são construções do sistema operacional, interpretador, runtime ou máquina virtual pra modelar e executar concorrência e paralelismo. Recursos das linguagens como filas, mailboxes e channels, promises e async/await são recursos das linguagens pra facilitar a programação de concorrência e paralelismo.</p>
<p>Finalmente, chegamos ao final. Espero que tenham aprendido o suficiente pra começar a entender como as diferentes linguagens lidam com concorrência e paralelismo, o que de fato acontece por baixo dos panos, e não misturem o que uma linguagem pode ou não pode fazer. Na prática, hoje em dia, todas as linguagens tem recursos muito bons. Alguns servem pra coisas que os outros não são tão bons, então nenhuma das linguagens que falei aqui é obrigatoriamente melhor do que a outra. Não adianta ser só rápido. Não adianta só ter concorrência massiva. Uma boa linguagem, primeiro, precisa de um ecossistema que gera resultados com eficiência. Quanto menos desperdício, quanto mais resultados, pra determinado caso de uso, melhor.</p>
<p>Estamos já faz muito tempo nesse assunto de back-end e o assunto de concorrência se esticou bastante. Ainda estou em dúvida se semana que vem ainda adiciono mais um tema, que é relacionado a gerenciamento de memória, ou se já vou direto pro assunto final da série: devops. Vou pensar ainda. Se ficou com dúvidas não deixe de mandar nos comentários abaixo, se curtiu mande um joinha, compartilhe com seus amigos, não deixe de assinar o canal e cilcar no sininho pra não perder os próximos episódios. A gente se vê semana que vem, até mais.</p>
]]></content:encoded><category>sistemas-distribuidos</category><category>linguagens-de-programacao</category><category>backend</category><category>akitando</category></item><item><title>[Akitando] #43 - Concorrência e Paralelismo (Parte 1) | Entendendo Back-End para Iniciantes (Parte 3)</title><link>https://akitaonrails.github.io/2019/03/13/akitando-43-concorrencia-e-paralelismo-parte-1-entendendo-back-end-para-iniciantes-parte-3/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/03/13/akitando-43-concorrencia-e-paralelismo-parte-1-entendendo-back-end-para-iniciantes-parte-3/</guid><pubDate>Wed, 13 Mar 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/cx1ULv4wYxM"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;De volta à série Começando aos 40, estamos já no oitavo episódio! E ela é a Parte 3 do tema de Back-end, mas desta vez vou precisar me alongar mais explicando conceitos antes de retornar às ferramentas.&lt;/p&gt;
&lt;p&gt;Concorrência e Paralelismo é algo que todo iniciante hoje em dia já esbarra logo cedo. Nós vivemos num mundo que é naturalmente paralelo e concorrente. Já estamos no ponto onde nos definimos como &amp;ldquo;multi-tarefas&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/cx1ULv4wYxM"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>De volta à série Começando aos 40, estamos já no oitavo episódio! E ela é a Parte 3 do tema de Back-end, mas desta vez vou precisar me alongar mais explicando conceitos antes de retornar às ferramentas.</p>
<p>Concorrência e Paralelismo é algo que todo iniciante hoje em dia já esbarra logo cedo. Nós vivemos num mundo que é naturalmente paralelo e concorrente. Já estamos no ponto onde nos definimos como &ldquo;multi-tarefas&rdquo;.</p>
<p>Mesmo assim, ainda existe mais superstição e &ldquo;misticismo&rdquo; do que real noção do que esse conceito realmente significa. E ao contrário do que possa parecer, na realidade o básico não é tão complicado assim.</p>
<p>Preste bastante atenção na explicação de hoje, porque isso vai ser base pra todo o resto que vou explicar até o fim desta série.</p>
<p>Se você já conhece os detalhes do que estou explicando, obviamente pra caber num vídeo para iniciantes, estou simplificando BASTANTE muitos conceitos pra ilustrar. Infelizmente não cabe tudo num episódio só, mas não deixem de comentar se achar que faltou alguma coisa muito importante!</p>
<p>Links:</p>
<ul>
<li>The Linux Scheduler: a Decade of Wasted Cores (<a href="https://blog.acolyer.org/2016/04/26/the-linux-scheduler-a-decade-of-wasted-cores/"target="_blank" rel="noopener">https://blog.acolyer.org/2016/04/26/the-linux-scheduler-a-decade-of-wasted-cores/</a>)</li>
<li>The C10K problem (<a href="http://www.kegel.com/c10k.html"target="_blank" rel="noopener">http://www.kegel.com/c10k.html</a>)</li>
</ul>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Vamos voltar à Parte 3 do tema de Back-end. Recapitulando já expliquei os conceitos básicos do computador e do sistema operacional, sobre processos e threads. Expliquei sobre compilação e bibliotecas estáticas e dinâmicas. Já expliquei sobre máquinas virtuais e interpretadores. Demos uma rápida passada em características de algumas linguagens, a história do Java, um pouco do lado .NET, até dei uma tangente na história inicial de desenvolvimento mobile e mais um pouco sobre licenças de software.</p>
<p>Estamos já no oitavo episódio da série Começando aos 40. E hoje acho que é hora de começar a tratar de um tema que é muito discutido mas eu acho que os iniciantes tem dificuldade pra visualizar na cabeça. Concorrência e Paralelismo. Mas antes entrarmos nos assuntos mais óbvios como Go ou Elixir ou Node.js precisamos voltar na história como sempre pra que todos finalmente comecem a entender esse assunto de verdade!</p>
<p>Infelizmente explicar concorrência e paralelismo só falando é muito difícil. Vocês ainda vão precisar praticar programando centenas de vezes coisas diferentes pra começar a entender de verdade como as coisas funcionam. Eu espero que a explicação que eu dê agora pelo menos ajude um pouco vocês terem uma imagem na cabeça do que esperar quando realmente colocarem as mãos no metal. Vamos lá!</p>
<p>(&hellip;)</p>
<p>Hoje em dia todo mundo tem um computador potente. Mesmo o mais fraco Intel i3 é dual core com 3 Ghz ou mais. E se você tiver a bufunfa pra comprar os novos Intel i9 ou AMD Threadripper com 8 cores e mais de 4 Ghz, você tem mais poder de computação do que os mais poderosos computadores dos anos 60, combinados. E se falarmos de processadores de servidor, como os Intel Xeon E7 temos até 24 cores. E vocês sabem que a Intel tem a tal tecnologia hyperthreading que faz você enxergar 2 threads simultâneas num mesmo core, então esse Xeon E7 de 24 cores pode ter até 48 threads de execução. Isso é bastante paralelismo. Até mesmo seu celular Android, com o chipset Qualcomm 845, é nada menos que octa-core com 2.8 Ghz cada.</p>
<p>E isso porque não estamos falando de GPUs que tem propósitos diferentes de um CPU, mas possuem cores que processam em paralelo também, no caso da NVIDIA o que chamamos de CUDA cores. E na nova RTX 2080 temos nada mais, nada menos, que 2.944 CUDA cores. Sendo que em placas mães melhores podemos colocar 2 placas dessas rodando em paralelo e poder ter acesso a quase 6 mil CUDA cores ao mesmo tempo. Isso sim é processamento paralelo massivo.</p>
<p>Então, do seu celular, a desktop, servidores, estamos claramente num mundo massivamente paralelo. Mas não foi sempre assim. Considere que acesso a esse mundo paralelo só começou de verdade pra nós programadores a coisa de pouco mais de uma década. Pra entender como tudo mudou, vou resumir como foi do fim dos anos 50 até agora.</p>
<p>Nos anos 50 talvez vocês já tenham ouvido falar do meme que a IBM dizia que o mundo não precisa de mais do que cinco computadores. Eles estavam falando do IBM 701. E se você não sabia disso, essa frase foi tirada de contexto e é fake news, pra variar. A frase em contexto foi numa reunião de acionistas que dizia que a IBM tinha planos de vender talvez cinco IBM 701 mas voltaram com pedidos pra 19. Estamos falando de uma máquina que, se fosse alugar na época, não custaria menos que 15 mil dólares, em dólares dos anos 50. E a potência? Era uma máquina de 18-bits. Lembrando que seu celular hoje é 64-bits. E essa máquina vinha com nada menos que 1 quilobyte de memória e podia suportar um máximo de 9 quilobytes. E ainda foi a primeira que vinha com … gravador de fita, que podia gravar nada menos que 1.2 megabytes a uma velocidade estrondosa de 5.6 kilobytes por segundo. Lembrando que sua placa de rede ethernet porcaria trafega dados a pelo menos 1 gigabits por segundo, e isso é considerado lento hoje em dia dado que existem placas de 10 gigabits.</p>
<p>Então você imagina que numa máquina cara dessas, basicamente uma calculadora glorificada, você gostaria de usá-la da forma mais eficiente possível. Era uma época onde a linguagem mais avançada era Fortran pra fazer cálculos. E onde você carregava programas com cartões perfurados, numa máquina que conseguia ler no máximo 150 cartões por minuto e perfurar no máximo 100 cartões por minuto. Então a forma mais eficiente de usar essa máquina era os programadores serem extremamente cuidadosos pra escrever programas de cálculo muito precisos e sem erros, que chamamos de jobs, e organizar esses jobs em batches ou grupos de programas similares, e deixar numa fila pra ir processando dia e noite. Foi a época dos jobs em batch. Se tivesse um erro no seu programa, você só ia saber no dia seguinte e ter que repetir todo o processo.</p>
<p>Então dos anos 50 pros anos 60, alguns grandes nomes que já falei antes como John Backus, criador do Fortran, ou John McCarthy que é considerado o pai da inteligência artificial e o criador do garbage collector do Lisp, começaram a trabalhar um conceito para aumentar a eficiência dos programadores. Em vez de terem que colocar seus jobs numa fila e ficar só esperando de braços cruzados, e se fosse possível usar as ineficiências do computador, como o tempo que ele fica lendo ou escrevendo nas fitas e outras pausas, pra permitir que múltiplos programadores pudessem trabalhar concorrentemente no mesmo computador?</p>
<p>O conceito básico era que fosse possível salvar o contexto do ambiente de um programador e trocar pelo contexto de outro programador. Entenda contexto como o que define o ambiente de trabalho do programador, o que está na memória basicamente. Se você trocar muito rápido entre um contexto e outro, ou seja fazer switch, para os programadores “parece” que ele está sozinho na máquina, mas na verdade a máquina está gerenciando entre múltiplos contextos. A máquina oferece um pedaço de tempo, ou time-slice, para cada ambiente e vai dando a vez pra outro ambiente, sucessivamente. Esse é o conceito de time-sharing baseado em context switching.</p>
<p>Esse conceito nasceu na época dos IBM 704 e IBM 709 e foi incorporado aos mainframes dos anos 60. O conceito de time-sharing existiu mais no mundo corporativo, em mainframes e mini-computadores dos anos 60 até 70. Nos anos 80 principalmente começou a aparecer nos UNIX usados em workstations. E só nos anos 90 começou a de fato a aparecer no mundo dos desktops. Se você usou micro-computadores nos anos 70 e 80, lidou com micros muito simples, com CPUs como o Z80 ou Motorola 68000 ou mesmo Intel 8088. Todos CPUs de um único core, que só sabiam executar uma instrução de cada vez a cada giro do clock.</p>
<p>Se você já teve um Sinclair, MSX, Commodore 64, Apple II ou outras máquinas 8 bits ou 16 bits da década de 80, você se viu na mesma situação dos computadores dos anos 50: só conseguir executar uma tarefa ou um job de cada vez. Num MS-DOS você podia executar um programa, mas pra executar outro precisava sair do primeiro e só depois carregar o segundo. Ou usar um truque de interrupção, a famosa INT 21H que era o recurso de Terminate and Stay Resident ou TSR. De uma forma rudimentar você podia carregar um segundo programa, chamar essa interrupção pra devolver controle pro DOS mas deixar residente em memória de forma que podia ser chamado de volta. Era como utilitários pequenos com o Borland Sidekick funcionavam na época. E isso meio que simulava um tipo de multi-tarefa.</p>
<p>Foi só com o Windows 3.1 que a maioria das pessoas realmente experimentou mais esse conceito de multi-tarefa, mas no caso era multi-tarefa cooperativa. Quando falamos em multi-tarefa num computador com CPU de um único core, pensa numa mesa de escritório sendo dividida entre mais de uma pessoa. A primeira pessoa chega, coloca as coisas dela na mesa e começa a trabalhar. Pra outra pessoa usar, a primeira precisa tirar todas as coisas dela da mesa e deixar a outra colocar as coisas dela. As coisas da primeira pessoa é o tal contexto e a troca é o switching. É o mesmo conceito de time-sharing que falei antes. Mas se a segunda pessoa se recusar a sair da mesa, ninguém mais consegue usar a mesa, então ela precisa cooperar pra todo mundo poder usar. Esse é o conceito de multi-tarefa cooperativa, que é o que tinha no Windows 3.1. Se um programa fizesse alguma coisa que bloqueasse o sistema como imprimir, todos os outros programas ficavam impedidos de ter sua vez pra rodar.</p>
<p>Isso veio a mudar em sistemas operacionais 32-bits como OS/2 ou Windows 95, Windows NT e também nas distribuições Linux que começaram a aparecer do começo pro meio da década de 90. Nesse caso temos o que já expliquei antes: cada processo era iniciado pelo sistema operacional num espaço de memória protegido, ou seja, onde um programa não consegue escrever em cima da memória do outro. Além disso agora não é o programa que tem que cooperar pros outros poderem rodar. Na metáfora da mesa, imagine que agora tenha um supervisor, que tira a pessoa da mesa pra deixar a outra usar mesmo se a pessoa não quiser sair naquele momento. Esse supervisor é que chamamos de scheduler do sistema operacional. Ele aloca um pedaço de tempo, ou time-slice pra cada programa e interrompe pra deixar outro rodar. A forma de fazer isso é pegar o que está na tal mesa, separar do lado e colocar as coisas da outra pessoa na mesa. E quando der o tempo, ele tira as coisas da mesa e devolve o que tava antes e vai fazendo esse switching de contexto. Isso é multi-tarefa preemptiva.</p>
<p>Isso é feito no nível do processo, que na metáfora é o que está executando na tal mesa naquele determinado momento. Mas digamos que quiséssemos ter tarefas em paralelo na mesma mesa ao mesmo tempo. Por definição, cada processo tem uma única thread de execução, pense numa thread como sendo uma tarefa sendo executada na mesa. Mas podemos ter multi-thread que seria o equivalente a ter 2 pessoas usando a mesma mesa e os mesmos materiais na mesa ao mesmo tempo. Pra isso acontecer o tal supervisor faz a mesma coisa: ele força uma das pessoas a parar de usar o material da mesa pra deixar a outra pessoa sentada do lado usar o mesmo material.</p>
<p>Multi-thread é mais ou menos isso: duas pessoas começando com o mesmo material ao mesmo tempo. Cada pessoa pode até usar seu próprio material, mas ela tem acesso ao material todo que está na mesa, que na metáfora é o espaço de memória do processo. Um processo pode ter uma ou mais threads. Num processador com um único core, não é possível que as duas pessoas trabalhem exatamente ao mesmo tempo, porque só tem um único espaço na mesa, então a única forma de haver concorrência é o tal supervisor ficar fazendo uma das pessoas pausar o trabalho pra dar um tempo pra outra pessoa continuar o trabalho dela.</p>
<p>Quando falei de um processador moderno como o Intel i3, ela tem 2 cores. Pense em 2 cores como duas mesas. Nesse caso cada thread ou cada pessoa realmente pode trabalhar verdadeiramente em paralelo. Mas nos anos 90 o comum era um processador 486 ou Pentium de um único core. Só servidores tinham processadores com dois ou mais cores. Então você tinha multi-tarefa preemptiva que fica pausando threads pra outra poder rodar, e agora você tem multi-processamento paralelo onde cada thread roda separadamente em um processador separado.</p>
<p>Quando são dois processos independentes, eles podem rodar 100% em paralelo. Se não estiverem usando nada que é compartilhado pelos dois como um arquivo ou outro recurso de hardware como impressora ou rede. Se um deles estiver usando a impressora, obviamente o segundo processo é obrigado a esperar ou fazer outra coisa enquanto espera. Mesma coisa se forem 2 threads dentro do mesmo processo. Seria o equivalente às duas pessoas uma em cada mesa mas onde ambos dependem do mesmo material pra trabalhar. E é nessa situação que multi-threading começa a ficar complicado.</p>
<p>Se ambas as pessoas quiserem escrever no mesmo pedaço compartilhado de papel ao mesmo tempo, imagine a zona que ia ser. Cada linha ou cada palavra de uma pessoa ia ficar misturado com o que a outra pessoa tá querendo escrever por cima. Ou seja, seja lá o que eles gostariam de escrever no papel ia virar um monte de dados misturados. Na realidade você talvez gostaria que a primeira pessoa usasse o papel, e só quando terminasse a segunda pessoa usasse na sequência. Mas como eles estão disputando ao mesmo tempo, pode ser que o inverso aconteça, e como não sabemos quem vai ganhar a disputa de quem começa a usar o papel na frente do outro literalmente temos uma condição de corrida, ou o que se chama de race condition, que pode levar a resultados inesperados ou não-determinísticos.</p>
<p>Então como fazer nesses casos? Pra isso precisamos de um sinalizador. Quando a primeira pessoa quiser escrever, ele coloca uma placa dizendo “ocupado”. Quando a segunda pessoa for escrever também ele vê a placa e espera. Quando a primeira pessoa termina o que queria, ele tira a placa de “ocupado”. Agora a segunda pessoa pode colocar a placa dela e escrever sua parte, e depois tirar a placa. Se ela vai embora e esquece de tirar a placa, aquele pedaço de papel fica bloqueado e ninguém mais consegue escrever nele. Agora pense que a primeira pessoa bloqueia o pedaço de papel, então a segunda pessoa resolve bloquear outro pedaço de papel que a primeira vai precisar na sequência. Mas ambos esquecem de tirar as placas de ocupado de seus respectivos papéis e cada um quer usar o papel do outro. Temos um impasse, onde nenhum sinaliza pro outro que já terminou e ambos param de trabalhar esperando. Eles estão literalmente mortos por causa do bloqueio, ou seja, o que chamamos de dead-lock.</p>
<p>Essa placa de “ocupado” é o que em programação multi-thread chamamos de lock ou mutex, que significa mutuamente exclusivo. É um sinalizador de exclusividade. E como no exemplo, quem bloqueia o recurso também tem a responsabilidade de desbloquear quando acabar. E com isso você consegue ver como duas threads, mesmo se tiver dois cores na CPU onde eles teoricamente poderiam trabalhar independentes em paralelo, se tiver um recurso compartilhado como o tal pedaço de papel, um é obrigado a esperar o outro terminar.</p>
<p>Eu tentei usar metáforas aqui mas você só vai entender realmente o que tudo isso significa o dia que realmente começar a programar usando esses recursos. Entenda alguns fatos primeiro: toda CPU moderna é feita pra rodar threads, é como se fosse a menor unidade de execução. Cada thread executa uma sequência de instruções e tem um contexto, que é seu material de trabalho, que fica nos registradores da CPU. Quando o CPU faz o tal context switching, ele guarda o que está nos registradores naquele momento e deixa outra thread executar. Fazer essa troca de contexto não sai de graça, porque tem o trabalho de tirar o material da mesa e colocar o material do outro na mesa. Então quanto mais threads você tenta executar num core, mais devagar o trabalho geral vai ficar. Então não adianta ficar criando threads como um idiota achando que tudo vai rodar em paralelo e ficar mais rápido, lógico que não. O ideal é ter o mesmo número de threads que número de cores no seu CPU. Por outro lado não é porque você tem 2 cores e inicia 2 threads que ambos vão rodar da forma mais eficiente, porque eles podem estar compartilhando o mesmo recurso e esperando liberar o mutex que o outro colocou. Nesse caso uma terceira thread poderia se aproveitar desse período de espera e executar enquanto isso, caso não precise usar o mesmo recurso. Então você vê como também ter exatamente o mesmo número de threads que cores não é um regra. Em muitos casos determinar qual o número ideal de threads que você pode subir naquela determinada máquina é uma questão de tentativa e erro.</p>
<p>Criar processos é um procedimento que custa pro sistema operacional, não é de graça. Ele precisa carregar o binário do programa, alocar memória do sistema, checar permissões de segurança, etc. Esse procedimento inclusive é diferente entre sistemas operacionais. Em particular, sistemas Linux são mais rápidos pra criar processos do que um MacOS e uma ordem de grandeza mais rápido do que um Windows. Já falei em outro episódio que no Linux muitos programas como o Apache tomam vantagem de fazer o tal FORK que cria uma cópia rápida do processo atual pra gerar outro processo que vai rodar concorrentemente. E fazer forks em Linux é uma operação comparativamente mais barata do que no Windows. Ou seja, só porque ambos fazem a mesma coisa não quer dizer que fazem do mesmo jeito. Qualquer sistema que dependa de forks vai rodar ordens de grandeza pior num Windows.</p>
<p>Por isso mesmo desenvolvimento em Windows privilegia e recomenda criar um único processo por aplicação mas usar mais threads dentro do mesmo processo. Como uma thread compartilha a memória interna do processo com outras threads, a ação de criar threads é ordens de grandeza mais barato do que criar novos processos. Por outro lado isso também é uma troca. Processos são isolados de outros processos e um não vai pisar no calo do outro. Você não tem race conditions, você não precisa manualmente gerenciar mutexes e nem se preocupar com dead-locks, mas processos em Windows são mais caros pra criar. Por outro lado threads são bem mais baratas, só que agora temos o trabalho de nos preocuparmos em ter que manualmente codificar mutexes e tudo mais.</p>
<p>Programação é sempre o ato de se comprometer, você aceita uma vantagem em troca de uma desvantagem. Tudo sempre depende e nunca tem uma resposta certa pra tudo. Alguém poderia dizer que porque threads são mais rápidas de criar e usam menos recursos, eles são obviamente melhores do que fazer forks de processos. Por outro lado alguém pode argumentar que como temos que gerenciar mutexes e coisas assim manualmente, você tem muito mais bugs e situações não-determinísticas com threads e, portanto, processos são infinitamente mais fáceis de programar com menos bugs. Ambos estão certos.</p>
<p>Na prática você vai encontrar mais soluções baseadas em fork de processos em sistemas Linux ou UNIX como no MacOS e mais soluções multi-thread no Windows. Mas isso não é uma regra e em alguns casos, a melhor opção não necessariamente é a mais óbvia. Por exemplo, um navegador web como Firefox ou Chrome. Acho que se você é desenvolvedor já sabe que eles usam recursos de multi-processos. Antigamente um navegador só abrir uma única página de cada vez. Mas aí inventaram a idéia de abas e abrir múltiplos sites diferentes em múltiplas abas. Como um navegador é razoavelmente pesado e consome muita memória pra guardar tantas imagens e javascript que cada página carrega, você quer tentar usar a menor quantidade de memória quanto possível. Então o primeiro instinto de um programador é fazer cada aba ser uma thread.</p>
<p>De fato você vai usar menos recursos, mas à medida que os navegadores vão ficando mais e mais complexos, a quantidade de bugs por causa de threads aumentam exponencialmente. Agora um bug numa thread pode desestabilizar o navegador inteiro porque dentro do processo do navegador, uma thread tem acesso a tudo que as outras threads tem acesso. Então era muito comum você abrir um site qualquer e de repente seu navegador inteiro com 20 abas dava pau e você perdia tudo.</p>
<p>Pra piorar, alguns desses bugs eram bugs de segurança. Imagine uma aba que abre um site conseguir vasculhar a memória da outra aba que está com um e-commerce aberto e com seus dados de cartão de crédito na memória global. Cada vez que você tenta consertar um bug, pode acabar acidentalmente largando um bug novo no lugar. Então é como tentar tampar o sol com a peneira. Some a isso a possibilidade de instalar plugins que ganham acesso à memória do navegador como os plugins de Java ou os antigos de Flash e você tem um desastre esperando pra acontecer.</p>
<p>Por isso muitos navegadores começaram a separar certas partes em processos independentes como o Safari que iniciou separando coisas como Flash e Java em processos mas ainda manteve muita coisa em threads pra usar menos memória. Mas no caso do Google Chrome ele resolveu separar completamente cada aba em seu próprio processo. Por um lado isso trouxe muito mais estabilidade. E você já deve ter visto muitas vezes uma aba inteira crashear, mas o navegador continuar rodando sem nenhum problema. E você pode ver cada processo separado num Task Manager do Windows por exemplo, e inclusive pode matar um processo por lá mesmo, na mão. É a grande vantagem de se separar tudo em processos como eu já falei antes. Por outro lado todo mundo sabe como o Chrome hoje é um dos aplicativos que mais come RAM do seu sistema e essa separação é uma das razões já que código comum entre as diversas abas precisa ser copiada em outro processo a cada nova aba.</p>
<p>Conceitualmente falando, um fork de um processo é como se você pausasse esse processo e fizesse uma cópia de tudo que ele tem na memória pra outro espaço isolado e resumisse os dois, e a partir daí cada um tem vida independente. Se um processo está consumindo 1 MB de RAM, com um fork você deveria estar agora usando 2 MB. Mas no Linux temos um recurso chamado copy-on-write ou CoW, onde o segundo processo não usa praticamente nenhuma memória adicional e fica apontando pra memória do processo original assim que é criado. Se a memória não for modificada, ambos podem reaproveitar o mesmo dado em vez de fazer cópias desnecessárias e só coisas que o segundo processo começa a gravar começa a usar mais memória e fica invisível ao processo original e vice-versa. No Windows o equivalente ao fork realmente duplica a memória, que é outro motivo de porque criar processos no Windows é considerado mais caro.</p>
<p>Um fork com copy-on-write no Linux cada processo tem acesso a uma cópia da memória do processo pai, mas diferente de threads dentro do processo que compartilha tudo, não existe a necessidade de mutexes ou outro tipo de controle de acesso porque a partir do ponto do fork, cada processo tem sua cópia da memória pai e um não escreve em cima da memória do outro. Isso faz as coisas serem muito mais simples de programar. Claro, threads ainda são mais leves e usam muito menos memória no total.</p>
<p>Você pensa no Linux como um sistema estático que é sempre igual, mas ele mudou radicalmente dos anos 90 pros anos 2000 e até agora. A interface que você interage com o sistema, que seria os shells de linha de comando, ou as APIs, são parecidas, muito porque ele segue o que chamamos de padrão POSIX que é sigla de Portable Operating System Interface onde o X no final denota sua herança de UNIX. Teoricamente os UNIX e seus clones como Linux deveriam seguir esse padrão pros programas serem portáveis entre eles, no nível do código fonte, porque os binários não são compatíveis.</p>
<p>No mundo POSIX você tem a abstração de Posix threads ou pthreads implementado como LinuxThreads anos atrás. Threads em Linux eram bugados e nunca foram grande coisa até que finalmente alguns projetos começaram a surgir pra reimplementar o suporte de threads no Linux, como o Next Generation Posix Threads ou NGPT da IBM que eu me lembro que uma característica marcante era ser um mapeamento M pra N, ou seja uma thread em Linux não seria equivalente um pra um com uma thread de verdade.</p>
<p>Mas o projeto que ganhou foi o Native Posix Thread Library ou NPTL de Ulrich Drapper que era mantenedor do glibc e Ingo Molnár que vou citar de novo a seguir. O NPTL é uma implementação um pra um em vez de ser M pra N como o NGPT, portanto era bem mais simples de ser implementado. Esse projeto corrige as falhas do LinuxThreads e só a partir desse ponto dá pra se dizer que threads em Linux ficaram decentes. E isso só é um fato a partir da kernel 2.6 mais ou menos por volta de 2007. Pra ver como as coisas são diferentes entre diferentes Unix, o Solaris usava uma estratégia M pra N até o Solaris 9. O FreeBSD e NetBSD eram M pra N também mas acho que o FreeBSD também ia ou foi pra uma pra um. Parece que um pra um começou a ganhar terreno mas vamos voltar nisso em breve, guarde esse ponto.</p>
<p>Eu já expliquei sobre o tal scheduler na metáfora das mesas sendo o supervisor que força uma pessoa a sair da mesa pra dar lugar pra outra. Existem diferentes estratégias que um scheduler pode tomar. Quando você só tem uma mesa, você é obrigado a deixar uma pessoa trabalhando e mandar as outras esperarem a vez, e o supervisor manda trocar de lugar com o próximo pra ir dando chance das outras trabalharem também. Mas imagine se o supervisor resolve deixar uma pessoa que ainda não está pronta sentar na mesa e ela fica sem fazer nada, enquanto as outras estão esperando.</p>
<p>E se você adicionar uma segunda mesa, parece que fica mais fácil, mas na verdade fica mais difícil. Imagina se você tem três pessoas pra usar as duas mesas. Mesma coisa, você vai rotacionando uma pessoa em cada mesa. Mas imagina o trampo de uma pessoa ficar transportando seu trabalho de uma mesa pra outra se o supervisor não for muito legal e não deixar ficar na mesma mesa. Quando você tem duas mesas, não dá pra deixar seu trabalho no canto da mesa pra voltar pra ela rápido na próxima rodada necessariamente, você pode ter que ficar transportando suas coisas de um lado pro outro, e obviamente isso é mais lento.</p>
<p>E se você não tomar cuidado, e seu scheduler for meio burrinho, ele vai dando vez pra threads que não precisam, ou deixa threads com pouco trabalho ocupando tempo demais na sua vez, ou pior, cortando no meio o trabalho de quem realmente precisa rodar. E por isso no começo dos anos 2000 um Linux era ruim pra coisas como tocar um vídeo. O vídeo ficava engasgando, mesma coisa com áudio. Foi só em 2007 com a Kernel 2.6 que ganhamos o Completely Fair Scheduler ou CFS que implementa um algoritmo melhor, de autoria do Ingo Molnár que já mencionei antes, que se inspirou nas idéias de Con Kolivas. Em resumo esse CFS maximiza o uso da CPU e tenta dar preferência pra programas interativos, como aplicativos desktop, que diferente de processamento intensivo, tende a ficar ocioso por períodos de tempo e de repente precisar de prioridade pra coisas como tocar vídeo.</p>
<p>Cada sistema operacional implementa estratégias diferentes de scheduler, MacOS, Windows, BSDs, Linux e outros priorizam coisas diferentes e usam algoritmos diferentes. O próprio Windows demorou anos pra chegar no nível de granularidade do MacOS que sempre foi considerado melhor principalmente em criação multimídia porque ele não engasgava no gerenciamento de threads. Por isso músicos e DJs sempre preferiram Macs, especialmente em apresentações ao vivo: porque eles sabiam que o áudio não ia engasgar do nada porque o scheduler resolveu do nada dar a vez pra outra thread à toa. Isso acontecia com Windows e com Linux por isso eles eram bem inúteis em situações assim.</p>
<p>De novo, entenda que threads também tem comportamentos diferentes em diferentes sistemas operacionais. Quem saiu na frente na estratégia de threads foram os Macs no começo dos anos 2000 com o OS X. Windows era o segundo melhor, mais ou menos, e foi passado pelos Linux com o CFS só depois de 2007. Ou seja, o Linux moderno que conhecemos hoje só tomou forma de verdade a partir de 2007. Em termos de engasgos você vai notar que o Windows ainda é menos polido que MacOS e também desktop Linux. E esse fator do scheduler antigo foi mais um motivo de porque no mundo Linux processos continuaram sendo importantes.</p>
<p>De 2007, agora vamos voltar quase uma década, em 1999 surgiu um paper que começou a ajudar a mudar a forma como pensamos sobre concorrência no mundo de redes e web. Estamos falando do lendário paper sobre o problema dos C10K de Dan Kegel do antigo site cdrom.com discutindo quando os servidores de FTP da Simtel conseguiam servir até 10 mil conexões simultâneas sobre uma ethernet de 1 gigabit. Começamos a usar esse termo pra discutir como servir grandes quantidades de conexões simultaneamente. 10 mil parece muita coisa, mas um só servidor do Whatsapp hoje serve mais de 2 milhões de conexões simultâneas. O problema é que existem diversas estratégias pra fazer isso.</p>
<p>Eu já mencionei I/O mas agora vou definir um pouco melhor. I/O é qualquer entrada e saída do sistema, arquivos, rede, USB, teclado, monitor. Agora pense nos mainframes antigos que usavam dispositivos de fita, como os do IBM 701 que eu falei que gravavam dados a fantásticos 5.6 quilobytes por segundo. Se ele tivesse que ficar esperando a operação toda acabar o computador não ia fazer mais nada a não ser esperar. Por isso mainframes lendariamente tem milhares de canais paralelos de I/O. Mas o mesmo não acontece no barramento de computadores comuns como o que usamos.</p>
<p>Ou seja, mesmo que você consiga ter paralelismo disponível na sua CPU e sistema operacional, se você só tem um HD, não tem como milhares de threads escreverem ao mesmo tempo. O barramento de I/O vira gargalo. É como se você dividir o trabalho de escrever um livro entre 500 pessoas, onde cada uma escreve uma página. Então você vai acabar o livro no tempo que leva pra escrever uma única página. Porém, se todo mundo tiver que escrever no mesmo livro, não adiantou nada, só dá pra escrever uma página de cada vez.</p>
<p>Lembra quando falei de locks em memória global do processo compartilhado entre múltiplas threads? Algo semelhante acontece quando threads tentam acessar recursos restritos do sistema. É o que chamamos de operações que bloqueiam ou bloqueantes. Então se uma thread resolve escrever alguns gigabytes num arquivo, essa thread fica bloqueada até terminar, nesse caso outra thread pode executar outra coisa que não depende do HD enquanto a primeira fica bloqueada.</p>
<p>Conexões de rede também são bloqueantes, imagine uma thread fazendo download de alguma coisa ou algum cliente de rede pendurado numa thread. Essas operações são bloqueantes e a sequência de instruções da thread vai esperar até a operação terminar e desbloquear o processamento. Com o tempo os sistemas operacionais passaram a implementar algo chamado I/O assíncrono. Ou seja, em vez de bloquear o processamento, a thread pode pedir pra escrever alguma coisa num arquivo, mas o sistema operacional não vai bloquear a thread. Em vez disso a thread pede pra escrever e indica na função do sistema pra quando ele terminar, enviar um evento de término. Daí a thread pode configurar pra quando receber esse evento daí executar alguma outra sequência de instruções.</p>
<p>Ou seja, a programação fica dependente do evento de término da operação de I/O, seja operação de leitura ou de escrita, e é isso que muitos chamam de orientação a eventos. Isso não é nada novo, qualquer linguagem de programação dos anos 90 usada pra fazer aplicativos gráficos sempre usou programação orientada a eventos. Por exemplo, o código que é executado quando você tem o evento de clicar num botão, ou o evento de arrastar uma janela. Num Linux podemos enviar sinais a um processo, que é uma forma rudimentar de comunicação inter processos, como os sinais de kill como SIGTERM ou SIGINT. Aqui estamos estendendo para eventos de I/O do sistema operacional.</p>
<p>Mesmo sem I/O assíncrono podemos atender múltiplas conexões como no problema do C10K fazendo cada conexão ao servidor ser servida por forks do processo servidor. É como a primeira versão de um Apache resolvia isso. Cada nova requisição era gatilho pra um fork do processo principal que gerava um processo filho. Já expliquei que isso é pesado, especialmente em Windows.</p>
<p>A segunda alternativa é cada nova conexão recebida no processo servidor ser servida por uma nova thread dentro desse processo. Também já expliquei como threads são bem mais leves que um processo, mas temos o problema de gerenciar o compartilhamento da memória interna do processo entre múltiplas threads e os bugs que isso pode causar como race conditions e dead locks. Além disso quando estamos falando de 10 mil conexões ou mais, seria necessário mais de 10 mil threads, e eu já expliquei como context switching entre threads não é algo tão simples assim nem barato, você precisa de um scheduler muito bom e mesmo assim cada thread também consome recursos do sistema operacional. Versões subsequentes do Apache e outros servidores web como o IIS no Windows se valeram de threads pra servir milhares de conexões, mas não era o bastante.</p>
<p>A terceira opção como já imaginaram é usar esse tal recurso de I/O assíncrono, que permite que uma única thread ordene múltiplas operações de I/O ao sistema operacional sem que ele fique bloqueado. E o sistema operacional se vira pra realizar essas operações e vai notificando à thread a medida que cada operação vai terminando.</p>
<p>Cada sistema operacional implementa esse sistema de filas de eventos para I/O de formas diferentes. Sistemas derivados de BSD como o FreeBSD ou mesmo o MacOS tem uma syscall chamada kqueue. O Windows usa uma coisa chamada I/O Completion Port ou IOCP. E os Linux tinham syscalls chamada select, depois poll e finalmente a mais usada agora que é a epoll. Assim como gerenciamento de threads e schedulers, cada uma dessas implementações se comportam de formas diferentes. Estão vendo como os sistemas operacionais são mais diferentes do que só uma interface gráfica diferente?</p>
<p>Como resultado de toda a discussão no fim dos anos 90 e começo dos 2000 em cima do paper do C10K de Dan Kegel surgiu um projeto que nasceu com o objetivo de superar o Apache em conseguir servir dezenas de milhares de conexões simultâneas usando o mínimo possível de recursos do sistema, ou seja, sem usar a estratégia de uma conexão por processo ou thread e sim usando uma combinação de processos e chamadas de I/O assíncrono. É assim que nasce o projeto do servidor web mais rápido e escalável que temos até hoje, o NGINX de Igor Sysoev que começou a ser desenvolvido a partir de 2002 com lançamento da versão estável em 2004.</p>
<p>O NGINX tem um processo master que carrega outros processos que ele chama de “workers” e você vai ver que a palavra “worker” é um termo muito usado em arquiteturas com concorrência. Cada worker consegue gerenciar milhares de sockets de conexões de navegadores requisitando páginas web, por exemplo. Em vez de gerar um fork ou uma thread por conexão, ele registra a requisição via epoll num Linux e implementa um loop de eventos. Pense que o programa do worker é um loop que fica fazendo syscalls não bloqueantes de I/O e cada vez que o loop repete ele checa se veio o evento de término e aí executa o que precisa e devolve a página de volta pro requisitante. É uma forma de numa única thread conseguir servir milhares ou dezenas de milhares de conexões simultaneamente.</p>
<p>Não sei se fica claro e isso é importante. Se fosse como era antigamente, e eu fizesse um loop com chamadas de I/O, digamos que ler o arquivo HTML do HD levasse 1 segundo e enviar o HTML pela rede pro navegador levasse 1 segundo - é bem menos que isso na realidade, claro. E agora digamos que esse processo recebesse 10 requisições da mesma página. Levaria pelo menos 20 segundos pra servir todo mundo. Com I/O assíncrono poderia levar pouco mais do que os 2 segundos pra servir as 10 páginas.</p>
<p>Numa metáfora, do jeito antigo é como se no restaurante todo cliente pedisse direto pro chef o prato que quer, ele só consegue atender e fazer um prato de cada vez e cada cliente na fila precisaria esperar o prato do cliente anterior ficar pronto pra pedir o seu. Ou seja, o último cliente da fila precisa esperar todos os clientes anteriores ter o prato pronto antes de poder pedir o seu. Agora adicione um garçom, todos os clientes fazem o pedido pro garçom um atrás do outro e podem se sentar. Agora o último cliente da fila é atendido bem mais rápido. E o garçom vai passando os pedidos pro chef, que daí vai devolvendo os pratos à medida que forem ficando prontos, é como o evento de término que eu falei antes, mas os clientes já todos foram atendidos, fizeram os pedidos e podem ficar sentados fazendo outra coisa. É mais ou menos a diferença entre só ter I/O bloqueantes e ter I/O não-bloqueante. Não fica todo mundo bloqueado esperando em sequência.</p>
<p>Voltando ao NGINX, como cada worker é uma única thread, o master sobe na média um worker por core disponível na CPU então numa CPU quad-core subiríamos 4 workers. Cada worker com um loop de eventos que fica esperando os eventos de término de cada requisição de I/O que ele recebe, com capacidade de servir dezenas de milhares de conexões simultaneamente. E como ele não precisa criar uma thread por conexão, ele não perde tempo nem recurso fazendo context switching. Então um NGINX num bom servidor consegue servir até milhões de conexões simultaneamente, especialmente se for servir coisas simples como ler uma página HTML estática e só devolver sem processar lógica nenhuma. Se você vem de Javascript talvez já tenha ouvido falar desse pattern de arquitetura, isso se chama um Reactor, porque dentro do loop ele reage aos eventos de I/O que vão terminando.</p>
<p>Na mesma época, por volta de 2002 outro framework também implementou o mesmo pattern de Reactor com um event loop e usando I/O assíncrono, Node.js? Nope, ainda não. Esse foi o framework Twisted implementado em Python em 2002. Assim além de servir páginas HTML muito rápido, a cada loop do Reactor você podia executar processamento escrito em Python. Twisted introduziu ainda outro conceito: os Deferreds ou Futures. Mas vou explicar isso no próximo episódio.</p>
<p>Nesta primeira parte eu queria deixar bem explícito conceitos que acho que a maioria dos programadores tem problemas em entender logo no começo: concorrência e paralelismo e como os sistemas foram evoluindo pra resolver esse problema. Aliás, eu não expliquei isso antes mas vamos definir agora: concorrência e paralelismo. Simplificando, lembram dos computadores até os anos 90? Você podia ter tarefas que pausam e deixam outras executarem no seu lugar e ficam trocando de lugar, fazendo o tal context switching. Elas são concorrentes mas não são paralelas. Quando você tem dois ou mais cores na CPU aí sim, você realmente tem paralelismo, porque duas tarefas concorrentes, por exemplo threads, podem ser executadas verdadeiramente em paralelo. Seja em uma CPU com capacidade pra somente uma thread ou uma CPU com capacidade pra múltiplas threads, ainda assim você precisa de um scheduler, porque seu programa pode modelar e gerenciar mais threads concorrentes do que existem cores físicas pra executá-las.</p>
<p>É importante também que todos entendam como os sistemas operacionais são muito diferentes especialmente nesses aspectos de concorrência, como implementam threads de maneiras diferentes, como existem diferentes schedulers e no Linux você pode inclusive escolher schedulers diferentes pra casos diferentes. E como I/O assíncrono é outro recurso implementado de formas diferentes. Sabendo desses conceitos no próximo episódio vamos falar de coisas mais abstratas como corotinas, fibers e o conceito de deferreds ou futures.</p>
<p>Esse assunto é bem cabeludo, concorrência e paralelismo não tem uma resposta única que resolve todos os problemas. Cada tipo de problema vai precisar de uma solução diferente. Forks de processos funcionam. Multi-threads funcionam. I/O assíncrono é só mais uma opção nessa caixa de ferramentas e ainda vamos explorar outras opções. Além disso no mundo Linux o suporte a threads só ficou realmente bom a partir de 2007. Por isso você vê tanta coisa interessante no mundo de concorrência dando as caras principalmente na década de 2010. Muitos acham que event loops, o pattern de Reactor nasce com Node.js a partir de 2009, mas não, NGINX e Twisted pelo menos, já implementavam isso em 2002, derivados diretamente do objetivo de resolver o problema C10K de Dan Kegel de 1999 e esses conceitos mesmo já vinham pesquisas de anos antes.</p>
<p>Se tiverem dúvidas não deixem de mandar nos comentários abaixo, se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, assinem o canal e não deixem de clicar no sininho pra não perderem os próximos episódios. A gente se vê semana que vem, até mais.</p>
<p>Links:</p>
<ul>
<li>The Linux Scheduler: a Decade of Wasted Cores (<a href="https://blog.acolyer.org/2016/04/26/the-linux-scheduler-a-decade-of-wasted-cores/"target="_blank" rel="noopener">https://blog.acolyer.org/2016/04/26/the-linux-scheduler-a-decade-of-wasted-cores/</a>)</li>
<li>The C10K problem (<a href="http://www.kegel.com/c10k.html"target="_blank" rel="noopener">http://www.kegel.com/c10k.html</a>)</li>
</ul>
]]></content:encoded><category>sistemas-distribuidos</category><category>backend</category><category>performance</category><category>akitando</category></item><item><title>[Akitando] #41 - Entendendo Back-End para Iniciantes em Programação (Parte 2) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/02/27/akitando-41-entendendo-back-end-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/02/27/akitando-41-entendendo-back-end-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</guid><pubDate>Wed, 27 Feb 2019 17:00:00 -0300</pubDate><description>&lt;p&gt;Disclaimer: esta série de posts são transcripts diretos dos scripts usados em cada video do canal &lt;a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener"&gt;Akitando&lt;/a&gt;. O texto tem erros de português mas é porque estou apenas publicando exatamente como foi usado pra gravar o video, perdoem os errinhos.&lt;/p&gt;
&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/N6vgZr1k03g"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h3&gt;Descrição no YouTube&lt;span class="hx:absolute hx:-mt-20" id="descrição-no-youtube"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o-no-youtube" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Esta é a Parte 2 do tema de Back-end para Iniciantes. Desta vez vamos olhar com mais cuidado sobre gerenciamento de dependências, o que diabos são dependências, e linguagens como Groovy, Scala, Elixir, Go e um pouco de Ruby e Ruby on Rails também.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Disclaimer: esta série de posts são transcripts diretos dos scripts usados em cada video do canal <a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener">Akitando</a>. O texto tem erros de português mas é porque estou apenas publicando exatamente como foi usado pra gravar o video, perdoem os errinhos.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/N6vgZr1k03g"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h3>Descrição no YouTube<span class="hx:absolute hx:-mt-20" id="descrição-no-youtube"></span>
    <a href="#descri%c3%a7%c3%a3o-no-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esta é a Parte 2 do tema de Back-end para Iniciantes. Desta vez vamos olhar com mais cuidado sobre gerenciamento de dependências, o que diabos são dependências, e linguagens como Groovy, Scala, Elixir, Go e um pouco de Ruby e Ruby on Rails também.</p>
<p>Provavelmente este vai ser o episódio mais difícil deste canal até agora, se você é iniciante, e você vai precisar ver e rever várias vezes, mas se você está levando a sério se tornar um programador, o que vou dizer hoje é de extrema importância pra você entender e estudar bastante.</p>
<p>Além de tudo este é o episódio mais longo deste canal até hoje. Nesta mini-série de &ldquo;Começando aos 40&rdquo; eu estou resumindo e dando o contexto geral do que você aprenderia (ou não) em meses de faculdade e trabalhando na prática. Aproveitem pra entender a idéia geral e puxar todos os assuntos para estudar em mais detalhes sozinho. Mandem suas dúvidas nos comentários também! E não deixem de compartilhar com seus amigos.</p>
<p>Akitando:</p>
<ul>
<li>Playlist de Programação (<a href="https://www.youtube.com/watch?v=WObC_2e0kZk&amp;list=PLdsnXVqbHDUcrE56CH8sXaPF3TTqoBP2z"target="_blank" rel="noopener">https://www.youtube.com/watch?v=WObC_2e0kZk&list=PLdsnXVqbHDUcrE56CH8sXaPF3TTqoBP2z</a>)</li>
<li>Playlist Começando aos 40 (<a href="https://www.youtube.com/watch?v=O76ZfAIEukE&amp;list=PLdsnXVqbHDUc7htGFobbZoNen3r_wm3ki"target="_blank" rel="noopener">https://www.youtube.com/watch?v=O76ZfAIEukE&list=PLdsnXVqbHDUc7htGFobbZoNen3r_wm3ki</a>)</li>
<li>Sua Linguagem NÃO é Especial - Parte 1 (<a href="https://www.youtube.com/watch?v=p9-WuJbVHHc"target="_blank" rel="noopener">https://www.youtube.com/watch?v=p9-WuJbVHHc</a>)</li>
<li>Sua Linguagem NÃO é Especial - Parte 2 (<a href="https://www.youtube.com/watch?v=XcTTajFENHI"target="_blank" rel="noopener">https://www.youtube.com/watch?v=XcTTajFENHI</a>)</li>
</ul>
<p>Links:</p>
<ul>
<li>So, What&rsquo;s wrong with SBT? (<a href="http://www.lihaoyi.com/post/SowhatswrongwithSBT.html"target="_blank" rel="noopener">http://www.lihaoyi.com/post/SowhatswrongwithSBT.html</a>)</li>
<li>Reia (<a href="http://reia-lang.org/"target="_blank" rel="noopener">http://reia-lang.org/</a>)</li>
<li>Go is not Good (<a href="https://github.com/ksimka/go-is-not-good"target="_blank" rel="noopener">https://github.com/ksimka/go-is-not-good</a>)</li>
<li>Why Go is a Poorly Designed Language (<a href="https://medium.com/@tucnak/why-go-is-a-poorly-designed-language-1cc04e5daf2"target="_blank" rel="noopener">https://medium.com/@tucnak/why-go-is-a-poorly-designed-language-1cc04e5daf2</a>)</li>
<li>Why Go is not Good (<a href="http://yager.io/programming/go.html"target="_blank" rel="noopener">http://yager.io/programming/go.html</a>)</li>
</ul>
<p>Me sigam também em outras redes sociais:</p>
<ul>
<li>Twitter (<a href="https://twitter.com/akitaonrails"target="_blank" rel="noopener">https://twitter.com/akitaonrails</a>)</li>
<li>Instagram (<a href="https://instagram.com/akitaonrails"target="_blank" rel="noopener">https://instagram.com/akitaonrails</a>)</li>
<li>Facebook (<a href="https://facebook.com/akitaonrails"target="_blank" rel="noopener">https://facebook.com/akitaonrails</a>)</li>
<li>Quora (<a href="https://www.quora.com/profile/Fabio-Akita"target="_blank" rel="noopener">https://www.quora.com/profile/Fabio-Akita</a>)</li>
<li>Blog (<a href="https://www.akitaonrails.com"target="_blank" rel="noopener">https://www.akitaonrails.com</a>)</li>
</ul>
<h2>Script<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Este já é sexto episódio da série Começando aos 40. O ideal é que vocês assistam todos os outros vídeos da série, e se possível os da playlist de programação que tem a rápida história das linguagens nos vídeos de Sua Linguagem Não é Especial. Eu vou deixar linkado nas descrições abaixo. Continuo bem contente com a repercussão dos últimos vídeos, parece que vocês estão gostando do formato então vou continuar assim até o fim desta série. Hoje por acaso estou sentindo minha garganta dando pra trás, é possível que minha voz comece a falhar até o fim do vídeo, mas vamos lá.</p>
<p>No episódio da semana passada eu expliquei os conceitos mais básicos da computação, sobre instruções de máquina, compilação, interpretadores, como os binários se linkam com suas dependências, lembra? Linkagem estática e dinâmica? Começamos a falar um pouco sobre Java e .NET e máquinas virtuais como JVM. Agora com os conceitos entendidos vamos entender porque eu disse que era importante entender tudo isso antes do que vou falar hoje.</p>
<p>Uma das coisas mais importantes pra um programador iniciante entender é que você nunca vai fazer softwares de verdade totalmente do zero, ou seja, que 100% do código foi só você que escreveu. Você vai ser obrigado a usar bibliotecas dos outros. Vamos expandir o conceito de bibliotecas dinâmicas e entender quão fundo vai o buraco desse coelho.</p>
<p>(&hellip;)</p>
<p>Até esse ponto na história você tinha então linguagens estilo Java, que é muito inspirado em C++ e alguns pontos de Smalltalk com alguns, mas não todos os conceitos de orientação a objetos e o conceito de máquina virtual e gerenciador de memória que também havia em Lisp. Na prática, em vez de orientação a objetos eu argumentaria que Java é uma linguagem mais orientada a classes. Muita coisa em Java não são objetos, começando com a própria classe em si. A orientação a objetos veio evoluindo desde Simula, CLU, C com Classes que originou C++, Smalltalk que foi provavelmente a melhor representação de orientação a objetos, e Java que deu alguns passos pra trás.</p>
<p>Até esse ponto já entendemos o conceito de bibliotecas dinâmicas que podem ser reusada. Bibliotecas em geral são conjuntos de funções que fazem alguma coisa útil como processar imagens, converter datas, formatar PDFs ou qualquer coisa assim que possa ser reusada. Componentes são funcionalidades, como um componente de calendário que tem a parte visual e a programação que controla as datas. Uma biblioteca pode ter vários componentes, por exemplo. A parte visual começou a ficar importante com o advento das interfaces gráficas, com a crescente adoção de ambientes gráficos como o Macintosh original, o OS/2 que foi um filho bastardo da IBM com a Microsoft e finalmente o Windows 3.1. Por acaso modelar componentes como classes e objetos era interessante e foi quando esses dois conceitos começaram a se mesclar.</p>
<p>No começo dos anos 90 um dos conceitos ligados a orientação a objetos começou a aparecer mais que foi a componentização. Em particular na forma de RAD ou Rapid Application Development, Na categoria de RAD foi onde se popularizou o conceito de Integrated Development Environment ou IDEs como os antigos PowerBuilder, Visual Basic, Visual dBase, Access, Delphi. As linguagens por baixo ainda não eram realmente orientadas a objeto como conhecemos mas aí surgiu o Java que trouxe uma evolução. Ainda longe do que era o Smalltalk mas já melhor do que Visual Basic.</p>
<p>Componentização também se tornou uma idéia importante primeiro para evitar reescrever software do zero toda vez e tornar mais fácil reusar software. Mas mais do que isso pra tornar mais fácil comercializar esses componentes. Hoje em dia se você quer uma biblioteca pra, sei lá, imprimir um PDF tem centenas disponíveis de graça no GitHub pra quase todas as linguagens. Mas naquela época a Internet só tava começando, nem 2% da população tinha acesso ainda. E o jeito pra se comercializar era distribuindo em disquetes, encaixotado, em prateleira de lojas. Pra começar, agora que tínhamos um ambiente gráfico mais sofisticado nos sistemas operacionais não dava pra todo programador toda hora ter que fazer tudo do zero. Desenhar um botão fazendo as linhas da borda pixel a pixel. Por isso linguagens como o Microsoft C++ já vem com um framework, o MFC ou Microsoft Foundation Classes e o Delphi vem como VCL ou Visual Components Library. Normalmente são conjuntos de DLLs como já expliquei antes.</p>
<p>Com C++ e MFC podemos criar aplicativos como um Word ou Excel. Mas daí a Microsoft foi além e criou outra tecnologia, o OLE ou Object Linking and Embedding que em resumo é uma forma de incluir componentes visuais complexos dinamicamente num aplicativo. Vocês já sabem o que são bibliotecas dinâmicas, mas do jeito como explicamos você precisa compilar o programa já com os bindings pras bibliotecas em tempo de compilação. Mas como fazer pra instalar um componente num programa que já existe sem recompilar? Uma demonstração impressionante na época era conseguir fazer uma tabela do Excel ser embutido dentro de um documento do Word e ser editável. Em cima do OLE surgiu o OLE control extensions ou OCX. Se você já instalou componentes num Access ou Delphi deve ter visto bibliotecas com extensão OCX que se tornou a principal forma de distribuir componentes em Windows.</p>
<p>Isso foi uma pequena revolução porque também permitia comercializar componentes num formato padrão que podia ser escrito em qualquer linguagem da época e embutida em qualquer aplicativo que suportasse o padrão, incluindo IDEs. Do OLE original pra OCX também se precisou de um modelo para linguagens interagirem com esses componentes. Então nasce o Component Object Model ou COM. É assim que componentização funcionou na época do Visual Basic e Delphi. No mundo Java o equivalente ao COM poderia ser um Java Bean. Hoje OCX é outra coisa, são componentes ActiveX. Assim como no Java toda classe herda de Object ou em Object Pascal toda classe herda de TObject, em COM você implementa a interface IUnknown.</p>
<p>Aqui vale entender a função de uma interface. Numa interface gráfica, tanto faz se você tem uma janela com um botão de OK em Windows, ou em MacOS, ou em GNOME do Linux, a interface é a mesma do ponto de vista do usuário, mas a programação, ou implementação por trás é completamente diferente. A interface garante que quem vai consumir o serviço por trás não precisa se preocupar como ele é construído. A mesma coisa são as interfaces para bibliotecas, nesse caso em vez de ser um elemento visual, se trata de um arquivo em um determinado formato que simplesmente declara que funções, com que argumentos, com que tipos de dados, e que tipo de resultados você espera. Daí você implementa como quiser por trás e quem consome essa biblioteca pode assumir que no mínimo ele vai implementar as funções conforme descritas na interface. Uma classe numa linguagem orientada a objetos é a implementação de alguma interface, como a tal IUnknown do padrão COM que falei antes.</p>
<p>Poderíamos argumentar que o modelo de componentes mais avançado na época, no começo dos anos 90, era a NextStep com sua linguagem Objective C que eu argumentaria que é mais simples, mais elegante e mais orientado a objetos do que C++ ou Object Pascal ou mesmo Java e tinha um conjunto de classes, que eles chamavam de kits como o Foundation Kit, melhor do que o MFC da Microsoft ou VCL da Borland. Pra vocês terem uma idéia de quão à frente do seu tempo ela estava, o desenvolvimento de iOS e MacOS de hoje ainda usa uma evolução direta das mesmas classes até hoje, por isso os nomes das classes como “NSString” ou “NSObject” começam com esse NS que é de NextStep. No fim dos anos 80 e começo dos anos 90, essa idéia de arrastar e soltar componentes direto numa janela gráfica pra criar programas era uma idéia revolucionária. No NextStep que surgiu o lendário primeiro navegador web de Tim Berners Lee, mas não só isso, pra nerds como eu é a Id Software desenvolveu a engine gráfica do jogo Doom. O também lendário John Carmack gostava dos NextCube.</p>
<p>A primeira geração de programas nessa época no fim dos anos 80 eram aplicativos desktop normais, como os feitos em PowerBuilder ou Access que rodavam só localmente em uma máquina. Esses programas normalmente tinham um binário da aplicação e outro arquivo que representava um banco de dados, muitos derivados do primitivo formato DBF (que literalmente significa database file) que usávamos em dBase ou Clipper ou FoxPro. O que a gente chamava de banco de dados era uma estrutura de dados muito simples, pense uma coisa parecida com uma lista ligada de estruturas e um índice primitivo. Numa época que empresas pequenas às vezes tinha só 1 computador pra empresa inteira, isso até funcionava. O banco inteiro era um único arquivo. E se dois usuários em duas máquinas quisessem compartilhar o mesmo banco de dados sem precisar ficar copiando o arquivo toda hora?</p>
<p>Com a popularização dos desktops, principalmente PCs e também com acesso a tecnologias de redes locais, antes ainda da Internet comercial realmente decolar, começa a surgir o conceito de aplicativos distribuídos. Foi com a popularização de tecnologias de redes como da Novell e do protocolo IPX/SPX e o Windows NetBios que permitiam compartilhar arquivos numa rede local que as coisas começaram a mudar. Com essa possibilidade, e se colocássemos o tal arquivo de banco de dados numa pasta compartilhada na rede, daí copiamos o programa na máquina de cada pessoa na rede e quando o programa abre ele pode apontar pro arquivo compartilhado e booom agora todo mundo pode acessar os mesmos dados! Sensacional né? Só que não &hellip;    Puta idéia errada.</p>
<p>Lembram no episódio passado que falei da dor de cabeça que é programar multi-threads porque cada thread tem acesso à memória global e uma pode ficar pisando na memória da outra? Mesma coisa com programas acessando um arquivo compartilhado na rede. ?O que acontece quando um programa tenta escrever no mesmo registro, no mesmo arquivo, que outro programa na rede? Caos!   Foi a era dos arquivos de banco de dados que corrompiam toda hora.</p>
<p>Essas linguagens primárias e as redes da época só tinham uma coisa pra fazer nessa época: bloquear o arquivo inteiro! Ou seja, se alguém decidisse gravar alguma coisa no banco de dados, todos os outros programas precisavam esperar a vez. Não era ideal mas em redes pequenas de 10 ou 20 máquinas não chegava a ser o fim do mundo. Mas o problema é que um arquivo compartilhado não é mágica. Tem um servidor onde fica o arquivo de verdade, na rede as pessoas não tem a cópia inteira do arquivo, ele vai lendo pedaços do arquivo em um stream de dados.</p>
<p>Aliás, esse conceito de stream é importante você entender se ainda não estudou. Se você tem um arquivo de, digamos, 100 GB na rede, e você quer só um pedaço no meio dele, você não precisa baixar o arquivo todo. Você abre um stream, tipo uma corrente ou esteira mesmo e vai puxando um pedaço de cada vez até chegar na parte que quer e aí pode fechar esse stream. Ou você pode abrir o stream direto já no meio do arquivo e ir lendo de lá um pedaço de cada vez. Quem é bem iniciante se confunde com isso porque você consegue listar os arquivos na rede como se estivesse local na sua máquina, mas “ver” os arquivos e “ter” os arquivos são coisas diferentes.</p>
<p>Mas se você tentar ler esse arquivo gigante inteiro, dependendo da velocidade da rede, que é sempre lento, pode levar muitos minutos. E na hora de gravar o servidor de rede precisava não errar onde ia gravar no meio do arquivo. ?E se seu programa dá pau na rede no meio de uma gravação? Ou se a rede dá pau? Ou se o servidor engasga? De repente a gente começou a ter um monte de variáveis a mais pra se preocupar onde antes era simplesmente abrir o arquivo, gravar e fechar.</p>
<p>Durante os anos 90 fomos evoluindo esse conceito de 2 camadas ou cliente-servidor primitivo. O próximo passo foi usar um servidor de banco de dados em vez de só compartilhar um arquivo na rede. Foi assim que começamos a usar bancos como Ingres, Caché, InterBase, IBM DB/2, Oracle e só depois que a Microsoft adquire a Sybase no meio dos anos 90 que ele constrói o SQL Server sobre ele.</p>
<p>A diferença com arquivos compartilhados é que em vez do programa cliente tentar abrir um arquivo na rede ele se conecta antes a outro programa que está remoto, um servidor. Nesse caso um servidor de banco de dados. Isso evita quase totalmente que o arquivo de dados em si se corrompa tão fácil. Esse servidor vai servir como um mediador, os clientes pedem a ele pra fazer operações no arquivo e o mediador vai executando na ordem correta as coisas, e como só ele abre e escreve no arquivo, as chances dele corromper eram muito menores.</p>
<p>Lembra no episódio sobre a história do front-end como eu expliquei que um navegador web é um cliente TCP e o servidor web é um servidor TCP? É a mesma coisa aqui, só que antes de começarmos a usar o protocolo TCP antes usávamos protocolos de rede local como o IPX que falei antes, então o programa em Delphi ou FoxPro podia ser um cliente IPX e o banco de dados era um servidor IPX. Pouco tempo depois que a Internet começou a se popularizar e o Windows finalmente começou a vir com uma pilha TCP/IP que começamos a trocar tudo por TCP.</p>
<p>Uma pequena evolução nessa arquitetura foi com bancos como Oracle ou SQL Server que permitem escrever programas que rodam dentro do servidor de banco de dados, os famigerados Stored Procedures em linguagens como PL/SQL ou T/SQL. Assim começamos a separar parte da lógica que ficava exclusivamente nos programas cliente para o lado do servidor. Isso ajuda em muito a diminuir a complexidade dos programas cliente, diminui a necessidade de ficar atualizando os programas nos desktops toda vez que a lógica de negócios muda. Mas por outro lado causa mais lentidão no banco de dados porque agora além de ter que fazer as operações básicas de ler e escrever dados ele vai precisar processar e computar cálculos e outras lógicas em cima desses dados.</p>
<p>De novo, lembre a dificuldade que era atualizar o programa cliente nos desktops. Você precisava gerar um novo binário e mandar todo mundo desligar tudo das máquinas, ir de máquina em máquina com um disquete pra instalar ou mandar todo mundo baixar do servidor de arquivos na rede. Pior ainda se a empresa tinha filiais em lugares geograficamente separados onde nem todas as filiais se comunicavam com as outras. Lembre-se, internet ainda era uma coisa opcional. A gente tinha redes locais dentro do escritório, mas não necessariamente internet entre os diferentes escritórios. ?E se uma filial começa a rodar o programa mais novo mas outra filial esquece e fica na versão mais velha?</p>
<p>Agora, alguém parou pra pensar: o banco de dados é um servidor de rede. Os programas são clientes que se conectam nesse servidor. Só que o banco faz duas funções: gerencia os dados e roda lógica de negócio com essas stored procedures. Porque não mais um servidor no MEIO do caminho? Daí o cliente se conecta nesse que tem lógica de processamento de dados primeiro, que por sua vez vai se conectar em um segundo servidor, que seria o de banco de dados. Dessa forma conseguimos tanto separar a lógica de negócio e diminuir o tamanho dos programas nos desktops e também tiramos a carga do servidor de banco de dados. Esse é o nascimento da arquitetura que conhecemos como três camadas ou three tiers. Se você assistiu o episódio sobre SAP é aqui que as soluções corporativas começam a se encaixar.</p>
<p>Com a idéia de um servidor de aplicação passamos a pensar mais em interoperabilidade e protocolos de rede. Junte a idéia de componentização, orientação a objetos e a necessidade de componentizar agora um conceito mais abstrato: chamar lógica de negócios em outro servidor. Assim nascem especificações como o COM e Distributed COM ou DCOM que depois seria renomeado como COM+ da Microsoft ou concorrentes como o famigerado CORBA. Na prática eram todas formas de chamadas de procedimento remotos ou o que chamamos hoje de RFC ou RPC, que é remote procedure calls.</p>
<p>Eu já expliquei que pra compilar um programa que depende de uma biblioteca dinâmica precisamos de um arquivo de cabeçalho que declara que funções essa biblioteca tem exposta para ser consumida, a tal da Interface. É exatamente a mesma coisa agora só que em vez de uma biblioteca dinâmica é uma biblioteca remota, mais precisamente um cliente de algum serviço remoto. Não temos como saber o que um servidor de aplicações tem de funções expostas então precisamos de algum tipo de interface que declara quaisfunções, com que argumentos, e que resultados esse servidor de aplicações expõe pra gente, através de que protocolo. Pra isso servem coisas como COM+ ou CORBA que tinham o protocolo IIOP ou Inter-ORB onde ORB significa object request broker, para especificar esse protocolo de comunicação, o formato das mensagens trocadas.</p>
<p>Esse conceito coincide com a fase do Java onde começa a surgir o famigerado J2EE ou Java 2 Enterprise Edition que hoje é chamado só de JEE, sem o dois. Java 2 é engraçado porque tivemos o Java 1, daí ele foi pro Java 1.1 e depois 1.2 e aí nasceu J2EE mas depois as versões foram 1.3, depois 1.4 e acho que só na 1.5 que ele chamou de direto de versão 5. Hoje acho que estamos no Java 10 ou 11 certo? Enfim, o J2EE era uma especificação realmente interessante porque ela consolidava tudo que tínhamos inventado até então em tecnologias de desenvolvimento de aplicações em três camadas e agora também usando tecnologias de internet como o protocolo TCP e HTTP.</p>
<p>Em resumo J2EE especifica o também famigerado servlet que é uma forma de encapsular as operações de HTTP, ele especifica uma forma complexa de gerar aplicações usando um modelo chamado Model 2 que é basicamente o Model - View - Controller ou MVC que você tanto ouve falar hoje. E na parte do model, que costuma ser uma abstração pra uma ou mais tabelas no banco de dados, temos os também famigerados EJB ou Enterprise Java Beans, além dos JMS ou Java Message Service, que encapsulam maneiras de acessar banco de dados remotos e outros tipos de serviços remotos como outros servidores de aplicação. E tudo isso conectado via arquivos de configuração. E nessa época se tornou moda formatar arquivos de configuração no novíssimo formato XML. Em breve a Microsoft ainda vai inventar Web Services usando o protocolo SOAP que trafega os dados todos formatados em XML também. Na verdade começamos a usar XML em tudo que é lugar. E assim começa a era onde passamos a associar tudo que tem XML com muito enterprise e passamos a odiar XML.</p>
<p>Em paralelo a isso, muita nova literatura começou a ser escrita pra ajudar a organizar toda essaa bagunça em metodologias e vocabulários pra conseguir desenhar sistemas complexos orientados a objetos. É quando surgem nomes com Os Tres Amigos, Grady Booch, James Raumbaugh, Ivar Jacobson, cada um com suas idéias de metodologias que se juntaram numa empresa chamada Rational e juntam suas idéias no famigerado Unified Modeling Language ou UML para representar soluções baseadas nessa idéias de programar objetos e componentes. Aliás, UML que podia ser representado em arquivos em que formato? Sim, em XML. A Rational que viria a desenvolver diversas ferramentas como Rose, ClearCase, ClearQuest, e outros. Depois teríamos a famosa gangue dos quatro ou Gang of Four que são Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides que escreveram o agora clássico livro de Design Patterns. O que chamamos hoje de mundo enterprise começa a ganhar forma nessa fase do fim dos anos 90, e ele começa de verdade com J2EE e as metodologias monumentais de engenharia de software orientados a objetos.</p>
<p>Em uma década evoluímos muito rápido de pequenos programas que rodavam só em uma máquina, pra soluções cliente-servidor em 2 camadas e agora a arquitetura em 3 camadas e as sementes para aplicações realmente distribuídas com o advento da Internet. Em paralelo, como já contei antes, tivemos C# com a origem num híbrido de J++ e Object Pascal. Como C# é basicamente a Microsoft que controla e basicamente depende só do Windows, ele conseguiu evoluir bem mais rápido do que Java nos anos seguinte, adicionando muito mais funcionalidades à linguagem que a tornaram mais do que simplesmente uma cópia da sintaxe do Java, tanto em termo de novas sintaxes quanto mais bibliotecas e serviços. COM+ e ActiveX se tornam as formas principais de componentização tanto local quanto remota e CORBA vai começando a desaparecer.</p>
<p>No mundo mais próximo de Linux, Perl já era popular principalmente como linguagem para scriptar coisas como ferramentas para administração de sistemas Linux. Ele veio pouco a pouco sendo substituído por Python, que era uma linguagem mais limpa e mais moderna, que também herdou algumas funcionalidades de orientação a objetos. Ele teve a sorte de pegar o crescimento das distribuições Linux e acabou se tornando a linguagem padrão em muitas distribuições. PHP pegou a onda das aplicações Web e ganhou espaço sendo mais fácil que Perl como sintaxe de linguagem. PHP se uniu a outras tecnologias emergentes no mundo open source como rodar em sistemas Linux, usar o servidor web Apache, e o ainda novato banco de dados MySQL, e assim nasce a pilha de tecnologias ou stack chamado de LAMP.</p>
<p>Mas o interpretador do PHP era realmente um puxadinho, bem ruim, sendo um dos piores em gerenciamento de memória e constantes bugs de segurança, numa época onde ainda estávamos aprendendo sobre segurança agora que começamos a colocar as coisas expostas na internet pública. Fora isso o estilo de programação derivado da filosofia de Larry Wall, criador do Perl, de “quick and dirty” ou “rápido pra desenvolver mas código sujo como resultado” não era atraente pra adoção mais séria da indústria. Mas a bolha da internet acelerou a adoção de todos eles ao mesmo tempo.</p>
<p>Com o crash da bolha demorou alguns anos pras pessoas voltarem a se interessar em novas linguagens. Como eu disse antes, o Manifesto Ágil só foi escrito bem no ano desse crash em 2001. Então o mercado de linguagens que já existiam não tinham muito interesse em adoção em massa de práticas como as ensinadas por metodologias como Extreme Programming ou Cristal. As metodologias num mundo como Java ou C# era ditado pelos fabricantes de ferramentas como a Rational que foi comprada pela IBM, BEA, Oracle, Microsoft. Google estava crescendo rápido mas ainda não tinha tanta influência nesse mercado. Facebook ainda não existia. O mercado passou a associar tecnologias enterprise como “sérias” e tecnologias open source como “amadoras”.</p>
<p>Mas a gente sabia que tinha jeitos mais simples de desenvolver, principalmente se olhar as linguagens interpretadas, mas ainda não havia uma adoção significativa a não ser do PHP, que pra muitos engenheiros era uma linguagem amadora e pouco sofisticada. Foi só quando Ruby começou a ser adotada por muitos dos signatários do Manifesto Ágil como Ward Cunningham, Martin Fowler, Andrew Hunt, Brian Marick, Robert C. Martin, Dave Thomas que as pessoas começaram a mostrar algum interesse e coincidentemente tivemos o lançamento do framework Ruby on Rails em 2004 e finalmente Ruby, que havia sido criado 10 anos antes em 1995 começou a ganhar tração. Diferente de Java ou C# ou PHP, em vez de ter origem na herança de sintaxe do C++ ele procurou inspiração no Smalltalk e Lisp. Isso a tornou diferente o suficiente e com o suficiente de sofisticação para atrair a atenção desses veteranos de cabelo branco.</p>
<p>Já que era muito difícil parar a inércia de gigantes como Java, .NET e PHP, pareceu interessante começar uma comunidade do zero, com uma linguagem sofisticada mas obscura, mas já aplicando as práticas evangelizadas pelas metodologias que seguiam o Manifesto Ágil. Essa Era marca o início da profissionalização das tecnologias open source para desenvolvimento de aplicações, que poucos anos antes eram consideradas mais amadoras pelo mundo enterprise. Em vez de convencer o mundo enterprise, um novo mundo começou a crescer rápido para competir com elas: as tech startups.</p>
<p>O sucesso quase instantâneo do Ruby on Rails para criar os tipos de aplicações que pouco tempo antes só deveriam ser feitos em Java ou C# trouxe uma nova idéia pra indústria: que pra uma linguagem fazer sucesso ela não precisava ser um clone da sintaxe de C++ ou Java ou ter o suporte de grandes nomes como IBM ou Microsoft. Nesse ponto estamos entre 2003 e 2007. Eu diria que esse período de transição é importante porque nos anos 80 surgiu de fato o movimento open source e GNU. Nos anos 90 surgiu o primeiro grande projeto que tornou open source relevante: a kernel do Linux e as distribuições de Linux.</p>
<p>Durante a bolha das ponto coms tivemos sucessos comerciais como o VA Linux e a RedHat. De repente open source não era só um hobby e tinha modelos de negócio viáveis que finalmente botaram pressão nas empresas tradicionais. Mas a manutenção e criação de novos projetos não era ainda uma coisa simples, haviam sites como SourceForge, CodeHaus e outros, mas contribuir em open source ainda tinha uma barreira de entrada muito alta. Lembre-se que GitHub só se torna realmente famoso a partir de 2008.</p>
<p>Em 2003 mesmo outra iniciativa estava começando com o surgimento do Groovy, uma nova linguagem que compila em bytecode pra JVM. Ela tem muita inspiração na sintaxe de linguagens como Python e Ruby e é uma linguagem muito mais agradável de trabalhar do que o Java original. E com o sucesso do Ruby on Rails, tivemos o Groovy on Grails. Mas nesse momento eu acho que a comunidade Java e mesmo a open source ainda não estava preparada pra se adaptar a uma nova linguagem. Um dos problemas de ser uma comunidade gigante é que é muito difícil mudar sua direção, especialmente quando essa direção ainda era meio incerta e experimental. Mesmo assim o Groovy teve um bom nicho e ainda hoje você vai ver Groovy se usa ferramentas como o Gradle. Groovy ganhou fôlego quando entrou no GitHub e quando ganhou patrocínio da Pivotal.</p>
<p>Pra quem não conhece a PivotalLabs foi uma consultoria que, junto com outros nomes emergentes como ThoughtWorks, ajudou a tornar Agile comercial mais visível, tinha na sua base tecnologias Java e Ruby também. Ela cresceu rápido pós 2008 e foi adquirida pela VMWare e depois pela EMC e hoje é uma empresa pública que mudou da área de serviços de consultoria pra produtos corporativos, mais especificamente cloud corporativo. Por 2 anos ela patrocinou o Groovy.</p>
<p>Voltando um pouco na história, até os anos 80 popularizou-se linguagens que chamamos de imperativas como C ou Pascal ou Perl. Elas são simples em estrutura. Você escreve funções que recebem argumentos e devolvem alguma estrutura de dados. Você compõe seus programas com funções que chamam outras funções. Rapidamente você nota que um conjunto de funções que atua em cima de uma mesma estrutura podem ser agrupadas em módulos. Assim nasce a idéia de linguagens como MODULA, precursor do Pascal. Mas então alguém resolve criar um conceito, ou uma abstração que uma estrutura com um grupo de funções pode ser o que chamamos de classes. E podemos criar várias instâncias dessa classe, que chamamos de objetos. Assim começa a nascer programação orientada a objetos.</p>
<p>Daí temos linguagens como Simula, Smalltalk, C++, Objective-C, Java, C#, Python. Organização de objetos pode ser implementada em cima de linguagens imperativas, então C com classes pode virar C++ ou Objective-C. Perl pode ganhar o equivalente a classes usando bless. PHP ganhou classes. Basic ganhou classes em Visual Basic. Pascal ganhou classes rudimentares em Turbo Pascal e depois em Object Pascal que é o Delphi. Até Cobol tem classes hoje em dia.</p>
<p>Em paralelo a isso havia outra área de pesquisas que deriva da linhagem de linguagens funcionais como Lisp, Scheme, ML, que lida com a matemática dessa composição de funções ou lambdas e é definida em Lambda Calculus, que se iniciou com o famoso Alonzo Church, conterrâneo de Alan Turing. Turing aliás, que foi aluno de Church, então você pode ver como as idéias dos dois se misturam e o trabalho de ambos é a fundação do que chamamos de Ciência da Computação. No caso da vertente de Church vamos simplificar assim, uma função recebe um argumento tipo um número ou uma string ou outra coisa, devolve outro tipo de elemento como resultado e, principalmente, tem um nome, e só.</p>
<p>?Mas por que toda função tem que ter um nome? Por que não podemos ter funções sem nome, o que chamamos de funções anônimas, e por que funções não podem receber funções como argumento e devolver funções como resultado? Em C temos algo rudimentar onde podemos passar o que chamávamos de callbacks, ou ponteiros pra memória onde está outra função, mas como eu disse era uma forma rudimentar.</p>
<p>Provavelmente uma das coisas mais diferentes que Ruby trouxe da herança de Lisp foi o conceito de lambdas para o mundo web, mesmo que ainda não tão avançado como era em Lisp, que implementava as idéias de lambda calculus de Church. Podemos chamar de funções anônimas, ou um tipo mais avançado que chamamos de closures ou blocos. Groovy também trouxe lambdas pra Java dentre outras coisas. Mas esse tipo de composição abriu caminho pra coisas como metaprogramação e DSLs ou Domain Specific Languages, que é como se fossem mini-linguagens em cima da linguagem principal. Em vez de somente seguir o que a linguagem dita e se pudéssemos manipular a própria estrutura e sintaxe da linguagem? A capacidade de moldar a linguagem pra ficar com a “cara” que você quisesse. Isso tornou o chato estilo imperativo de programar, em algo bem mais maleável e flexível e divertido, embora isso tenha aumentado mais a complexidade para quem está iniciando. Lambdas vieram pra ficar e depois disso toda nova linguagem e mesmo as antigas adotaram esses recursos. Hoje Java, C# e outros também ganharam lambdas e alguma capacidade de metaprogramação.</p>
<p>Com a virada do século e a aceleração antes do crash que tivemos nesse período, o poder computacional cresceu exponencialmente desde os anos 80, era a consequência da Lei de Moore, que ditava que o poder computacional dobraria mantendo o mesmo preço a cada 12 a 18 meses. Antes nos preocupávamos bem mais com a performance crua dos nossos programas pra tirar o máximo de máquinas caras e fracas, mas agora as máquinas finalmente ficaram poderosas e acessíveis, então passamos a ter surplus ou sobra de processamento. É importante salientar essa mudança de perspectiva. Finalmente em vez de termos que sofrer com linguagens simplistas e burocráticas em nome de economizar performance, porque não passar a pensar em formas de tornar a programação mais agradável?</p>
<p>Antes, linguagens mais simples de trabalhar era quase sinônimo de baixa performance, mas agora que a web provou que performance pura não era o único fator pra escolher uma linguagem, poderíamos começar a experimentar em funcionalidades e a escola de lambda calculus começou a aparecer novamente, graças aos programadores de cabelos brancos que haviam experimentado Lisp ou Scheme nas décadas de 70 a 80. E a tal orientação a objetos também veria uma evolução graças a quem experimentou Smalltalk no mesmo período.</p>
<p>Vale lembrar que Lisp já tinha o conceito de máquina virtual e garbage collector décadas antes de Java. E Smalltalk tinha o conceito de que tudo é realmente objeto e que objetos ficam vivos mesmo se a máquina virtual seja desligada. Pense num VirtualBox ou VMWare quando você sobre outro sistema operacional e então suspende a máquina. O Smalltalk tem esse conceito de suspender a execução sem bootar e limpar a memória, dentro do que ele chama de imagens ou no caso literalmente um banco de dados de objetos. Quando você abre uma imagem de Smalltalk ele pode ter objetos em memória que foram instanciados 30 anos atrás. E você vai compondo novos objetos usando os objetos vivos que já existem. Pensem que louco essa arquitetura.</p>
<p>E falando em arquiteturas exóticas. Uma editora de livros que ganhou notoriedade nessa virada de século foi a The Pragmatic Programmer, mesmo nome do clássico livro de 2 signatários do Manifesto Ágil e que ajudaram a dar credibilidade à adoção de Ruby: Dave Thomas e Andy Hunt. Eles também popularizaram o conceito de que todo programador deveria tentar aprender pelo menos uma nova linguagem por ano, e em 2007 eles publicaram suas experiências com a linguagem Erlang dos anos 80, de Joe Armstrong, de quando trabalhava na Ericsson. Erlang que como eu expliquei em outro episódio tem uma sintaxe muito diferente, herdada de Prolog, e que tinha uma arquitetura que ninguém no mundo web tinha visto ainda: o modelo de Atores. Erlang, como Java, tem uma máquina virtual. Mas diferente da JVM ele vai um passo além: ele tem partes do conceito de lambda calculus sem ser puramente funcional e partes das principais propriedades de objetos sem ser orientada a objetos. Mas em vez de organizar as coisas como classes e objetos, ele organiza como módulos e atores.</p>
<p>Um ator é como se fosse um objeto com seu estado encapsulado e se comunicando com outros atores via passagem de mensagem, diferente do Java, C++ ou C# e mais similar a Smalltalk e Objetive-C. E mais do que isso, num sistema operacional, além de carregar o Kernel ele também carrega serviços ou daemons via um serviço de supervisão como systemd ou initd ou outros dependendo do seu sabor favorito de Unix ou Linux. O Erlang tem algo similar, um sistema de supervisores que iniciam atores e se encarregam do ciclo de vida dos mesmos. Erlang assim como a JVM ou Smalltalk são quase como sistemas operacionais inteiros. Ao contrário dos interpretadores como Perl ou Python ou Ruby que foram feitos pra iniciar rápido e terminar rápido, Erlang e Java funcionam melhor se você iniciar e deixar elas parasitar a máquina toda.</p>
<p>Assim como Java a idéia é subir um único processo de Erlang e deixar que ele gerencie a máquina e seus programas rodando dentro dele. Erlang não foi feito pra ser reinicializado toda hora, e sim pra ficar de pé eternamente, inclusive essa era a grande funcionalidade pra indústria de telecomunicações: que você não estivesse numa ligação e de repente fosse cortado da sua conversa porque alguém precisou subir uma atualização. Ele tem inclusive o conceito de carregar código novo sem precisar necessariamente reinicializar tudo. Na prática é mais ou menos o que Java e J2EE também preferem: ficar de pé sem ficar reinicializando o tempo todo. Ficar de pé eternamente era o grande objetivo. Guardem essa característica porque vai ficar mais óbvio quando falarmos de infraestrutura e Devops mais pra frente. Mas o ponto é que essa linguagem super estranha despertou o interesse em arquiteturas muito diferentes do que estávamos usando.</p>
<p>Em 2009 Martin Odersky trouxe ao mundo o resultado das pesquisas que ele vinha fazendo no mundo de lambda calculus e Java desde os anos 80 e 90 na forma de outra linguagem que compilava em bytecodes Java como o Groovy. Essa linguagem foi Scala. E ele se popularizou muito rápido porque adicionava funcionalidades que tornavam o Java mais palatável, coisas como Traits que em Objective-C são o equivalente a Protocolos e alguns diriam que é uma forma mais limpa de se fazer herança de funcionalidades. Inferência de Tipos que eu já expliquei que vem de Hindley-Milner Type System de linguagens como ML, que levantou outro dilema da programação: a discussão se linguagens de tipagem estática são melhores ou não que tipagem dinâmicos.</p>
<p>Em um super resumo, todos os dados no seu código, variáveis, estruturas, listas, precisam ter tipos. Pense em tipos como moldes de biscoito. Você precisa dizer de antemão qual o tamanho do molde você vai querer, pequeno, grande. No caso de números, vai ser um inteiro de 32 bits? Um inteiro de 64 bits? Um inteiro sem sinal, ou seja, sem números negativos? Isso é importante pra um compilador saber quanto de memória precisa alocar pra cada coisa. Se fizer isso errado vai faltar memória e dar erro ou vai usar memória de forma desnecessária, causando leaks ou vazamentos. Em linguagens dinâmicas como Lisp, Python, Ruby a gente não determina isso no código, o interpretador pode determinar o molde, ou o tipo certo, durante a execução do código, por isso chamamos isso de tipos dinâmicos. Eles têm tipos, só que não estão estaticamente declarados no código como fazíamos em Java ou C#.</p>
<p>Declarar tipos é super tedioso e você pode errar quando está programando também. Simplesmente não declarar e deixar o interpretador decidir só quando já está rodando é muito mais conveniente mas pode causar desperdícios e alguns erros porque ele pode assumir o tipo que você não esperava e você só vai descobrir quando estiver rodando. Inferência de tipos é meio que o meio do caminho dos dois mundos porque a gente não escreve todos os tipos estaticamente no código, ou seja, deixa de ser absurdamente tedioso. Mas o compilador vai tentar adivinhar os tipos em tempo de compilação, daí o binário ou bytecode no final vai conter os tipos certos antes do programa executar. Só isso já torna escrever em Scala muito mais conveniente do que escrever em Java antigo. E Inferência de tipos foi melhorando e hoje se encontra em linguagens novas como Rust, Swift, Kotlin, Typescript.</p>
<p>Mesmo Java, desde a versão 5 você tinha possibilidade de omitir alguns tipos como quando usava a implementação rudimentar de Generics. No Java 8 você pode omitir tipos em declaração de lambdas. E no Java 10 ele ganha inferência de tipos locais como C# também. Como iniciante você vai notar que estudar tipos é muito importante, independente se você vai aprender uma linguagem dinâmica ou estática, se tem inferência de tipos ou não porque mesmo inferência não é mágica, o compilador ainda precisa de algum contexto ou anotação pra inferir o tipo certo. Na prática entenda: escolha o molde errado e seu biscoito vai sair do tamanho errado.</p>
<p>Scala cresceu rápido por um momento mas estagnou. Um grande problema do Scala é que o binário bytecode que ele gera de uma versão de Scala pra outra são incompatíveis. Ou seja, se você tem uma biblioteca compilada pra bytecode de Scala 1 e você faz um programa com Scala 2 e tenta carregar a biblioteca antiga, vai tomar um LinkageError da JVM. Isso era uma puta dor de cabeça: incompatibilidade binária entre coisas compiladas em diferentes versões de Scala. A única forma de solucionar isso é você recompilar tudo de novo sem reusar os binários antigos. É uma escolha de design do criador pra que ele pudesse inovar a linguagem sem se preocupar em ficar dando suporte pra coisas antigas. Hoje em dia o Go meio que exige a mesma coisa: ele prefere que você baixe o código fonte de todas as dependências, em vez de baixar binários pré-compilados, e compile tudo junto de uma só vez.</p>
<p>Aliás, um comentário importante: eu falei lá atrás que você como programador vai precisar sempre se preocupar com as dependências e as versões certas dessas dependências. Lembra da discussão de compilação estática vs compilação dinâmica? Nos anos 80 e 90 fabricantes de ferramentas de desenvolvimento como Borland, Microsoft, BEA, Oracle, traziam tudo junto. Eles que se viraram pra ter todas as dependências que você iria precisar já instalado ou de forma organizada que você pudesse comprar um CD e instalar na sua máquina. E mesmo assim os anos 90 foram infestados do que chamamos de “DLL Hell” com programas diferentes que precisam da mesma DLL mas em versões diferentes mas tudo no mesmo sistema entrando em conflito, literalmente um inferno de DLLs.</p>
<p>Com o advento do mundo open source, de repente você podia baixar bibliotecas gratuitas de código aberto e instalar você mesmo. Mas eles não estavam esperando por isso e virou uma grande bagunça no começo. Quem daquela época nunca copiou um site em PHP que na sua máquina tudo funciona mas quando copia pro servidor ele falha porque esqueceu de também copiar ou instalar as bibliotecas de dependência? Ou no caso de Java esquecia de copiar os tais Jars e a aplicação não queria nem iniciar?</p>
<p>Assim que nascem ferramentas como Maven no Java que é um grande banco de dados de bibliotecas binárias. Agora você tem um repositório centralizado onde quem escrevia bibliotecas poderia postar lá e você como desenvolvedor podia declarar num arquivo em XML, lógico, os nomes e versões das bibliotecas que precisava no seu projeto e o Maven se virava pra baixar tudo que você precisava. E como uma biblioteca pode precisar de outras bibliotecas, a gente costumava brincar que toda vez que precisávamos baixar todas as dependências do projeto, o Maven baixava a internet inteira … duas vezes. Hoje essa posição é do NPM do Javascript. No mundo Microsoft demorou mais pra algo similar aparecer mas no .NET você tem hoje NuGet que inclusive passou a vir pré-instalado no Visual Studio 2012 em diante.</p>
<p>No mundo de programação até esse ponto a gente tinha o Maven no mundo Java, e hoje muitos usam junto o Gradle que é feito em Groovy como eu disse antes. Temos o CPAN no mundo Perl até hoje. No mundo PHP era um híbrido, todo mundo baixava as dependências na mão mesmo tendo coisas como o Pear. Hoje em dia felizmente PHP tem algo mais moderno que é o PHP Composer. No mundo Ruby estávamos evoluindo a idéia de gerenciamento de versões com algo rudimentar implementado dentro do próprio Ruby on Rails, que baixava as dependências direto do GitHub e depois evoluímos pra ferramenta Bundler que na época se tornou o benchmark sobre o qual todo mundo se comparava.</p>
<p>Python preferia usar o próprio sistema de pacotes das distribuições Linux, então era uma zona porque você precisava fazer pacotes RPM ou DEB ou outros pra cada distribuição e você ainda tinha a zona de ferramentas como setuptools e easy_install, que de easy não tinha nada. Aliás, pra uma linguagem que se orgulha de ter um único jeito pra fazer cada coisa, instalar pacotes não era assim. Felizmente o mundo Python tem o PIP agora. No mundo Javascript como já falei nasceu o NPM que ganhou concorrência com o Yarn, co-autorado pelo mesmo autor do Bundler de Ruby. No mundo Elixir existe o Hex. No mundo Rust existe o Cargo. O mundo Go tem o dep, mas aqui a coisa é um pouco mais controversa.</p>
<p>Declarar dependências no seu projeto, instalar de algum repositório e gerenciar os upgrades de versão é uma coisa extremamente importante. Você nunca pode simplesmente atualizar todas as bibliotecas pras versões mais novas, porque seu código vai quebrar. Por outro lado você não pode ficar pra sempre com versões velhas, principalmente se elas tiverem relatos de buracos de segurança. Então você vai precisar atualizar as versões e alterar seu código pra comportar as mudanças. Vai ter vezes que o autor da biblioteca simplesmente desiste de manter sua biblioteca, vai ter buraco de segurança, agora você precisa achar outra biblioteca pra substituir e aí vai precisar reescrever toda a parte do seu código que dependia daquela biblioteca. É uma parte considerável do trabalho de ser um programador e manter um projeto grande.</p>
<p>Isso tudo dito, eu estava falando da incompatibilidade binária do Scala. Assim como Python tem PIP, Ruby tem Bundler o Scala tem o SBT que assim como muitas ferramentas tem a função híbrida de ser um gerenciador de pacotes e também um executor de tarefas. O Scala jogou na mão dos desenvolvedores de bibliotecas gerenciar diferentes binários de cada versão e tem um guia pra tentar manter compatibilidade binária ou o que fazer quando isso não é possível. É meio uma zona porque toda vez que o Scala lança uma versão nova, todo mundo que faz bibliotecas precisa ficar recompilando uma nova versão. Só que é irreal achar que todo mundo vai fazer isso, então você como programador que depende dessas bibliotecas, ou tem que ficar meses esperando todo mundo alcançar a versão nova, ou você mesmo precisa lidar com recompilar manualmente e ver se nada quebra.</p>
<p>Eu mencionei Go porque o Go é diferente da idéia de Java ou Scala, ele não é uma máquina virtual, nem é um interpretador. Ele é mais próximo de C++, ele compila direto num binário nativo e é feito para fazer aplicações e não drivers ou coisas mais baixo nível como fazemos em C. E ele prefere compilação estática pra criar binários que acabam ficando gigantes em tamanho mas que você pode mover pra outro lugar e ele simplesmente funciona. O Go fez uma coisa que a comunidade Ruby on Rails fez em 2008 ou 2009. Ele se integra direto com repositórios Git, em particular o GitHub. Todo desenvolvedor de bibliotecas mantém o código fonte no Github. Você usa o recurso de dep que eu falei pra baixar todas as dependências do GitHub e vai vir todos os código-fonte.</p>
<p>Como o intuito do Go é compilação estática ele precisa de todos os fontes disponíveis pra gerar um binário único. Então a menos que a sintaxe da linguagem não mude de forma drástica de uma versão pra outra, você sempre consegue um binário que não quebra no final sem depender de linkagem dinâmica e LinkageError que o Scala sofre. O Elixir faz algo similar com o Hex, o Rust faz algo similar com o Cargo. A idéia é sempre baixar os código-fontes das dependências e deixar próximo do código do seu próprio projeto e compilar tudo junto depois que baixa em vez de baixar bibliotecas em formato binário como os Jars de Java.</p>
<p>A diferença é que em Java o compilador é notoriamente lento. Em vez de toda vez compilar binários a partir do código fonte, nós compilamos e empacotamos em zips que chamamos de JARs, é o que se publica num Maven por exemplo. Daí você ou manualmente ou via Maven ou SBT no caso do Scala, baixa os JARs pré-compilados. A mesma coisa é no mundo .NET, mas a Microsoft tende a ser melhor em manter compatibilidade binária das suas dependências e agora de gerenciar as diferentes versões de cada uma. Antigamente isso era importante porque não se tinha esse conceito de código aberto, as empresas vendiam bibliotecas e preferiam não abrir o código, na verdade eles faziam o máximo inclusive pra obfuscar os bytecodes gerados pra dificultar alguém fazer engenharia reversa e ver como que ele foi implementado.</p>
<p>Por isso eu salientei a diferença na virada do século da explosão do mundo de código aberto e serviços como GitHub e linguagens interpretadas como PHP, Python, Ruby que precisam ter os códigos disponíveis. Scala ainda acaba herdando essa parte do Java, mas linguagens como Go, Rust e Elixir investiram em fazer compiladores bem mais velozes e baixar dependências na forma de pacotes que embutem o código-fonte e compilar seu programa e as dependências tudo localmente em vez de depender de versões binárias pré-compiladas.</p>
<p>Voltando ao papo de arquiteturas, Groovy e Scala trouxeram ao mundo Java uma sintaxe bem mais moderna do que o antigo Java e isso ajudou a linguagem a evoluir muito da versão 6 até agora. Scala também trouxe a arquitetura de Atores para o mundo Java e somado à The Pragmatic Programmer evangelizando Erlang, trouxe também a possibilidade de ir direto na fonte, já que foi ela que introduziu o conceito de Atores com seu framework OTP ou Open Telecom Platform. Softwares que foram escritos pra Erlang antes também começaram a ganhar a luz do dia, em particular ejabberd e RabbitMQ numa época onde produtos de comunicação como Facebook Messenger ou Whatsapp começaram a aparecer. Erlang, sendo um produto desenhado pra indústria de telecom era um par perfeito pra produtos de comunicação em massa. Mas sejamos honestos, a linguagem do Erlang é horrível de usar nos dias de hoje, talvez ok pros anos 80, mas não pro século XXI.</p>
<p>A comunidade Ruby começou a olhar Erlang e rubistas como Tony Arcieri criou a linguagem Reia. Assim como a JVM do Java roda os bytecodes compilados de linguagens como Groovy e não só do Java puro, o Erlang também tem uma máquina virtual chamada BEAM com seus próprios bytecodes. O Reia foi o primeiro experimento de tentar modernizar a sintaxe do Erlang, nesse caso imitando quase diretamente a sintaxe do Ruby para compilar em bytecodes que rodam no BEAM. Em paralelo a ele surgir outra tentativa de fazer a mesma coisa, desta vez de outro rubista, o brasileiro José Valim com a linguagem Elixir.</p>
<p>O Elixir foi uma tentativa mais avançada de não imitar diretamente o Ruby mas de criar uma sintaxe moderna em cima das capacidades do BEAM do Erlang e também de criar um conjunto de bibliotecas base para possibilitar realmente criar aplicações de verdade. Levou quase 4 anos pra Elixir finalmente se tornar usável de verdade pra fazer aplicações de verdade em produção. Ele aproveita tudo que o Erlang já faz mas com uma sintaxe e um conjunto de bibliotecas que remetem a coisas como Ruby, aumentando muito a produtividade. Também por causa da influência do framework Ruby on Rails surgiu o framework Phoenix para desenvolvimento de aplicações Web.</p>
<p>Ruby, como vocês já devem ter percebido, é uma linguagem que eu pessoalmente gosto. Ela ajudou a influenciar outras linguagens como Groovy, depois da comunidade saiu CoffeeScript que ajudou a influenciar o atual Javascript ES6, e também influenciou a sintaxe do Elixir e do estilo da programação de suas bibliotecas. Em paralelo começamos a experimentar mais com outras linguagens uma vez que o paradigma funcional, baseado no lambda calculus que falei acima começou a ganhar terreno. Linguagens obscuras como Haskell e OCAML começaram a ganhar um pouco mais de terreno, inclusive OCAML influenciando a linguagem F# no mundo .NET. Tudo isso também ajudou a evoluir a própria linguagem Java, agora com mais de 10 anos de idade.</p>
<p>Para finalizar este episódio preciso falar da linguagem Go mais um pouco, que eu já vim citando durante este episódio e o anterior. Todos sabem que é a linguagem do Google, mas eu não gosto muito de Go porque pra mim ela representa um passo pra trás. Ela foi criada em 2009 e ignorou muitos avanços que outras linguagens já estavam fazendo. Por exemplo, a inferência de tipos que eu falei que o Scala popularizou e linguagens modernas como Swift, Rust, C# e mesmo Java começaram a usar é menos poderosa em Go. Os tipos do Go são mais pobres, ele não implementa Traits como Rust. Em uma linguagem criada em 2009 que se poderia escolher tudo do melhor, ela preferiu o caminho mais fácil de escolher permitir o ponteiro nulo que é causa de muitos bugs em muitas linguagens. O sistema de tipos é mais pobre, os Generics são mais pobres. Ela não permite estender e redefinir a linguagem, não suporta sobrescrever coisas como operadores. Ela é de fato uma versão melhor do que um C++, que cresceu tão monstruoso e complexo que é realmente difícil de usar hoje em dia. Mas ela não evoluiu a linhagem das linguagens. Como linguagem ela representa um passo pra trás.</p>
<p>Nenhuma linguagem é perfeita mas cada nova linguagem veio evoluindo em cima dos seus antecessores e o Go é basicamente mais um caso da síndrome do Google de reinventar a roda. Eu quase diria que o Go é no back-end o que o Javascript se tornou no front-end: nós usamos mais porque existem softwares importantes escritos em Go, como o ultra famoso Docker, e agora ela está se tornando grande demais pra falhar, em breve não vai mais ser uma escolha não usarmos. Vou deixar linkado nas descrições abaixo um link com diversos artigos explicando todos os problemas dessa linguagem, ela compete com Javascript em críticas. Porém, não se pode negar que ela de fato trouxe algumas coisas importantes que vou explicar também no próximo episódio.</p>
<p>No próximo episódio eu vou continuar essa história com Go, mais Elixir, um pouco mais de Rust, mais de Javascript, talvez eu fale de Python e principalmente vou tentar explicar mais da importância da Apple nos últimos 20 anos para o desenvolvimento de software e como ela cruza com um evento importante no mundo open source.</p>
<p>A história do Back-end não acabou, eu disse que ia ser longo. No episódio de hoje eu voltei à década de 90 e dei uma breve introdução da evolução das arquiteturas de desenvolvimento de software, retornei mais ao mundo Java e entendemos mais sobre a longa e inacabada novela chamada gerenciamento de dependências, uma praga que vai te perseguir por toda sua carreira de programação e finalmente começamos a ter um gosto das novas linguagens, começando com Groovy, Scala, Erlang, Elixir, e um pouco do Go.</p>
<p>Acho que finalmente passamos da metade deste tema nesta série, não deixem de mandar suas perguntas nos comentários abaixo, se curtiu o vídeo mande um joinha, assinem o canal e pra não perder a continuação dessa história clique no sininho e não deixe de compartilhar com seus amigos. Como semana que vem temos Carnaval é possível que eu tire o feriado pra descansar, vamos ver. Se não nos vermos até lá, bom Carnaval pra todo mundo!</p>
]]></content:encoded><category>linguagens-de-programacao</category><category>backend</category><category>programacao</category><category>akitando</category></item><item><title>[Akitando] #40 - Entendendo Back-End para Iniciantes em Programação (Parte 1) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/02/20/akitando-40-entendendo-back-end-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/02/20/akitando-40-entendendo-back-end-para-iniciantes-em-programacao-parte-1-serie-comecando-aos-40/</guid><pubDate>Wed, 20 Feb 2019 17:00:00 -0300</pubDate><description>&lt;p&gt;Disclaimer: esta série de posts são transcripts diretos dos scripts usados em cada video do canal &lt;a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener"&gt;Akitando&lt;/a&gt;. O texto tem erros de português mas é porque estou apenas publicando exatamente como foi usado pra gravar o video, perdoem os errinhos.&lt;/p&gt;
&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/Qjk-cSW-jk4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h3&gt;Descrição no YouTube&lt;span class="hx:absolute hx:-mt-20" id="descrição-no-youtube"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o-no-youtube" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Este é o 5o episódio da série &amp;ldquo;Começando aos 40&amp;rdquo;. Você deve assistir os episódios anteriores da série pra entender onde estamos e recomendo assistir os 2 vídeos da série &amp;ldquo;Sua Linguagem Não É Especial&amp;rdquo;. No episódio de hoje vou começar a introduzir os conceitos básicos para o que chamamos de &amp;ldquo;back-end&amp;rdquo;, que na prática é a própria introdução à programação.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Disclaimer: esta série de posts são transcripts diretos dos scripts usados em cada video do canal <a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener">Akitando</a>. O texto tem erros de português mas é porque estou apenas publicando exatamente como foi usado pra gravar o video, perdoem os errinhos.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/Qjk-cSW-jk4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h3>Descrição no YouTube<span class="hx:absolute hx:-mt-20" id="descrição-no-youtube"></span>
    <a href="#descri%c3%a7%c3%a3o-no-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Este é o 5o episódio da série &ldquo;Começando aos 40&rdquo;. Você deve assistir os episódios anteriores da série pra entender onde estamos e recomendo assistir os 2 vídeos da série &ldquo;Sua Linguagem Não É Especial&rdquo;. No episódio de hoje vou começar a introduzir os conceitos básicos para o que chamamos de &ldquo;back-end&rdquo;, que na prática é a própria introdução à programação.</p>
<p>A intenção do vídeo não é serem aulas nem tutoriais, mas conectar os pontos de diversos assuntos que vocês vão encontrar de forma desconexa se sair pesquisando pelo Google. Se você fez faculdade, já deveriam saber tudo que vou dizer aqui, então veja o que faltou nos estudos e acelere!</p>
<p>Em resumo, vou falar rapidamente sobre os conceitos mais simples, linguagem Assembly, compiladores, interpretadores, máquinas virtuais, bytecode, processos, forks, threads, tudo que qualquer programador tem obrigação de saber na ponta da língua e em paralelo vou passar rapidamente de novo pelos anos 90.</p>
<p>Como o episódio ficou longo, eu dividi em 2 partes. Na de hoje vou só até o crash das ponto coms em 2001, e vamos continuar dali na Parte 2 da semana que vem.</p>
<p>Links:</p>
<ul>
<li>
<p>Playlist &ldquo;Começando aos 40&rdquo; (<a href="https://www.youtube.com/watch?v=O76ZfAIEukE&amp;list=PLdsnXVqbHDUc7htGFobbZoNen3r_wm3ki"target="_blank" rel="noopener">https://www.youtube.com/watch?v=O76ZfAIEukE&list=PLdsnXVqbHDUc7htGFobbZoNen3r_wm3ki</a>)</p>
</li>
<li>
<p>Sua Linguagem Não É Especial Parte 1 (<a href="https://www.youtube.com/watch?v=p9-WuJbVHHc"target="_blank" rel="noopener">https://www.youtube.com/watch?v=p9-WuJbVHHc</a>)</p>
</li>
<li>
<p>Sua Linguagem Não É Especial Parte 2 (<a href="https://www.youtube.com/watch?v=XcTTajFENHI"target="_blank" rel="noopener">https://www.youtube.com/watch?v=XcTTajFENHI</a>)</p>
</li>
<li>
<p>Conheça a história (brasileira) da informática (<a href="https://keepgeek.wordpress.com/2010/08/01/conheca-a-historia-brasileira-da-informatica/"target="_blank" rel="noopener">https://keepgeek.wordpress.com/2010/08/01/conheca-a-historia-brasileira-da-informatica/</a>)</p>
</li>
<li>
<p>Meet the Unitron Mac 512 – the World’s First Macintosh Clone (<a href="https://www.cultofmac.com/266710/meet-unitron-mac-512-worlds-first-macintosh-clone/"target="_blank" rel="noopener">https://www.cultofmac.com/266710/meet-unitron-mac-512-worlds-first-macintosh-clone/</a>)</p>
</li>
</ul>
<p>Canais Retro:</p>
<ul>
<li>The 8-bit Guy (<a href="https://www.youtube.com/channel/UC8uT9cgJorJPWu7ITLGo9Ww"target="_blank" rel="noopener">https://www.youtube.com/channel/UC8uT9cgJorJPWu7ITLGo9Ww</a>)</li>
<li>Modern Vintage Computer (<a href="https://www.youtube.com/channel/UCjFaPUcJU1vwk193mnW_w1w"target="_blank" rel="noopener">https://www.youtube.com/channel/UCjFaPUcJU1vwk193mnW_w1w</a>)</li>
<li>LGR (<a href="https://www.youtube.com/user/phreakindee"target="_blank" rel="noopener">https://www.youtube.com/user/phreakindee</a>)</li>
</ul>
<p>Me sigam também em outras redes sociais:</p>
<ul>
<li>Twitter (<a href="https://twitter.com/akitaonrails"target="_blank" rel="noopener">https://twitter.com/akitaonrails</a>)</li>
<li>Instagram (<a href="https://instagram.com/akitaonrails"target="_blank" rel="noopener">https://instagram.com/akitaonrails</a>)</li>
<li>Facebook (<a href="https://facebook.com/akitaonrails"target="_blank" rel="noopener">https://facebook.com/akitaonrails</a>)</li>
<li>Quora (<a href="https://www.quora.com/profile/Fabio-Akita"target="_blank" rel="noopener">https://www.quora.com/profile/Fabio-Akita</a>)</li>
<li>Blog (<a href="https://www.akitaonrails.com"target="_blank" rel="noopener">https://www.akitaonrails.com</a>)</li>
</ul>
<h2>Script<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Espero que vocês tenham sobrevivido ao meu último episódio onde gastei um longo tempo tentando dar contexto sobre as tecnologias de Front-End. Também espero que tenham entendido a receita: na dúvida, encontrem o contexto de porque determinada tecnologia foi criada. Tecnologia são ferramentas, ferramentas foram criadas com um propósito. Por isso não existe uma única tecnologia que resolve todos os problemas. Já diria o sábio que pra quem acha que só existe martelo, todos os problemas são pregos.</p>
<p>E seguindo nessa mesma linha, no episódio de HOJE, vou tentar dar contexto para as tecnologias que chamamos de Back-end. Porém, back-end é muito, mas muito maior do que front-end e mesmo se eu fizer um episódio de uma hora, não vai dar pra explicar tudo que eu quero. Eu comecei a escrever o script deste episódio, aí cheguei na página 10, fui escrevendo e cheguei na página 20 e mesmo vocês tendo gostado do episódio anterior que deu quase 50 minutos se eu tentar encaixar tudo agora vai facilmente dar mais de 2 horas. E se eu fizer isso vou correr demais e talvez não explicar direito, então vou ser obrigado a cortar em vários episódios e mesmo assim vai ficar grande! Então vamos lá porque temos bastante coisa pra cobrir.</p>
<p>Pra começar, a pergunta mais comum quando se fala em programação back-end pra Web é qual linguagem aprender? Javascript? Java? C#? PHP? Python? Ruby? Elixir? Go? Scala? Clojure? Rust?  Nos episódios sobre Sua Linguagem não é especial que eu espero que tenham assistido, eu dei uma rápida introdução nos primeiros 40 anos de história das linguagens e como uma linguagem veio influenciando as outras. Assistam essa história antes e, mesmo que não tenha intenção de se tornar um desenvolvedor front-end, assista o episódio anterior antes pra ter o contexto necessário. Se não assistiu vai lá, eu espero ….</p>
<p>Pronto, já foi? Então vamos lá!</p>
<p>(&hellip;)</p>
<p>No Brasil infelizmente nós praticamente pulamos a história da computação até os anos 80 e só começamos mais ou menos nos anos 90. Isso porque antes dos anos 80 vivíamos numa ditadura, e perdemos o timing da evolução dos transistores e nunca tivemos um mercado decente de microprocessadores. Nosso mercado era fechado a importar coisas de fora. Maldita reserva de mercado, toda reserva de mercado é um puta erro. A gente precisava ir até o Paraguai pra conseguir comprar máquina decente. Havia também computadores nacionais como os da Itautec e Microsiga que faziam clones de IBM PC e mesmo de outros como os famosos Sinclairs, quem não lembra da série TK como o TK-90? Tinha a Hotbit e Gradiente fazendo MSX que eu gostava bastante. E tinha a Unitron que fazia clones de Apple II e chegou até mesmo a fazer um clone excepcional do Mac 512, que era quase igual e rolou até uma controvérsia direto com a Apple onde eles quase pediram sanções dos EUA contra exportações do Brasil por causa disso.</p>
<p>Essa história é interessante e explica porque por muitos anos Steve Jobs odiou o Brasil, vou deixar linkado na descrição abaixo sobre isso. E tivemos até sistemas operacionais nacionais como o Sisne que era um clone de MS-DOS, só que em português. Eu brincava de Basic com computadores como Apple II e MSX quando ia na casa de amigos, meu primeiro mesmo veio do paraguai no fim dos anos 80. Foi um PC XT de talvez 10 Mhz ou algo assim, poderosos 1 MB de RAM e 10 MB de HD, com monitor CGA verde, de incríveis 4 tons de verde. Fiz muitos programas em dBase e Clipper nessa máquina, fiz muitos trabalhos de escola com ele. E joguei muito também. Você não precisa da máquina mais potente pra aprender a programar, você precisa aprender a controlar a máquina que tem e tirar o absoluto máximo que ela pode dar. Ninguém nunca vai dirigir bem uma Ferrari se sequer tem coordenação pra trocar marcha num Gol.</p>
<p>Vocês vão notar que mesmo hoje em dia, em pleno 2019, é muito raro encontrar programadores de cabelo branco. Eu mesmo só comecei a ganhar os meus nos últimos poucos anos. E esse é um dos motivos: havia muito pouco programador antes dos anos 80. Mesmo durante os anos 80 era difícil ser programador. Então os que estão na ativa são ainda mais raros. Nos Estados Unidos você vê vários, incluindo famosos como os Uncle Bob da vida. Por isso temos muito pouca gente realmente com décadas de experiência. Se programador no Brasil só se tornou uma carreira realmente mainstream depois do advento da Web, antes era bem mais nicho. E por isso você vê muito pouca gente que pode dizer coisas como: trabalhei em projetos com Lisp, ou Smalltalk, ou PROLOG, sem ser do meio acadêmico.</p>
<p>Bom, antes de falarmos sobre as linguagens novas, precisamos começar do começo. Todo hardware, mais especificamente a CPU vem de fábrica entendendo um certo conjunto de instruções ou funções. Eles tem registradores que é como se fosse um tipo de memória. Você coloca valores nesses registradores e chama uma instrução que é mais ou menos a mesma coisa que chamar uma função passando argumentos, se você começou a aprender alguma linguagem. Daí a CPU executa alguma coisa e grava o resultado em outros registradores e você pode ler a resposta. A linguagem que usamos pra falar diretamente com a máquina é Assembly, e seu montador, o assembler, traduz o código escrito em texto com mnemônicos como JMP ou ADD ou MOV diretamente nas instruções binárias da máquina.</p>
<p>Além do hardware temos instruções específicas do sistema operacional, o que chamamos de syscalls ou chamadas de sistema. Então o tal binário nativo que falamos são instruções pra máquina e pro sistema operacional que é uma abstração da máquina.  Sempre que falamos em abstrações pense que é uma forma de pegar uma sequência de instruções que, por si só, podem ser difíceis de entender o que vai fazer e encapsular numa chamada só com um nome mais descritivo. É o que você chamaria de funções. Um sistema operacional tem várias funções que abstraem instruções mais simples que a máquina oferece, como ler arquivos, ou ou alocar espaço na memória pra um programa.</p>
<p>É extremamente chato e trabalhoso escrever Assembly diretamente, além de ser bem ineficiente hoje em dia, tanto porque precisamos escrever muito código pra fazer uma aplicação inteira e porque a performance não vai ser melhor e vou explicar porque a seguir. Mas pior, o código que escrevemos é totalmente dependente da CPU que estamos usando. Programar Assembly de Intel x86 ou PowerPC ou ARM exigem instruções diferentes. Por isso temos linguagens como C. Um “compiladores” traduz o código como escrito em C e realiza diversas otimizações antes de terminar de traduzir em instruções binárias. Mas mais importante, podemos usar o mesmo código C com diferentes compiladores pra gerar as instruções de um Intel x86 ou ARM sem mudar o código C.</p>
<p>Uma linguagem como C cria um programa binário nativo. Ou seja, ele depende de chamar diretamente instruções do sistema operacional e da máquina. Um compilador faz muito mais que só traduzir código um pra um pra instruções de máquina, ele vai tentar o possível pra melhorar seu código primeiro. Vai tirar instruções que não servem pra nada. Vai tentar executar coisas fora de ordem se o resultado for o mesmo. Vai trocar pedaços inteiros por instruções mais simples. Vai tentar prever o que é o próximo resultado e já pré-calcular e assim por diante. Ele literalmente faz mágica, portanto parta do princípio que seu código deve ser escrito primariamente pra que outro ser humano consiga entender e é trabalho do compilador melhorar pra ficar mais eficiente na máquina. Você eventualmente pode otimizar seu código pra ficar mais eficiente quando ficar complexo, mas se sua linguagem exige que você faça isso o tempo todo, sua linguagem é defeituosa.</p>
<p>No processo de transformar um código texto em um binário que executa na máquina você precisa primeiro de um compilador. Só que ninguém escreve um programa 100% do zero. Normalmente você nem fala diretamente com o hardware ou o sistema operacional. Existem dezenas de bibliotecas que você deve reusar e que vão facilitar seu trabalho. No C você vai precisar de coisas como LIBC ou no C++ você talvez vai usar coisas como Boost. São grandes bibliotecas reusáveis pra você não ter que ficar reinventando a roda toda hora. No Windows seriam as DLLs que você já deve ter visto, como win32. No Linux a extensão dessas bibliotecas binárias é ponto SO. Agora você precisa de um linker que vai ligar seu programa compilado com essas bibliotecas. E você tem duas opções, linkar estaticamente ou linkar dinamicamente.</p>
<p>Pra linkar estaticamente você precisa do binários antes dele virarem um SO ou DLL, ou seja do que chamamos de arquivo de objetos, normalmente com extensão ponto O que é transformado em ponto A ou ponto LIB em Windows. Se seu programa é um binário e a biblioteca é outro binário, ao linkar estaticamente você vai ter as duas coisas mescladas num único bináriozão. Porém se todo programa que você fizer tiver que mesclar o binário das bibliotecas, vai ter um binários gigantes. Por outro lado você pode linkar dinamicamente, pra isso você precisa do arquivo de cabeçalho da biblioteca que é um código que declara quais funções estão expostas no binário dessa biblioteca. Dessa forma todos os programas vão ter tamanho menor e vão reusar o mesmo binário da biblioteca que está instalado no seu sistema.</p>
<p>Porém ao copiar esse binário pra outra máquina você precisa garantir que a biblioteca dinâmica está lá também. É por isso que muitos programas no Windows antigamente mandavam você instalar antes o runtime do Visual Basic 6 ou hoje em dia o runtime do .NET ou outros componentes como DirectX pra games. E mais, você precisa instalar a versão certa de cada biblioteca ou vai ter problemas. Se você já está instalando e configurando seu Linux do zero na mão, já deve ter aprendido sobre variáveis de ambiente. Não basta só baixar as bibliotecas e copiar em algum diretório qualquer. Pra que seus executáveis encontrem as bibliotecas, se você instalar na mão, vai precisar mexer em variáveis de ambiente como LD_LIBRARY_PATH que indicam onde procurar por elas.</p>
<p>Essa discussão se linkar dinamicamente ou estaticamente é melhor é uma das coisas que programadores gostam de discutir. Ambas tem vantagens e desvantagens. Se você tem todo o código-fonte de tudo disponível o tempo todo e quer um binário que você possa copiar pra outra máquina e não se preocupar com dependências, pode compilar tudo estático, ficar com um binário gigante e quando eu digo gigante estou falando de binários de 200Mb ou mais ainda. É o que linguagens novas como o Go do Google fazem: ele prefere baixar o código fonte das dependências e compilar e linkar tudo estaticamente. Tem algumas exceções que vou falar depois mas na maior parte do tempo todo binário de Go você pode gerar na sua máquina, copiar num servidor e na maior parte do tempo tudo funciona sem se preocupar em precisar instalar dependências dinâmicas.</p>
<p>Mas se é tão simples assim e ficar se preocupando com versões de bibliotecas dinâmicas ou se elas estão instaladas é tão chato, porque não compilar tudo estaticamente? Primeiro porque você não precisa ter o código fonte das bibliotecas, só o arquivo de cabeçalho que lista suas funções. Segundo, porque digamos que você tenha 10 programas, todas elas linkam estaticamente uma biblioteca de criptografia. Digamos que descobrimos que tem um bug de segurança crítico nessa biblioteca. Se todos os programas estiverem linkados dinamicamente, basta recompilar só a biblioteca de criptografia e substituir na máquina em cima da antiga e todos os 10 programas automaticamente vão carregar a versão corrigida. Caso contrário, você agora precisa recompilar todos esses 10 programas pra que eles gerem novos binários com as correções e substituir na máquina um a um. Tudo sempre depende de pra que serve seu programa. Aliás, programação é muito isso: aprender a fazer escolhas entre opções que não estão erradas, que você precisa saber o contexto. Nem sempre as escolhas são simples, vá se acostumando com isso. É como em arte, qual cor é melhor? Vermelho? Azul? Amarelo? Não existe cor certa. Existe saber usar a cor certa no lugar certo.</p>
<p>Na maioria das distribuições Linux existe o conceito de um gerenciador de pacotes. No Redhat tempos o RPM e a ferramenta yum, em distribuições derivadas de Debian como o Ubuntu tempos DEB e a ferramenta apt.
Em Arch Linux e derivados como Manjaro temos o pacman. E assim por diante. Mas quando não existe o pacote de algum programa que queremos você normalmente baixa um tarball que é tipo um zip, e vai precisar descomprimir, compilar e instalar a partir desse código fonte. E você normalmente vai ver a seguinte sequência de 3 comandos que precisa executar na linha de comando. Primeiro tem o ./configure. Configure é responsável justamente por checar se você tem todas as dependências que se precisa pra compilar já instalados na sua máquina, se não tiver ele vai dar erro e você precisa instalar as dependências e rodar configure de novo. Daí você roda make. Make lê um arquivo chamado Makefile que vem no projeto e que declara tarefas que a ferramenta make pode executar, e o default ou padrão é compilar.
Ele vai chamar compiladores como o GCC ou CLANG e transformar o código C em binários.
E por fim você tem o make install que é o make chamando a tarefa install do Makefile que vai pegar esse binário que acabou de compilar, dar permissão de executar, e mover pra um diretório que o PATH consiga encontrar, como o /usr/local/bin ou pra outro lugar arbitrário tipo /opt/blabla e colocar esse diretório no PATH se precisar. Toda vez que você faz essa sequência vê como demora.</p>
<p>Programas modernos são dependentes de dezenas de bibliotecas e o compilador tem bastante trabalho pra transformar centenas de arquivos ponto C em ponto O e depois no binário final linkado estaticamente ou dinamicamente com as bibliotecas que precisa, e só aí finalmente você pode executar seu programa. Em Windows você quase nunca instala programas assim, você normalmente já baixa o binário que vem embutido num instalador. O instalador normalmente instala as dependências que precisa. Em Windows, ao contrário de Linux, preferimos manter todas as versões de uma determinada biblioteca no sistema. Por isso você tende a ter um sistema operacional mais gordo que o Linux e com bibliotecas bem antigas ainda disponíveis, mas a vantagem é que conseguimos pegar um binário feito pra Windows 98 e ele vai provavelmente funcionar no Windows 10. MacOS é um híbrido dos dois conceitos, ele mantém muitas bibliotecas antigas até um certo ponto, mas não tão longe quanto o Windows, e ao contrário do Windows ele trás já os compiladores e dá a opção de compilar direto do código-fonte mantendo os cabeçalhos de bibliotecas que precisam pra compilar. Por isso existem adaptações de programas de Linux que funcionam perfeitamente em MacOS.</p>
<p>Você vai ver mais sobre esse tipo de escolha quando estiver usando linguagens que compilam em binários nativos, como C, C++, Pascal ou Object Pascal do Delphi, Objective-C ou Swift pra iOS e até em linguagens novas como o Google Go que eu falei a pouco e o Rust da Mozilla. Mas do outro lado espectro existe o conceito de um interpretador. Um interpretador é em si só um programa mas a função dele é ler código que você escreveu e traduzir em instruções pra máquina sem necessariamente precisar compilar esse código em binário nativo. Ou seja, ele depende de ter o código fonte do seu programa e traduzir, ou interpretar, o código toda vez que carrega.</p>
<p>Ele contém partes de um compilador, como o parser. Pra interpretar o código que você escreveu, precisamos de um programa que leia esse código e interprete de acordo com uma gramática da linguagem e traduza isso em instruções de máquina ou uma representação similar, pra isso serve o parser. Aqui que a coisa começa a ficar confuso, mas vamos focar pra tentar entender. Originalmente um interpretador é só um programa como eu já disse, você executa na linha de comando e passa como argumento o caminho pra um arquivo de texto que contém o código que você escreveu na linguagem que esse interpretador entende. Ele vai carregar esse arquivo, vai usar o parser pra checar que você escreveu certo, vai transformar numa representação interna que ele entende, normalmente numa árvore - lembra que eu falei no episódio anterior da importância de você entender estruturas de árvores? E finalmente vai começar a interpretar e executar as instruções pra fazer o que você pediu. Quando terminar seu programa, o interpretador também termina.</p>
<p>É assim que funcionam originalmente linguagens como Perl, PHP, Python, Ruby, até Javascript. Elas variam em algumas funcionalidades, por exemplo, Perl e Python permitem pré-compilar seu código fonte na representação intermediária que eu falei. O termo “compilar” pode confundir porque você pode achar que ele está compilando num binário nativo como o compilador de C, mas é diferente. Ele apenas vai economizar o tempo de ler o arquivo texto e parsear na tal estrutura de árvore que num Ruby chamamos de AST ou Abstract Syntax Tree. Por isso em Perl você tem a extensão ponto PL pro código fonte e ponto PLX pro pré-compilado ou em Python você tem ponto PY pro código fonte e ponto PYC pro pré-compilado. Ruby e Javascript não tem essa funcionalidade ainda. Na prática tanto faz, hoje em dia os HDs são rápidos o suficiente pra não importar tanto isso.</p>
<p>Antigamente fazia sentido porque a principal função de um interpretador era permitir que você pudesse criar programas de linha de comando que executam e terminam muito rápido. Você podia chamar de duas formas: digitando perl e passando como argumento o arquivo do código ou ligando o flag de execução direto do arquivo texto de código e tendo como primeira linha o que chamamos de “shebang” que é a cerquilha com exclamação e o caminho absoluto pro executável do interpretador. E se você já fuçou pela Web por scripts deve ter visto isso. Existia ainda outra opção que era criar um pacote que embute o interpretador junto com a representação do seu código fonte num único executável. O Perl permitia isso e é o que o Visual Basic antes da versão 6 também fazia. No final parecia que era um binário nativo como de um C, mas na verdade era um pacote que embutia as duas coisas. De curiosidade eu disse Visual Basic até o 6 porque no 6 você tinha a opção de compilar o código fonte em binário nativo quando ele passou a adaptar e usar o compilador do Visual C++. Visual Basic 6 é um produto que me impressionou bastante na época.</p>
<p>Como eu disse antes, um interpretador originalmente foi feito pra iniciar rápido e terminar rápido. No episódio anterior eu comecei a falar sobre servidores web. Todo sistema operacional lida com processos. Toda vez que você executa uma aplicação, seja o executável nativo compilado em C ou os interpretadores que citei antes que também são feitos em C ou C++, o trabalho do sistema operacional é carregar, alocar memória e criar um processo em execução. Em ambientes UNIX ou principalmente Linux criar processos é uma coisa relativamente barata, ordens de grandeza se comparar com MacOS ou Windows. Em Windows criar processos envolve mais peso ou o que chamamos de overhead, não é uma coisa barata e nem é porque ele é pior mas porque tem requerimentos diferentes. Pensando antigamente num UNIX, um programador naturalmente  faria um programa que é executado e, se for um servidor TCP, ele só consegue servir um cliente TCP de cada vez, como já expliquei no episódio anterior.</p>
<p>Pra conseguir servir múltiplos clientes TCP ao mesmo tempo, a coisa mais fácil de se fazer é um FORK do processo, ou seja, criar uma cópia do programa em execução na memória. Cada nova conexão o servidor faz um novo FORK. E esse fork é razoavelmente rápido de criar e usa pouca memória porque ele reusa os mesmos bits do programa pai original que também está na memória, é um truque do sistema operacional e esse recurso é chamado de COW ou Copy on Write que evita fazer uma cópia inteira do executável da memória pra outro espaço de memória. Então se um processo precisa de 200 kbytes em memória, no instante que fizer fork ele não vai dobrar a memória e usar 400, vai usar muito menos que isso.</p>
<p>Um servidor web como o Apache antigo que eu falei, no começo só usava FORK. Por isso ele era simples. E mais do que isso, ganhou a capacidade de executar programas dependendo da URL que era pedido, através do padrão que falei que ficou conhecido como CGI. No começo esses executáveis eram só programas nativos compilados de C. Mas falei que rapidamente fizeram Perl funcionar no Apache também e começamos a era da web dinâmica. Pois bem, interpretadores com programas curtos iniciam e terminam rápido. Mas se o programa vai ficando mais e mais complicado, incluindo coisas custosas como conectar num banco de dados ou manipular arquivos, ou carregar muitos dados em memória, é muito custoso ficar iniciando e terminando esses programas toda vez. Foi quando surgiu a primeira gambiarra na forma de coisas como o mod_perl e depois o mod_php que são módulos compilados dentro do Apache e que dá inclusive acesso às estruturas internas do Apache. Toda vez que um novo fork do Apache é feito ele já pré-carrega com a capacidade de executar Perl ou PHP sem precisar carregar o interpretador separadamente. Isso é mais rápido do que CGI puro, porém é um terrível buraco de segurança já que bugs no Perl ou no PHP dão acesso ao Apache inteiro e a tudo que ele tem acesso na máquina.</p>
<p>A segunda gambiarra pra evitar ter que carregar os interpretadores a cada nova requisição foi o FastCGI que é basicamente um segundo servidor que carrega paralelo ao Apache ou outro servidor web, ele mantém o interpretador e seu programa ativos em memória e pode servir múltiplas requisições sem precisar terminar e reiniciar toda vez. Daí o servidor web e o servidor de FastCGI se comunicam via TCP ou outras formas como Unix Sockets que é recurso em Unix para que dois processos consigam se comunicar. Quando uma requisição chega ao Apache ele passa essa informação e outros metadados pro servidor de FastCGI executar o programa Perl ou PHP e devolve a resposta resultante de volta pro Apache devolver pro navegador. Uma grande vantagem disso era poder isolar os dois servidores de forma que um não invada demais o espaço do outro em caso de bugs de segurança.</p>
<p>Mas em Windows, você não tem o conceito de FORK na API de alto nível do sistema, o CreateProcess. Internamente o kernel do Windows permite algo parecido com FORK incluindo o recurso de COW ou Copy on Write, mas não é tão usado e mesmo assim ainda é uma ordem de grandeza mais lento. Isso explica porque os primeiros Apache eram uma porcaria em Windows, nem perto da performance num Linux e por isso dávamos preferência a usar o próprio IIS da Microsoft. Só porque dois programas compilam pra dois sistemas operacionais diferentes e rodam, não significa que rodam igual. Coisas feitas pra Windows funcionam melhor em Windows e coisas feitas pra Linux só rodam bem em Linux. Lembre-se disso. Dada essa limitação você acaba sendo obrigado a usar mais Threads que são linhas de execução em paralelo dentro de um mesmo processo.</p>
<p>Já falamos da discussão de compilação estática vs compilação dinâmica. É hora de falar de outro dilema da programação: programação baseada em processos com fork versus um processo com múltiplas threads. Hoje em dia usamos muito mais Threads mas processos com fork não estão obsoletos, por exemplo, o conector do banco de dados PostgreSQL funciona via fork assim como muitos outros softwares. Como eu disse antes, toda vez que você executa um programa o sistema operacional carrega os bits em memória e inicia uma entidade chamada processo. O sistema operacional tem o poder de matar esse processo ou pausar e reiniciar sua execução. Dentro do processo ele não tem consciência de todos os outros processos ao seu redor a menos que tenha privilégios de perguntar ao sistema operacional. Pra todos os efeitos, ele se sente como se fosse a única coisa rodando na máquina. Quando o processo precisa escrever na memória ele não escreve diretamente.</p>
<p>Pense na memória como um livro com páginas numeradas sequencialmente de 1 até um número grande. Num computador de 32-bits temos a capacidade de mapear de 1 até 2 elevado a 32 ou seja mais de 4 bilhões ou 4GB. Num computador de 64-bits temos a capacidade de mapear até 2 elevado a 64 ou seja mais de 18  quintilhões de bytes ou 18 exabytes, que é bastante coisa. Se tem vários programas na memória você imaginaria que o certo seria cada programa começar a partir de uma determinada página dessa memória, tipo o Word começando na página 1, o Excel na página 100, o Chrome na página 200 e assim por diante, mas isso seria extremamente complicado de controlar. Em vez disso todo mundo começa na página 1, mas de uma memória virtual e o sistema operacional que se encarrega de traduzir a página 1 da memória virtual do Excel pra página 100 da memória real. Então o Excel nunca teria como acessar a memória do Chrome. E assim começamos a criar um mínimo de segurança entre os programas. No MS-DOS antigo que rodava no que chamamos de modo real, cada programa tem acesso a todos os endereços reais da máquina, mas não tinha problema porque no MS-DOS só dava pra rodar um programa de cada vez. Pra trocar do Word pro Excel, tinha que primeiro sair do Word e daí carregar o Excel. Mas hoje em dia onde vários programas.</p>
<p>Isso facilita muito pra nós programadores porque não precisamos nos preocupar com a memória de terceiros. E se precisamos fazer algo como um servidor Web onde cada conexão de um cliente TCP precisa rodar em paralelo a outras conexões, podemos fazer só um FORK do processo pra cada um. Cada FORK é um processo isolado e daí um não afeta a memória do outro. Tudo funciona perfeitamente. Mas se é caro,  ou não é possível fazer FORKS, a única outra solução é dentro de um único processo iniciarmos múltiplas Threads, que são bem mais baratas que forks. Cada Thread daí pode servir um cliente TCP conectado ao processo. Mas agora começamos a ter problemas. Porque não existe memória isolada pra cada thread, dentro do processo cada thread tem acesso a toda a memória virtual do processo. Por isso você vai ouvir falar, caso ainda não tenha passado por isso, que escrever programas que são multi-thread é uma puta dor de cabeça, porque você precisa garantir o que chamamos de thread-safety ou seja, código seguro pra rodar em threads. Nesse caso, você como programador tem a responsabilidade de escrever código que, quando executado numa thread, não pise sem querer na memória de outra thread executando ao mesmo tempo. A forma de fazer isso normalmente envolve toda vez que você precisa escrever algum dado em memória, primeiro você notifica o sistema que vai escrever, pega o que chamamos de um lock, escreve e depois libera esse lock. Se falhar em pegar o lock, se arrisca a corromper a memória de outra thread. Se esquecer de soltar o lock vai causar um deadlock e bloquear outras threads de pegar o lock. Pense essa rotina em milhares de linhas de código. Ou seja, dor de cabeça.</p>
<p>Recapitulando, um programa binário é executado pelo sistema operacional num espaço de memória isolado, chamado processo. Cada processo pode ser clonado via fork num Linux. E cada processo pode rodar uma ou mais threads dentro dele. Um servidor web é um processo e pra servir múltiplas requisições ele faz um fork pra cada uma. Daí nasce CGI ou FastCGI. Mas em Windows como forks são ordens de grandeza mais caros, o servidor web IIS precisa de outro truque e isso veio na forma do padrão ISAPI que é como se fosse um programa escrito pra CGI mas com modificações pra ser thread-safe e compilado dentro de uma biblioteca DLL que é carregado pelo IIS. O servidor web da Netscape, que não existe mais, tinha outro padrão similar, o NSAPI. Muitos programadores de Delphi já devem ter feito programas pra Web e compilado em DLLs ISAPI ou NSAPI.</p>
<p>Com o advento de coisas como CGI, FastCGI, ISAPI, NSAPI e outras arquiteturas e o fato de termos interpretadores que possibilitam escrever código que tem pouca dificuldade de rodar porque não precisamos nos preocupar com compilação e gerenciamento de dependências, é quando linguagens interpretadas como Perl, PHP e ASP Clássico ganham notoriedade no mercado, especialmente com o timing do início da bolha das ponto coms a partir do meio dos anos 90.</p>
<p>Voltando a threads mais um pouco, como faz pra várias threads rodarem em paralelo? Pense assim, um processo tem pelo menos 1 thread. Pra um processo executar múltiplas threads ao mesmo tempo, o CPU tem uma coisa chamada agendador ou scheduler, mas só pode rodar um número limitado de threads ao mesmo tempo. Então ele pausa uma thread em execução de acordo com vários critérios e acorda outra thread que tava pausada pra dar chance dela rodar e vai fazendo isso com os vários threads de tal maneira que você tem a impressão que estão todos rodando ao mesmo tempo. E quando você pede o tal lock é esse scheduler que vai evitar que outras threads pisem no seu calo por exemplo. Programação multi-thread vai ser uma longa jornada pra vocês que estão iniciando e é até hoje considerada uma das coisas mais difíceis pra um programador realmente entender, só competindo com gerenciamento de memória e segurança.</p>
<p>Entendendo esses conceitos básicos que eu expliquei muito rapidamente, podemos começar a entender coisas como o surgimento do Java. Você precisaria ter tido experiência com C++, Smalltalk e Lisp pra entender realmente como um Java nasce mas vamos começar por aqui. Em 1991 surge uma nova linguagem na Sun com o codenome Oak, e o objetivo inicial era ser um sistema pra set-top-boxes. Naquela época se tinha o conceito que as pessoas teriam “caixas” multimidia nas salas integrando a tv a cabo e outros conteúdos. Esse conceito anos depois viria a se tornar um Tivo ou Roku ou mesmo Google Chromecast e Apple TV de hoje, que inclusive seriam substituídos pelas Smart TVs e consoles de videogame também. Lembrem-se que Linux ainda estava só no começo, ainda era rudimentar, e coisas como Android estão muitos anos à frente ainda.</p>
<p>Java embute alguns conceitos de compiladores e interpretadores. Mais especificamente de máquinas virtuais, por isso você tem a JVM ou Java Virtual Machine. Uma máquina virtual é uma evolução de um interpretador. Em vez de ser só um programa que executa código escrito numa determinada linguagem, como Java, a JVM tem uma ambição maior, ele quer ser o próprio sistema operacional e abstrair a máquina real por baixo. Pro programa em Java não existe um sistema operacional Linux ou Windows rodando num processador Intel ou ARM. Só existe a JVM.</p>
<p>O conjunto todo que a Sun inventou tem várias ferramentas. Diferente de um interpretador e mais similar a uma linguagem compilada como C++, ele separa a fase de compilação da fase de execução. O compilador JAVAC pega seu código fonte escrito em Java (eles usaram o mesmo nome pra tudo e isso dificulta explicar, eu sei) e compila numa representação intermediária que chamamos de Java Byte Code. Na prática, lembra que eu falei que o compilador de C traduz seu código C em instruções nativas do sistema operacional e do processador? E se seu sistema operacional e processador forem a JVM? Também falei que cada processador hardware vem com um conjunto particular de instruções que só roda nesse processador. No caso da JVM é a mesma coisa, é isso que chamamos de bytecode. Então quando o JAVAC compila, ele gera um binário nativo só que específico pra máquina virtual Java. Na época se pesquisou inclusive fazer um processador hardware mesmo que entendia essas instruções mas nunca foi mesmo pra frente. Entenderam? A JVM é como se simulasse um computador.</p>
<p>Assim como um interpretador, você carrega a JVM e aí ele carrega o seu programa, abstraindo o sistema real por baixo. Mas diferente de um interpretador ele faz coisas mais pesadas e tem premissas mais caras. Java é péssimo pra ser algo como substituto de Perl na linha de comando porque a inicialização dele é extremamente lenta até hoje. Isso porque ele tem um gerenciador de memória muito mais sofisticado do que a maioria dos interpretadores e carrega muito mais coisas na inicialização. Por causa disso também ele não é bom pra fazer FORK pra rodar em múltiplos processos, porque o custo desse gerenciador de memória é extremamente alto. Como simulador de um sistema operacional, nesse sentido ele se parece mais com o Windows do que um Linux. Então o caso de uso ideal do Java é rodar um único processo, sozinho na máquina, e seus programas rodam em paralelo dentro da JVM e a JVM substitui o sistema operacional pra gerenciar seus programas rodando em paralelo. E cada programa seu precisa usar Threads pra executar coisas em paralelo dentro dele. Viu porque eu disse que a JVM praticamente substitui o sistema operacional? Você tira o máximo dele quando não roda mais nada além da JVM e dá todos os recursos da máquina pra ele gerenciar.</p>
<p>E eu disse que compiladores como C fazem dezenas de otimizações pra reescrever seu código da maneira que rode mais rápido possível. A JVM vai um passo além, ele fica medindo como seu programa roda e otimiza em tempo real pra binário nativo da máquina por baixo pra ficar mais rápido. É a técnica que chamamos de JIT ou compilador Just In Time. Javascript faz algo semelhante em navegadores modernos como Safari ou Chrome. Um otimizador em tempo de compilação demora Muito pra compilar, se você já tentou compilar um programa manualmente, dependendo do tamanho já viu que pode levar de segundos a minutos. Uma página web não pode demorar tanto pra carregar. Em vez de tentar otimizar tudo, ele só lê o código fonte em Javascript e faz o mínimo pra interpretar e executar, mas à medida que ele executa um compilador JIT vai otimizando seu código em tempo de execução e só o código que está executando. Se você por acaso carregou uma biblioteca que nunca é usada, o compilador JIT nem vai perder tempo tentando otimizar esse pedaço. Então é tipo um meio termo entre um compilador AOT ou Ahead of Time como em C e um interpretador.</p>
<p>A vantagem do Java compilar num binário intermediário de bytecode e rodar numa JVM é que eliminamos o problema de ter que ter compiladores pra cada arquitetura de computador como Intel ou ARM. Basta que cada tipo de computador e sistema operacional tenha uma JVM. Você já deve ter visto isso com programas Java que rodam em Mac ou Linux ou Windows, mas em cada um você precisa baixar um Java específico uma vez só. Além disso o Java cresceu rapidamente e temos quase tudo escrito em Java. Assim como uma das maiores idéia do C era sua portabilidade para diferente arquiteturas, contanto que você recompilasse o código C, no caso do Java eles também queriam portabilidade escrevendo o máximo possível de tudo somente em Java.</p>
<p>Aliás, essa é outra diferença do Java com interpretadores. Em Java, você tem praticamente tudo que um sistema operacional como Windows ou Linux oferecem mas escritos todos em Java e embutidos dentro da JVM. Em interpretadores como Perl ou Python ou PHP ou Ruby eles dependem bastante do sistema operacional por baixo. Interpretadores de código aberto como eles nunca tiveram a ambição de substituir o sistema operacional, mas de se integrar a eles. Pra fazer isso usamos o conceito de binding, que é uma forma do interpretador expor a uma biblioteca de código nativo o acesso a suas estruturas internas e sua memória. Então, em vez de escrever uma biblioteca de criptografia toda em Python, ele simplesmente mapeia para o que o OpenSSL já instalado no sistema operacional oferece. Perl, PHP e Ruby fazem a mesma coisa. Isso inclusive cria um problema: se a biblioteca nativa que você está fazendo binding não for thread-safe, você bloqueia o interpretador de rodar threads em paralelo. Isso acontece o tempo todo em todos os interpretadores.</p>
<p>Por isso também é mais difícil de fazer interpretadores rodarem em todos os sistemas operacionais. Muitas vezes eles rodam, mas algumas funcionalidades são diferentes ou tem qualidade inferior porque as mesmas dependências não existem em todos os sistemas operacionais. Por isso mesmo, nunca faça programas complexos que rodam num interpretador no Windows esperando que vá funcionar igualzinho em Linux ou Mac. Entre Mac e Linux existe mais compatibilidade porque ambos compartilham muitas das mesmas bibliotecas open source. No Windows elas precisam ser remendadas pra se ligar a bibliotecas proprietárias que só existem no Windows. Portanto, se vai codar em linguagens interpretadas como essas, prefira sempre Linux ou Mac. Não é uma questão de ser anti-Microsoft ou algo assim, os interpretadores que foram criados em Linux funcionam bem só em Linux, mesmo que exista uma versão pra Windows.</p>
<p>Macs passaram a ser construídos sobre um UNIX baseado na Kernel do BSD UNIX quando o OS X saiu em 1999, sistema esse que foi uma evolução do NextStep criado no fim dos anos 80 depois que Steve Jobs foi removido da Apple. Ele é um UNIX de verdade, assim como o Solaris da Sun ou o HP-UX. Eles se parecem com distribuições Linux porque o Linux tentou ser um clone de UNIX. E eles são razoavelmente compatíveis porque os UNIX modernos e distribuições Linux usam mais ou menos as mesmas ferramentas gerais, incluindo o mesmo compilador GCC e suas dependências como o GLIBC. Só o Windows que é completamente diferente dentre os principais sistemas operacionais. E o Java nasceu com a promessa de permitir que você pudesse escrever programas que rodavam em qualquer lugar, porque havia JVM para Linux, para Solaris, pra Mac, pra Windows. Por isso ele se popularizou tanto tão rápido. E também coincidiu com o começo da bolha da internet que acelerou ainda mais a adoção de Java.</p>
<p>Vendo isso, a Microsoft não quis perder o bonde. Primeiro, aproveitando a experiência com Visual Basic e Visual C++ que compilavam binários nativos compatíveis com Windows, eles contrataram o Anders Hejlsberg, criador do Turbo Pascal e do Object Pascal do Delphi da Borland. Seu primeiro produto foi o Visual J++ que era uma versão de Java da Microsoft que compilava pra uma JVM proprietária e com várias funcionalidades incompatíveis. Por exemplo, notável era que o Java também tinha um jeito de fazer bindings pra binários nativos chamado JNI mas a no J++ a Microsoft fez uma forma diferente e discutivelmente melhor ou mais performática chamada RMI e o J++ fazia vários atalhos pra se ligar ao Windows de forma mais eficiente. A linguagem tinha a mesma cara do Java mas não era compatível com o Java da Sun. Por conta disso os programas compilados em J++ rodavam de forma mais eficiente só que o bytecode era incompatível com a JVM da Sun e como pra se chamar Java precisava ser compatível a Sun processou a Microsoft. O J++ acabou sendo descontinuado por conta disso mas as suas sementes é que deram origem ao que você conhece como o CLR o Common Language Runtime, ao framework .NET e à linguagem C#. Por isso as primeiras versões de C#, pra mim, tinham cara de Java misturado com Delphi.</p>
<p>Veja, como a JVM executa bytecodes binários, qualquer linguagem pode ser transformada nesses bytecodes. A sintaxe que conhecemos como Java é só um deles. Por isso hoje em dia você tem linguagens como Scala, Clojure e Kotlin que compilam para o mesmo bytecode e rodam na JVM. Mas a Microsoft usou essa característica antes e além de C# já fez também Visual Basic.NET e ambas compilavam pro bytecode do CLR. O CLR ao contrário da JVM fazia muitos bindinds direto pro sistema operacional pra conseguir ter a melhor performance. Portabilidade não era importante porque bastava rodar onde o Windows rodava, e nessa época a Microsoft ainda considera o mundo open source como inimigo, coisa que só veio a mudar nos anos recentes. Essa é a grande diferença entre Java e .NET. Embora ambas sejam máquinas virtuais e os códigos compilem em bytecodes, o Java tinha como meta rodar no maior número de dispositivos quanto possível e o .NET só em Windows.</p>
<p>Mas as coisas foram mudando. Essa ambição da Sun se mostrou muito a frente do seu tempo. A Sun teve muitas ingerências, perdeu valor e foi comprada pela Oracle e desde antes disso com a depressão depois do crash da bolha da internet em 2001 que ela evolui a passos de tartaruga. Em paralelo no mundo open source, surgiu uma iniciativa ousada. A Microsoft abriu a linguagem C# como um padrão aberto Ecma, então Miguel de Icaza, do projeto GNOME decidiu tentar implementar o CLR no Linux, esse projeto recebeu o nome de Mono. Levou quase 10 anos pra conseguir de fato criar uma CLR e todo o framewortk quase totalmente compatível com o da Microsoft. Porque não era só uma questão de fazer a máquina virtual e o compilador, era necessário também reescrever do zero todas as bibliotecas que compõe o pacote .NET framework.
E muitas delas dependem de bindings para bibliotecas nativas do Windows como já falei. Isso foi um dos maiores problemas pra conseguir compatibilidade no Linux. Por exemplo, as bibliotecas para fazer aplicativos desktop, tipo um Word da vida, usam bibliotecas nativas do Windows pra gerar janelas nativas e não janelas diferentes como o Java fazia com seu toolkit Swing. Do jeito do Java o peso era maior e a performance era menor. Mas usando bibliotecas nativas a velocidade do C# e do .NET pra desenhar as mesmas janelas era muito maior e mais comparável com o que o Visual Basic ou Visual C++ anterior faziam. O Mono no Linux fazia bindings com o GNOME pra também conseguir ter performance similar.</p>
<p>Depois de muitos anos e com a troca de CEO com a saída de Steve Balmer e a entrada de Satya Nadella e sua política de boa vizinhança, a Microsoft se tornou finalmente mais amigável ao mundo open source e acabou comprando a empresa de Miguel De Icaza, a Xamarin. E o Mono foi renomeado como .NET Core e hoje está em caminho de substituir o antigo .NET framework. E apesar do CLR suportar múltiplas linguagens na prática todo mundo programa ou em C# ou em Visual Basic.NET mesmo.</p>
<p>E pro episódio de hoje acho que já cobrimos bastante terreno. Eu tentei explicar um pouco sobre como as coisas funcionam por baixo dos panos. A idéia de CPUs, sistemas operacionais, gerenciamento de processos e de memória. As diferenças entre linguagens compiladas de forma estática e dinâmica. O que são interpretadores e o conceito de máquinas virtuais e bytecodes. E também as diferenças de rodar coisas em paralelo com forks e com threads. Os anos 90 foram o terreno para o crescimento de Java e .NET no mundo mais proprietário e em paralelo pra evolução do mundo open source como um todo, do Linux, e linguagens interpretadas como Perl e PHP. Estamos ainda só começando o século XXI. No próximo episódio vamos precisar do que expliquei aqui pra você finalmente começarem a entender as diferenças entre todas as linguagens modernas.</p>
<p>Como eu já disse no episódio anterior, se eu fiz meu trabalho direito espero que vocês tenham ficado com mais dúvidas do que quando começaram a assistir. Mandem suas dúvidas nos comentários, se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, não deixem de assinar o canal e clicar no sininho. Aguardem a parte 2 sobre tecnologias back end na semana que vem, a gente se vê, até mais!</p>
]]></content:encoded><category>backend</category><category>linguagens-de-programacao</category><category>performance</category><category>akitando</category></item><item><title>A Solução de Upload Nativa do Rails 5.2: ActiveStorage</title><link>https://akitaonrails.github.io/2017/07/07/a-solucao-de-upload-nativa-do-rails-5-2-activestorage/</link><guid isPermaLink="true">https://akitaonrails.github.io/2017/07/07/a-solucao-de-upload-nativa-do-rails-5-2-activestorage/</guid><pubDate>Fri, 07 Jul 2017 14:40:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;Atualização 07/11/2017&lt;/strong&gt;: Logo depois do nosso brainstorm no Twitter, DHH e @gauravtiwari se mobilizaram e iniciaram um &lt;a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener"&gt;novo branch&lt;/a&gt; para adicionar suporte a Direct Upload para a nuvem imediatamente. Claro, ainda é trabalho em andamento, mas vai ser ótimo ter esse suporte nativo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Atualização 07/20/2017:&lt;/strong&gt; Preciso esclarecer alguns pontos. Primeiro, estou recomendando a solução proprietária do Cloudinary, mas há ressalvas. Você está trocando facilidade de uso por vendor lock-in. E só recomendo para fotos e imagens. Se precisar fazer upload de arquivos gigantes (vídeos, ou binários arbitrários como tarballs pesados), você precisa pesquisar mais e talvez construir uma solução customizada com Shrine ou similar. Outro ponto que faço é que se você usa Heroku, sua única opção é Direct Upload para serviços de nuvem como o AWS S3. Já entreguei vários apps que bateram no &lt;a href="https://devcenter.heroku.com/articles/request-timeout#uploading-large-files"target="_blank" rel="noopener"&gt;H12 Routing Timeout&lt;/a&gt; — está na documentação oficial. Mas há pouco deployei um Rails 5.1 simples com Carrierwave padrão e um formulário HTML multipart. Consegui fazer upload de um vídeo de mais de 2GB e ele passou pela camada de roteamento do Heroku. Parece que o roteador absorve o arquivo, deixa o upload terminar e só então passa para a aplicação Rails. Agora, se você bloqueia a requisição (por exemplo, fazendo upload do Rails para o S3 ou qualquer operação demorada), a camada de roteamento vai dar H12/timeout após 30 segundos. Então, se você permitir uploads de arquivos grandes, vai acabar tendo desconexões aleatórias. Mesmo que você consiga receber o arquivo, não pode mantê-lo no sistema de arquivos (que é volátil num Dyno do Heroku), então ainda precisa enviá-lo para algum lugar — e essa é a parte que pode dar timeout. Portanto, o Direct Upload continua sendo a abordagem correta no Heroku.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Atualização 07/11/2017</strong>: Logo depois do nosso brainstorm no Twitter, DHH e @gauravtiwari se mobilizaram e iniciaram um <a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener">novo branch</a> para adicionar suporte a Direct Upload para a nuvem imediatamente. Claro, ainda é trabalho em andamento, mas vai ser ótimo ter esse suporte nativo.</p>
<p><strong>Atualização 07/20/2017:</strong> Preciso esclarecer alguns pontos. Primeiro, estou recomendando a solução proprietária do Cloudinary, mas há ressalvas. Você está trocando facilidade de uso por vendor lock-in. E só recomendo para fotos e imagens. Se precisar fazer upload de arquivos gigantes (vídeos, ou binários arbitrários como tarballs pesados), você precisa pesquisar mais e talvez construir uma solução customizada com Shrine ou similar. Outro ponto que faço é que se você usa Heroku, sua única opção é Direct Upload para serviços de nuvem como o AWS S3. Já entreguei vários apps que bateram no <a href="https://devcenter.heroku.com/articles/request-timeout#uploading-large-files"target="_blank" rel="noopener">H12 Routing Timeout</a> — está na documentação oficial. Mas há pouco deployei um Rails 5.1 simples com Carrierwave padrão e um formulário HTML multipart. Consegui fazer upload de um vídeo de mais de 2GB e ele passou pela camada de roteamento do Heroku. Parece que o roteador absorve o arquivo, deixa o upload terminar e só então passa para a aplicação Rails. Agora, se você bloqueia a requisição (por exemplo, fazendo upload do Rails para o S3 ou qualquer operação demorada), a camada de roteamento vai dar H12/timeout após 30 segundos. Então, se você permitir uploads de arquivos grandes, vai acabar tendo desconexões aleatórias. Mesmo que você consiga receber o arquivo, não pode mantê-lo no sistema de arquivos (que é volátil num Dyno do Heroku), então ainda precisa enviá-lo para algum lugar — e essa é a parte que pode dar timeout. Portanto, o Direct Upload continua sendo a abordagem correta no Heroku.</p>
<p>O DHH acabou de anunciar uma funcionalidade nova para o Rails 5.2 que está por vir. É o <a href="https://github.com/rails/activestorage"target="_blank" rel="noopener">ActiveStorage</a>.</p>
<p>Ele deve se tornar a solução padrão para suporte a uploads de arquivos, basicamente substituindo o Paperclip, o Carrierwave, e algumas funcionalidades do Dragonfly e do Shrine (que fazem bem mais do que isso).</p>
<p>Escrevo este post não para apresentar a solução, mas para esclarecer algumas críticas que fiz no Twitter. Acontece que o Twitter é uma plataforma péssima para discussões mais aprofundadas, daí este texto.</p>
<p>A thread original no Twitter pode ser encontrada <a href="https://twitter.com/AkitaOnRails/status/882998977754537984"target="_blank" rel="noopener">aqui</a>.</p>
<p>O processo tem pelo menos 3 pontos importantes a considerar:</p>
<ol>
<li>O upload do arquivo, do usuário final até o seu controller Rails.</li>
<li>Uma etapa opcional de transformação (que você deveria fazer, para redimensionar imagens e servi-las para diferentes dispositivos e resoluções de tela).</li>
<li>Servir o blob do arquivo de volta ao usuário final como uma URL de imagem, por exemplo.</li>
</ol>
<h3>A Etapa de Upload<span class="hx:absolute hx:-mt-20" id="a-etapa-de-upload"></span>
    <a href="#a-etapa-de-upload" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A maioria das soluções simples de upload — como o ActiveStorage e as antigas como o Paperclip original e o Carrierwave numa instalação padrão — basicamente configuram o formulário HTML como multipart e adicionam um campo de arquivo HTML simples. Isso vai enviar o formulário diretamente para uma action do controller Rails, que receberá o arquivo no hash <code>params</code> e você pode lidar com o blob binário a partir daí.</p>
<p>Numa implantação ingênua, isso vai <strong>bloquear</strong> o MRI durante todo o upload. Se o arquivo for muito grande, pode bloquear qualquer outra requisição entrante pela duração desse upload. (Tecnicamente, como o Rails suporta threads MRI e threads MRI são teoricamente não-bloqueantes para operações de IO, não deveria ser tão grave quanto parece.)</p>
<p>Felizmente, acredito que ninguém em sã consciência expõe um processo MRI diretamente para a internet. Normalmente ficamos atrás de um <a href="https://www.digitalocean.com/community/tutorials/how-to-deploy-a-rails-app-with-puma-and-nginx-on-ubuntu-14-04"target="_blank" rel="noopener">proxy reverso</a>, como Haproxy, NGINX, Apache HTTPD ou algo similar.</p>
<p>E geralmente esses proxies reversos são os que recebem o upload e só fazem proxy da requisição quando o upload está concluído. Assim, a aplicação MRI/Rails pode continuar respondendo às outras requisições enquanto isso.</p>
<p>Então, se você tem uma infraestrutura customizada, está bem.</p>
<p>Se você usa algo como Heroku, está basicamente ferrado. A camada de roteamento do Heroku tem um timeout fixo de 30 segundos. Acho isso uma coisa boa, porque você não deveria ter requisições que demoram tanto para processar. Ora, você não deveria ter uma requisição respondendo em 1 segundo, quanto mais 30 segundos.</p>
<p>Mas upload de arquivos é a exceção. Um arquivo grande demora para transferir pela internet. E aí o timeout do Heroku entra em ação e interrompe a transmissão. O usuário tenta de novo, e se você tiver gente suficiente acessando, começa a encher a fila HTTP até ter timeouts em cascata.</p>
<p>É por isso que a única solução disponível para Heroku é fazer &ldquo;Direct Upload&rdquo; para um serviço de nuvem como o AWS S3. Você pode adicionar o add-on <a href="https://github.com/dwilkie/carrierwave_direct"target="_blank" rel="noopener">Carrierwave Direct</a>, ou usar uma solução completa de terceiros como o <a href="http://www.akitaonrails.com/2016/07/28/atualizando-meus-posts-antigos-sobre-uploads"target="_blank" rel="noopener">Cloudinary</a>, com sua biblioteca cliente Attachinary para facilitar as coisas. E pronto!</p>
<p>O Active Storage, como está agora, vai funcionar bem para qualquer deploy customizado razoável (NGINX + Rails/Puma) <del>mas não vai funcionar de jeito nenhum no Heroku</del> e quando o <a href="https://github.com/rails/activestorage/pull/35"target="_blank" rel="noopener">novo branch</a> estiver pronto, será uma boa opção para usar no Heroku também.</p>
<h3>A Etapa de Transformação<span class="hx:absolute hx:-mt-20" id="a-etapa-de-transformação"></span>
    <a href="#a-etapa-de-transforma%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso pode ser feito logo após o upload ou logo antes de servir o arquivo de volta ao usuário.</p>
<p>A primeira opção pode ser feita de forma síncrona ou assíncrona.</p>
<p>Síncrona é &ldquo;ruim&rdquo; (quer dizer, na própria action do controller, porque essa etapa é CPU-intensiva e leva tempo). É basicamente transformar a imagem (usando algo como Rmagick ou MiniMagick) em outras versões de tamanhos diferentes (thumbnail, versão mobile, versão high-dpi, etc.) e armazenar os caminhos para as diferentes versões no storage.</p>
<p>Assíncrona é delegar essa transformação custosa para o ActiveJob, para que um worker do Sidekiq pegue a tarefa mais tarde e faça o processamento. Enquanto isso, você pode servir um placeholder se a versão específica ainda não estiver pronta.</p>
<p>Uma ressalva é que se você tem cloud storage e transformação assíncrona em jobs, vai ter muito tráfego, porque você vai gastar tempo fazendo upload para o cloud storage, depois o job vai ter que baixar de lá, fazer a transformação e fazer novos uploads.</p>
<p>A outra solução é não fazer nenhum processamento e delegar o processamento para ser feito sob demanda. É isso que o <a href="http://cloudinary.com/documentation/image_transformations"target="_blank" rel="noopener">Cloudinary/Attachinary</a> de terceiros faz — você pode fazer transformações customizadas usando parâmetros URI. Ele faz a transformação uma vez e faz cache dos resultados para uso futuro. Um exemplo de URL de transformação de imagem do Cloudinary:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>http://res.cloudinary.com/demo/image/upload/w_400,h_400,c_crop,g_face,r_max/w_200/lady.jpg</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso também é o que você obtém se implementar o <a href="https://github.com/refile/refile"target="_blank" rel="noopener">Refile</a> ou o <a href="https://github.com/janko-m/shrine"target="_blank" rel="noopener">Shrine</a>. Eles adicionam um endpoint Rack à sua aplicação Rails que vai buscar o binário da imagem, executar a transformação de acordo com os parâmetros recebidos na URI, fazer cache da transformação e então enviar o binário.</p>
<h3>A Etapa de Servir os Arquivos<span class="hx:absolute hx:-mt-20" id="a-etapa-de-servir-os-arquivos"></span>
    <a href="#a-etapa-de-servir-os-arquivos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Aqui é fazer sua aplicação Rails servir o blob armazenado. O arquivo pode ser armazenado localmente (ou em um mount NFS remoto) ou na internet a partir de qualquer cloud storage como o AWS S3 (caso em que você simplesmente linka diretamente para o endpoint HTTP deles).</p>
<p>Quando sua aplicação Rails serve um arquivo local, pode apenas enviar um Header especial para o proxy reverso (NGINX ou Apache — <code>X-SendFile</code> ou <code>X-Accel-Redirect</code>, que é a diferença entre <code>send_file</code> e <code>send_data</code> no <code>ActionController::DataStreaming</code>, por sinal) e eles vão servir o arquivo diretamente, evitando bloquear o MRI durante a transferência.</p>
<p>Se está em cloud storage, é ainda mais fácil porque você simplesmente imprime a URL do arquivo diretamente no template HTML e não há processamento nenhum.</p>
<p>Haverá algum processamento se você fizer o Rails ler o blob e fazer streaming diretamente (às vezes isso é necessário porque você tem acesso restrito aos arquivos e não quer arriscar usar uma URL aleatória para o arquivo).</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O DHH está certo: o Basecamp serve muitos arquivos e o ActiveStorage (assim como o Paperclip e o Carrierwave) funciona bem, desde que você tenha um proxy reverso NGINX adequado na frente e tenha adicionado um CDN para fazer cache dos arquivos.</p>
<p>Se você não quer ter que gerenciar seu próprio storage, use um Cloud Storage (AWS S3, Google Cloud, Azure, etc.). O ActiveStorage ou outras soluções vão receber o arquivo no nível do controller Rails e você DEVE usar o ActiveJob para fazer POST do blob para o serviço de nuvem em background — sem bloquear sua aplicação no processo. Mas o trade-off é que se você adicionar transformação assíncrona em jobs, vai acabar tendo que buscar a imagem original do cloud storage para fazer as transformações.</p>
<p>Você deveria fazer transformações nas suas imagens para enviar a imagem no tamanho ideal de volta aos seus usuários. Novamente, se fizer isso na sua aplicação, considere um worker do ActiveJob ou as soluções Rack de transformação em tempo real com cache disponíveis no Refile ou Shrine. Pelo menos como está agora, o ActiveStorage não fornece uma solução para as transformações de imagem.</p>
<p><a href="http://www.akitaonrails.com/2016/07/28/atualizando-meus-posts-antigos-sobre-uploads"target="_blank" rel="noopener">Minha recomendação</a> dos posts anteriores sobre o assunto continua valendo: se é sua primeira vez ou seu negócio está começando, não se estresse. Use uma solução completa como Cloudinary/Attachinary. Ela vai cuidar de tudo da forma mais otimizada possível.</p>
<p>Mas isso NÃO é uma recomendação definitiva. Se você tem deploys customizados e conhece seus requisitos e restrições, uma solução como ActiveStorage, Carrierwave vanilla, etc. funciona bem. Sempre há trade-offs, e ter muitas partes móveis sempre soma complexidade. A recomendação do Cloudinary é apenas para que você possa começar com o mínimo de partes móveis possível e depois migrar para cenários mais complexos se precisar.</p>
<p>Lidar com todas as combinações possíveis de gerenciamento de upload de arquivos não é tarefa simples. E você provavelmente tem preocupações mais urgentes na sua lógica de negócios do que lidar com arquivos.</p>
<p>Ah, e repetindo o mesmo de sempre: use um CDN, pelo amor de Deus! Independentemente da solução, você sempre deveria <a href="https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn"target="_blank" rel="noopener">adicionar um CDN</a> para servir seus assets (javascripts, stylesheets, imagens, etc.).</p>
]]></content:encoded><category>rails</category><category>backend</category><category>cloud</category></item><item><title>Ex Pusher Lite - Parte 3 - Uma Solução Completa</title><link>https://akitaonrails.github.io/2016/12/30/ex-pusher-lite-parte-3-uma-solucao-completa/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/12/30/ex-pusher-lite-parte-3-uma-solucao-completa/</guid><pubDate>Fri, 30 Dec 2016 17:38:00 -0200</pubDate><description>&lt;p&gt;Já faz mais de um ano desde que escrevi as 2 partes sobre o meu conceito &amp;ldquo;Ex Pusher Lite&amp;rdquo;. O código de um ano atrás já está obsoleto, pois eu ainda estava aprendendo Elixir e Phoenix na prática.&lt;/p&gt;
&lt;p&gt;Este mês publiquei um artigo sobre &lt;a href="http://www.akitaonrails.com/2016/12/06/coherence-e-exadmin-devise-e-activeadmin-para-phoenix"target="_blank" rel="noopener"&gt;ExAdmin e Coherence&lt;/a&gt; e outro sobre &lt;a href="http://www.akitaonrails.com/2016/12/23/aplicativo-elixir-phoenix-deploy-balanceamento-carga-digitalocean"target="_blank" rel="noopener"&gt;Deploy de Elixir no DigitalOcean&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A ideia é bem simples: é uma homenagem ao &lt;a href="https://pusher.com/"target="_blank" rel="noopener"&gt;Pusher&lt;/a&gt;. Se você já usou o Pusher, isso é muito parecido (embora com bem menos funcionalidades, claro).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Já faz mais de um ano desde que escrevi as 2 partes sobre o meu conceito &ldquo;Ex Pusher Lite&rdquo;. O código de um ano atrás já está obsoleto, pois eu ainda estava aprendendo Elixir e Phoenix na prática.</p>
<p>Este mês publiquei um artigo sobre <a href="http://www.akitaonrails.com/2016/12/06/coherence-e-exadmin-devise-e-activeadmin-para-phoenix"target="_blank" rel="noopener">ExAdmin e Coherence</a> e outro sobre <a href="http://www.akitaonrails.com/2016/12/23/aplicativo-elixir-phoenix-deploy-balanceamento-carga-digitalocean"target="_blank" rel="noopener">Deploy de Elixir no DigitalOcean</a>.</p>
<p>A ideia é bem simples: é uma homenagem ao <a href="https://pusher.com/"target="_blank" rel="noopener">Pusher</a>. Se você já usou o Pusher, isso é muito parecido (embora com bem menos funcionalidades, claro).</p>
<p>Montei uma solução completa inspirada no Pusher, usando o framework Phoenix, deployada no Digital Ocean — e você já pode testar agora mesmo, é só se cadastrar em <a href="http://expusherlite.cm42.io"target="_blank" rel="noopener">expusherlite.cm42.io</a>.</p>
<p>Depois do cadastro, você terá um token secreto (não compartilhe, claro) e estará vinculado a uma Organization. A partir daí pode criar Applications dentro dessa Organization. Cada Application terá um token único para identificá-la.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/576/expusherlite_cm42_io_registrations.png" alt="dashboard"  loading="lazy" /></p>
<p>Suponha que você queira criar uma aplicação Rails com uma funcionalidade de Chat. Qualquer versão de Rails serve — não precisa ser 5.0 e nem precisa de ActionCable.</p>
<p>Primeiro, vamos configurar o <code>config/secrets.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>development:
  secret_key_base: b9a1...e7aa
  pusher_host: &#34;expusherlite.cm42.io&#34;
  org_id: acme-inc
  app_key: 0221...f193
  secret_token: 4036...f193
...
production:
  secret_key_base: &lt;%= ENV[&#34;SECRET_KEY_BASE&#34;] %&gt;
  pusher_host: &lt;%= ENV[&#39;PUSHER_LITE_HOST&#39;] %&gt;
  org_id: &lt;%= ENV[&#39;PUSHER_LITE_ORGANIZATION&#39;] %&gt;
  app_key: &lt;%= ENV[&#34;PUSHER_LITE_APP_KEY&#34;] %&gt;
  secret_token: &lt;%= ENV[&#34;PUSHER_LITE_SECRET_TOKEN&#34;] %&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Substitua <code>pusher_host</code>, <code>org_id</code>, <code>app_key</code> e <code>secret_token</code> pelos valores que você criou antes.</p>
<p>Agora quero adicionar um <code>PageController</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;net/http&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;uri&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PageController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">index</span>
</span></span><span class="line"><span class="cl">    <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="s2">&#34;http://</span><span class="si">#{</span><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_host</span><span class="si">}</span><span class="s2">/api/sessions&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">post_form</span><span class="p">(</span><span class="n">uri</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;token&#34;</span> <span class="o">=&gt;</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">secret_token</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@guardian_token</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span><span class="o">[</span><span class="s2">&#34;jwt&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span> <span class="vi">@guardian_token</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Se você estiver conectando ao meu servidor online, é obrigatório usar SSL — troque a URL acima por &ldquo;https&rdquo;)</p>
<p>O que esse trecho faz é enviar o token secreto pelo lado do servidor, para o meu serviço, e receber de volta um JSON Web Token (JWT). Aí você passa esse JWT para o front-end para habilitar a autenticação.</p>
<p>No front-end podemos ter essa <code>app/views/page/index.html.erb</code> bem simples:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;h1&gt;Ex Pusher Lite - Rails Integration Example&lt;/h1&gt;

&lt;script type=&#34;text/javascript&#34; charset=&#34;utf-8&#34;&gt;
  window.guardian_token = &#34;&lt;%= @guardian_token %&gt;&#34;;
  window.org_id = &#34;&lt;%= Rails.application.secrets.org_id %&gt;&#34;
  window.app_key = &#34;&lt;%= Rails.application.secrets.app_key %&gt;&#34;;
  window.pusher_host = &#34;&lt;%= Rails.application.secrets.pusher_host %&gt;&#34;;
&lt;/script&gt;

&lt;div id=&#34;chat&#34; class=&#34;fixedContainer&#34;&gt;
&lt;/div&gt;

&lt;input type=&#34;text&#34; name=&#34;name&#34; id=&#34;name&#34; value=&#34;&#34; placeholder=&#34;Name&#34;/&gt;
&lt;input type=&#34;text&#34; name=&#34;message&#34; id=&#34;message&#34; value=&#34;&#34; placeholder=&#34;Message&#34;/&gt;
&lt;input type=&#34;checkbox&#34; name=&#34;channel&#34; id=&#34;channel&#34; value=&#34;api&#34;/&gt;
&lt;label for=&#34;channel&#34;&gt;send through API&lt;/label&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Bem simples. Podemos ajustar o CSS (<code>app/assets/stylesheets/application.css</code>) só para deixar mais apresentável:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">fixedContainer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">height</span><span class="p">:</span> <span class="mi">250</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding</span><span class="p">:</span><span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="kc">black</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">overflow</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">body</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-family</span><span class="p">:</span> <span class="n">Helvetica</span><span class="p">,</span> <span class="n">Arial</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por último, precisamos carregar o Javascript principal do servidor ExPusherLite. Edite o layout em <code>app/views/layouts/application.html.erb</code> e adicione esta linha logo após a tag de fechamento <code>&lt;/body&gt;</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;http://&lt;%= Rails.application.secrets.pusher_host %&gt;/js/pusher.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora podemos usar esse Javascript no <code>app/assets/javascripts/application.js</code> para ligar tudo. Esta é a parte relevante:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">ready</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">PusherLite</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s2">&#34;pusher_lite&#34;</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PusherLite</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">app_key</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">host</span><span class="o">:</span> <span class="nb">window</span><span class="p">.</span><span class="nx">pusher_host</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">jwt</span><span class="o">:</span> <span class="nb">window</span><span class="p">.</span><span class="nx">guardian_token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">uniqueUserId</span><span class="o">:</span> <span class="s2">&#34;robot&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ssl: true - if you&#39;re connecting to my online server
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">publicChannel</span> <span class="o">=</span> <span class="nx">pusher</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="s2">&#34;general&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">publicChannel</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="s2">&#34;new_message&#34;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">chat</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">chat</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s2">&#34;&lt;p&gt;&lt;strong&gt;&#34;</span> <span class="o">+</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="s2">&#34;&lt;/strong&gt; &#34;</span> <span class="o">+</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">message</span> <span class="o">+</span> <span class="s2">&#34;&lt;/p&gt;&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">chat</span><span class="p">.</span><span class="nx">scrollTop</span><span class="p">(</span><span class="nx">chat</span><span class="p">.</span><span class="nx">prop</span><span class="p">(</span><span class="s2">&#34;scrollHeight&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">pusher</span><span class="p">.</span><span class="nx">joinAll</span><span class="p">();</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Podemos continuar no mesmo arquivo com o Javascript que se conecta ao campo de mensagem, ouvindo o evento de tecla &ldquo;Enter&rdquo; para enviar as mensagens:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">message_element</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#message&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">message_element</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;keypress&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">keyCode</span> <span class="o">!=</span> <span class="mi">13</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">name_element</span>    <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#name&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">check_element</span>   <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#channel&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">name</span><span class="o">:</span> <span class="nx">name_element</span><span class="p">.</span><span class="nx">val</span><span class="p">(),</span> <span class="nx">message</span><span class="o">:</span> <span class="nx">message_element</span><span class="p">.</span><span class="nx">val</span><span class="p">()</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">check_element</span><span class="p">.</span><span class="nx">prop</span><span class="p">(</span><span class="s2">&#34;checked&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">sendPusher</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">sendAPI</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">message_element</span><span class="p">.</span><span class="nx">val</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">window</span><span class="p">.</span><span class="nx">publicChannel</span> <span class="o">=</span> <span class="nx">publicChannel</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E é assim que enviamos mensagens para o ExPusherLite, seja diretamente via WebSockets full-duplex:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">sendPusher</span><span class="p">(</span><span class="nx">payload</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;sending through socket&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nb">window</span><span class="p">.</span><span class="nx">publicChannel</span><span class="p">.</span><span class="nx">trigger</span><span class="p">(</span><span class="s1">&#39;new_message&#39;</span><span class="p">,</span> <span class="nx">payload</span> <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou fazendo POST para a API disponível:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>function sendAPI(payload) {
  console.log(&#34;sending through API&#34;)
  $.ajax({
    type : &#39;POST&#39;,
    crossDomain: true,
    url : makeURL(&#34;new_message&#34;),
    headers : { Authorization : &#39;Bearer &#39; &#43; window.guardian_token },
    data : payload,
    success : function(response) {
      console.log(response);
      console.log(&#34;sent through API successfully&#34;);
    },
    error : function(xhr, status, error) {
      console.log(error);
    }
  });
}

function makeURL(event) {
  return &#34;http://&#34; &#43; window.pusher_host &#43; &#34;/api/organizations/&#34; &#43; window.org_id &#43; &#34;/applications/&#34; &#43; window.app_key &#43; &#34;/event/&#34; &#43; event;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Vale dizer que você pode enviar mensagens pela API a partir do lado do servidor também. Especialmente de um processo ActiveJob para manter a aplicação web Rails rápida — e aproveitando para salvar a mensagem no banco de dados ou aplicar filtros.</p>
<p>E é isso! Sua aplicação Rails agora tem WebSockets. Dá pra ter o melhor dos dois mundos.</p>
<p>Se quiser ver o exemplo funcionando, publiquei um app de demo <a href="http://ex-pusher-lite-demo.herokuapp.com/"target="_blank" rel="noopener">no Heroku</a>. É só uma demo, sem autenticação, sem sanitização contra cross-scripting, sem nada.</p>
<p><a href="http://ex-pusher-lite-demo.herokuapp.com/"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/577/Screen_Shot_2016-12-30_at_17.42.22.png" alt="chat demo"  loading="lazy" /></a></p>
<p>Resumindo: é uma aplicação Rails (poderia ser Django, Laravel, ASP.NET MVC, tanto faz) se comunicando via WebSocket + APIs com um cluster Phoenix.</p>
<h3>Próximos Passos<span class="hx:absolute hx:-mt-20" id="próximos-passos"></span>
    <a href="#pr%c3%b3ximos-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Continue acompanhando meu blog (ou meu Twitter em <a href="https://twitter.com/akitaonrails"target="_blank" rel="noopener">@akitaonrails</a>) para os próximos posts.</p>
<p>Ainda estou avaliando se vou abrir o código do ExPusherLite como open source — me avise se tiver interesse.</p>
<p>Também estou pensando em manter os servidores online como um serviço barato. Você pode usar de graça agora para brincar, mas não use em produção ainda. Como estou codificando pesado, vou ficar atualizando os servidores frequentemente, então não há SLA. Me avise se tiver interesse em um serviço assim que mantenha o código aberto para você poder confiar nele.</p>
<p>Ainda faltam recursos importantes, como suporte adequado a SSL, canais criptografados, APIs de Presence melhores e por aí vai — mas o que está disponível agora já cobre a maioria dos casos de uso para WebSockets.</p>
<p>E tem mais: por ser Phoenix, por ser Elixir, por ser Erlang, ganhamos PubSub distribuído &ldquo;de graça&rdquo;. Como expliquei no meu post de <a href="http://www.akitaonrails.com/2016/12/23/aplicativo-elixir-phoenix-deploy-balanceamento-carga-digitalocean"target="_blank" rel="noopener">deploy</a>, é um setup com um servidor em Nova York e outro em Londres, só para demonstrar a natureza distribuída do Erlang.</p>
<p>Foi muito divertido brincar com Elixir nos últimos dias e ver com que rapidez consegui montar uma solução completa como essa. Teve muitos quebra-cabeças que me fizeram coçar a cabeça: como lidar com problemas de cross origin, como fazer os nós se encontrarem pelo deploy via edeliver, descobrir as peças faltantes na transição do exrm para o distillery (que ainda está em andamento na comunidade), etc.</p>
<p>Agora estou bem confortável com o básico, do bootstrap do projeto até o deploy em cenário de cluster. E espero que esse serviço seja útil para mais pessoas.</p>
<p>Como este é possivelmente meu último post do ano: Feliz Ano Novo! E nos vemos em 2017!</p>
]]></content:encoded><category>elixir-e-erlang</category><category>sistemas-distribuidos</category><category>backend</category></item><item><title>Coherence e ExAdmin - Devise e ActiveAdmin para Phoenix</title><link>https://akitaonrails.github.io/2016/12/06/coherence-e-exadmin-devise-e-activeadmin-para-phoenix/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/12/06/coherence-e-exadmin-devise-e-activeadmin-para-phoenix/</guid><pubDate>Tue, 06 Dec 2016 18:18:00 -0200</pubDate><description>&lt;p&gt;Este post é voltado para Rubyistas pesquisando a possibilidade de substituir parte do Ruby e Rails com Elixir e Phoenix.&lt;/p&gt;
&lt;p&gt;Indo direto ao ponto: muitos dos meus apps Rails pequenos começam com 2 add-ons bem simples — Devise para autenticação e ActiveAdmin para gerenciamento básico do banco de dados. A partir daí construo o resto.&lt;/p&gt;
&lt;p&gt;Tanto Elixir quanto Phoenix são alvos em constante movimento hoje em dia, o que dificulta que um conjunto estável de bibliotecas se solidifique direito. Mas acho que estamos finalmente passando da curva dos early adopters.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é voltado para Rubyistas pesquisando a possibilidade de substituir parte do Ruby e Rails com Elixir e Phoenix.</p>
<p>Indo direto ao ponto: muitos dos meus apps Rails pequenos começam com 2 add-ons bem simples — Devise para autenticação e ActiveAdmin para gerenciamento básico do banco de dados. A partir daí construo o resto.</p>
<p>Tanto Elixir quanto Phoenix são alvos em constante movimento hoje em dia, o que dificulta que um conjunto estável de bibliotecas se solidifique direito. Mas acho que estamos finalmente passando da curva dos early adopters.</p>
<p>Um ponto de atrito considerável sempre foi a autenticação de usuários. Muitos puristas vão argumentar que você precisa construir a sua do zero ou usar bibliotecas de baixo nível como o <a href="https://github.com/ueberauth/guardian"target="_blank" rel="noopener">Guardian</a>.</p>
<p>Se você está construindo uma aplicação que só expõe endpoints de API, tudo bem. Mas para um app web completo feito para humanos usarem, essa dificilmente é uma boa escolha. Não vou entrar nessa discussão hoje, pois foge do ponto.</p>
<p>Assumo que você já seguiu os tutoriais tanto do <a href="http://elixir-lang.org/getting-started/introduction.html"target="_blank" rel="noopener">Elixir</a> quanto do <a href="http://www.phoenixframework.org/docs/installation"target="_blank" rel="noopener">Phoenix</a>. Se ainda não fez, vá lá e faça — vai levar uns um ou dois dias para aprender o básico se você já é um Rubyista experiente. Depois volta e lê <a href="/elixir">meus posts sobre Elixir</a> para entender onde ele se destaca em relação ao resto.</p>
<p>Dito isso, vamos começar.</p>
<h3>Coherence (alternativa ao Devise)<span class="hx:absolute hx:-mt-20" id="coherence-alternativa-ao-devise"></span>
    <a href="#coherence-alternativa-ao-devise" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Finalmente encontrei este projeto que está em desenvolvimento intenso há 6 meses chamado <a href="https://github.com/smpallen99/coherence"target="_blank" rel="noopener">Coherence</a>. Para todos os efeitos práticos, ele imita o Devise em quase todos os aspectos. E isso é muito bom para uma série de cenários.</p>
<p>O <a href="https://github.com/smpallen99/coherence/blob/master/README.md"target="_blank" rel="noopener">README</a> deles é bem explicado, então não vou copiar e colar aqui — é só ler lá para colocar no ar. Mas se você quiser testar todas as funcionalidades, pode ajustar o procedimento com esse conjunto de opções na task de instalação do Mix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix coherence.install --full --rememberable --invitable --trackable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Execute <code>mix help coherence.install</code> para ver a descrição de todas as opções.</p>
<p>E se você não for mexer no front-end, pode simplesmente adicionar os links de sign up, sign in e sign out ao layout adicionando o seguinte trecho em <code>web/templates/layout/app.html.eex</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">header</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;header&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">nav</span> <span class="na">role</span><span class="o">=</span><span class="s">&#34;navigation&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">ul</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;nav nav-pills pull-right&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="err">&lt;</span>%= if Coherence.current_user(@conn) do %&gt;
</span></span><span class="line"><span class="cl">        <span class="err">&lt;</span>%= if @conn.assigns[:remembered] do %&gt;
</span></span><span class="line"><span class="cl">          <span class="p">&lt;</span><span class="nt">li</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;color: red;&#34;</span><span class="p">&gt;</span>!!<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">      <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">      <span class="err">&lt;</span>%= YourApp.Coherence.ViewHelpers.coherence_links(@conn, :layout) %&gt;
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;http://www.phoenixframework.org/docs&#34;</span><span class="p">&gt;</span>Get Started<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">nav</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">header</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(A propósito, sempre que você ver <code>YourApp</code> nos trechos de código, troque pelo nome do módulo da sua aplicação.)</p>
<p>Se você se perder na documentação deles, pode consultar o repositório <a href="https://github.com/smpallen99/coherence_demo"target="_blank" rel="noopener">Coherence Demo</a> para ver um exemplo de app Phoenix básico com Coherence já configurado e funcionando. Você vai precisar principalmente cuidar do <code>web/router.ex</code> para criar um pipeline <code>:protected</code> e configurar os escopos adequadamente.</p>
<p>Se fizer corretamente, é isso que você vai ver:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/572/Screen_Shot_2016-12-06_at_17.45.47.png" alt="Coherence Navigation Links"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/573/Screen_Shot_2016-12-06_at_17.45.52.png" alt="Coherence Sign In Form"  loading="lazy" /></p>
<p>Faz tempo que eu não ficava animado com uma simples página de login!</p>
<h3>Ex Admin (alternativa ao ActiveAdmin)<span class="hx:absolute hx:-mt-20" id="ex-admin-alternativa-ao-activeadmin"></span>
    <a href="#ex-admin-alternativa-ao-activeadmin" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O próximo passo que costumo dar é adicionar uma interface de administração simples. Para isso encontrei o <a href="https://github.com/smpallen99/ex_admin"target="_blank" rel="noopener">Ex Admin</a>, que está em desenvolvimento ativo desde pelo menos maio de 2015. É tão parecido com ActiveAdmin que o tema antigo deles vai te fazer esquecer que não está num app Rails.</p>
<p>De novo, é bem direto configurar — basta seguir as instruções do <a href="https://github.com/smpallen99/ex_admin"target="_blank" rel="noopener">README</a> deles.</p>
<p>Com tudo instalado e configurado, você consegue expor o model User na interface Admin rapidamente assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix admin.gen.resource User</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E podemos editar o <code>web/admin/user.ex</code> com o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">ExAdmin</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExAdmin</span><span class="o">.</span><span class="n">Register</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">register_resource</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">index</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">selectable_column</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:id</span>
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:name</span>
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:email</span>
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:last_sign_in_at</span>
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:last_sign_in_ip</span>
</span></span><span class="line"><span class="cl">      <span class="n">column</span> <span class="ss">:sign_in_count</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">show</span> <span class="n">_user</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">attributes_table</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:id</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:name</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:email</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:reset_password_token</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:reset_password_sent_at</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:locked_at</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:unlock_token</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:sign_in_count</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:current_sign_in_at</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:last_sign_in_at</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:current_sign_in_ip</span>
</span></span><span class="line"><span class="cl">        <span class="n">row</span> <span class="ss">:last_sign_in_ip</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">form</span> <span class="n">user</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputs</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">input</span> <span class="n">user</span><span class="p">,</span> <span class="ss">:name</span>
</span></span><span class="line"><span class="cl">        <span class="n">input</span> <span class="n">user</span><span class="p">,</span> <span class="ss">:email</span>
</span></span><span class="line"><span class="cl">        <span class="n">input</span> <span class="n">user</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:password</span>
</span></span><span class="line"><span class="cl">        <span class="n">input</span> <span class="n">user</span><span class="p">,</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:password</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sim, isso é assustadoramente similar ao DSL do ActiveAdmin. Parabéns para o time responsável — e isso mostra bem como o Elixir é adequado para Domain Specific Languages, se você curte isso.</p>
<p>Se você seguiu as instruções do Coherence, ele pede para adicionar um pipeline <code>:protected</code> (um conjunto de plugs) para suas rotas protegidas. Por ora, você pode adicionar a rota <code>/admin</code> para passar por esse pipeline. Para os não iniciados, um &ldquo;plug&rdquo; é similar em conceito a um app Rack, ou mais especificamente, a um middleware do Rails. Mas no Rails temos apenas um pipeline de middlewares. No Phoenix podemos configurar múltiplos pipelines para diferentes conjuntos de rotas (browser e api, por exemplo).</p>
<p>Então podemos adicionar o seguinte ao <code>web/router.ex</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="s2">&#34;/admin&#34;</span><span class="p">,</span> <span class="no">ExAdmin</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pipe_through</span> <span class="ss">:protected</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_routes</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Com essas configurações simples no lugar, você vai terminar com algo assim:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/574/Screen_Shot_2016-12-06_at_17.56.17.png" alt="Ex Admin"  loading="lazy" /></p>
<p>E se ainda não estiver convencido, que tal mudar para o tema antigo deles?</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/575/Screen_Shot_2016-12-06_at_17.56.25.png" alt="ActiveAdmin knockoff"  loading="lazy" /></p>
<p>Caramba! Me sinto em casa, embora eu prefira bastante o tema novo. Mas você poderia substituir seu app baseado em ActiveAdmin por este e seus usuários dificilmente vão notar as pequenas diferenças na interface. O comportamento é basicamente o mesmo.</p>
<p>Se ainda tiver dúvidas sobre como configurar o ExAdmin corretamente, confira o projeto <a href="https://github.com/smpallen99/contact_demo"target="_blank" rel="noopener">Contact Demo</a> deles, onde você encontra um exemplo real.</p>
<h3>Costurando um papel de Admin simples<span class="hx:absolute hx:-mt-20" id="costurando-um-papel-de-admin-simples"></span>
    <a href="#costurando-um-papel-de-admin-simples" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Obviamente, não queremos que todos os usuários autenticados tenham acesso à seção de Admin.</p>
<p>Podemos adicionar um campo boolean simples na tabela de usuários para indicar se um usuário é admin ou não. Você pode alterar sua migration para ficar assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">change</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">create</span> <span class="n">table</span><span class="p">(</span><span class="ss">:users</span><span class="p">,</span> <span class="ss">primary_key</span><span class="p">:</span> <span class="kp">false</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">add</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">add</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="n">add</span> <span class="ss">:admin</span><span class="p">,</span> <span class="ss">:boolean</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E você pode configurar o arquivo <code>priv/repos/seeds.exs</code> para criar 2 usuários, um admin e um guest:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">YourApp</span><span class="o">.</span><span class="n">Repo</span><span class="o">.</span><span class="n">delete_all</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">User</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">YourApp</span><span class="o">.</span><span class="n">User</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">YourApp</span><span class="o">.</span><span class="n">User</span><span class="p">{},</span> <span class="sx">%{name: &#34;Administrator&#34;, email: &#34;admin@example.org&#34;, password: &#34;password&#34;, password_confirmation: &#34;password&#34;, admin: true}</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">|&gt;</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">Repo</span><span class="o">.</span><span class="n">insert!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">YourApp</span><span class="o">.</span><span class="n">User</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">YourApp</span><span class="o">.</span><span class="n">User</span><span class="p">{},</span> <span class="sx">%{name: &#34;Guest&#34;, email: &#34;guest@example.org&#34;, password: &#34;password&#34;, password_confirmation: &#34;password&#34;, admin: false}</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">|&gt;</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">Repo</span><span class="o">.</span><span class="n">insert!</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Como é só um exercício, você pode dropar o banco e recriar assim: <code>mix do ecto.drop, ecto.setup</code>.</p>
<p>O Coherence cuida da <strong>autenticação</strong>, mas precisamos cuidar da <strong>autorização</strong>. Você vai encontrar muitos exemplos online de algo semelhante ao Pundit do Rails, como o <a href="https://github.com/schrockwell/bodyguard"target="_blank" rel="noopener">Bodyguard</a>. Mas neste post vou me limitar a um Plug simples e criar um novo pipeline no Router.</p>
<p>Precisamos criar <code>lib/your_app/plugs/authorized.ex</code> com o seguinte conteúdo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">Plugs</span><span class="o">.</span><span class="n">Authorized</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@behaviour</span> <span class="no">Plug</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">import</span> <span class="no">Plug</span><span class="o">.</span><span class="n">Conn</span>
</span></span><span class="line"><span class="cl">  <span class="n">import</span> <span class="no">Phoenix</span><span class="o">.</span><span class="n">Controller</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">init</span><span class="p">(</span><span class="n">default</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="sx">%{assigns: %{current_user: current_user}}</span> <span class="o">=</span> <span class="n">conn</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">current_user</span><span class="o">.</span><span class="n">admin</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="n">flash_and_redirect</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">flash_and_redirect</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">flash_and_redirect</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_flash</span><span class="p">(</span><span class="ss">:error</span><span class="p">,</span> <span class="s2">&#34;You do not have the proper authorization to do that&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">redirect</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;/&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">halt</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quando um usuário faz login, o Coherence coloca a estrutura do usuário autenticado no <code>conn</code> (uma estrutura Plug.Conn), então podemos fazer pattern match a partir daí.</p>
<p>Agora precisamos criar o pipeline do router em <code>web/router.ex</code> assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">pipeline</span> <span class="ss">:protected_admin</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:accepts</span><span class="p">,</span> <span class="o">[</span><span class="s2">&#34;html&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:fetch_session</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:fetch_flash</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:protect_from_forgery</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:put_secure_browser_headers</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="no">Coherence</span><span class="o">.</span><span class="n">Authentication</span><span class="o">.</span><span class="n">Session</span><span class="p">,</span> <span class="kp">protected</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="no">YourApp</span><span class="o">.</span><span class="n">Plugs</span><span class="o">.</span><span class="n">Authorized</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="s2">&#34;/&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pipe_through</span> <span class="ss">:protected_admin</span>
</span></span><span class="line"><span class="cl">  <span class="n">coherence_routes</span> <span class="ss">:protected_admin</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="s2">&#34;/admin&#34;</span><span class="p">,</span> <span class="no">ExAdmin</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pipe_through</span> <span class="ss">:protected_admin</span>
</span></span><span class="line"><span class="cl">  <span class="n">admin_routes</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O pipeline <code>:protected_admin</code> é exatamente igual ao <code>:protected</code>, mas adicionamos o plug <code>YourApp.Plugs.Authorized</code> recém-criado no final. Depois alteramos o escopo <code>/admin</code> para passar por esse novo pipeline.</p>
<p>E é isso. Se você logar com o usuário <code>guest@example.org</code>, ele será redirecionado para a homepage com uma mensagem dizendo que não está autorizado. Se logar com <code>admin@example.org</code>, terá acesso à interface do ExAdmin em <code>/admin</code>.</p>
<h3>Concluindo<span class="hx:absolute hx:-mt-20" id="concluindo"></span>
    <a href="#concluindo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mesmo que agora seja relativamente simples adicionar Authentication, Administration e Authorization básica, não se engane — a curva de aprendizado ainda é íngreme, mesmo que você seja um desenvolvedor Rails experiente.</p>
<p>Por causa do que está por baixo — a arquitetura OTP, os conceitos de Applications, Supervisors, Workers, etc — não é imediatamente simples entender o que está <strong>realmente</strong> acontecendo. Se não tomar cuidado, bibliotecas como Coherence ou ExAdmin vão te fazer achar que é tão simples quanto Rails.</p>
<p>E não é assim. Elixir é uma criatura completamente diferente. E digo isso de forma alguma de maneira negativa — pelo contrário. Ele é feito para sistemas altamente confiáveis e distribuídos, e exige muito mais conhecimento, paciência e treinamento do programador.</p>
<p>Por outro lado, exatamente porque bibliotecas como o Coherence facilitam bastante o começo, você pode se tornar mais motivado a colocar algo em funcionamento e depois investir mais tempo realmente <strong>entendendo</strong> o que está acontecendo por baixo. Por isso a recomendação é: coloque a mão na massa, consiga aquela gratificação instantânea de ver algo rodando, e então vá refinando seu conhecimento. Vai ser muito mais recompensador assim.</p>
<p>Não vejo o Phoenix como um simples substituto do Rails. Isso seria pouco ambicioso. Vejo ele mais como uma peça para tornar o Elixir o conjunto de tecnologias mais adequado para construir <strong>sistemas altamente escaláveis, altamente confiáveis e altamente distribuídos</strong>. Parar em aplicações web simples não faria jus ao potencial do Elixir.</p>
]]></content:encoded><category>elixir-e-erlang</category><category>backend</category><category>seguranca</category></item><item><title>Consertando o Demo de Chat do DHH no Rails 5</title><link>https://akitaonrails.github.io/2015/12/28/consertando-o-demo-de-chat-do-dhh-no-rails-5/</link><guid isPermaLink="true">https://akitaonrails.github.io/2015/12/28/consertando-o-demo-de-chat-do-dhh-no-rails-5/</guid><pubDate>Mon, 28 Dec 2015 16:11:00 -0200</pubDate><description>&lt;p&gt;Pois é, o Rails 5.0.0 Beta 1 &lt;a href="http://weblog.rubyonrails.org/2015/12/18/Rails-5-0-beta1/"target="_blank" rel="noopener"&gt;acabou de ser lançado&lt;/a&gt; e a grande novidade é o &lt;a href="https://github.com/rails/rails/tree/master/actioncable"target="_blank" rel="noopener"&gt;Action Cable&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Basicamente é uma solução completa em cima do Rails padrão para você implementar aplicações baseadas em WebSocket (os famosos chats e notificações em tempo real) com acesso total ao que o Rails te dá (models, view templates, etc). Para apps pequenos e médios, é uma solução excelente que você pode usar no lugar de ter que partir para Node.js.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Pois é, o Rails 5.0.0 Beta 1 <a href="http://weblog.rubyonrails.org/2015/12/18/Rails-5-0-beta1/"target="_blank" rel="noopener">acabou de ser lançado</a> e a grande novidade é o <a href="https://github.com/rails/rails/tree/master/actioncable"target="_blank" rel="noopener">Action Cable</a>.</p>
<p>Basicamente é uma solução completa em cima do Rails padrão para você implementar aplicações baseadas em WebSocket (os famosos chats e notificações em tempo real) com acesso total ao que o Rails te dá (models, view templates, etc). Para apps pequenos e médios, é uma solução excelente que você pode usar no lugar de ter que partir para Node.js.</p>
<p>Resumindo, você controla Cable Channels que recebem mensagens enviadas por uma conexão WebSocket no cliente. O novo gerador de Channel cuida do boilerplate e você só precisa preencher os espaços em branco para definir que tipo de mensagem quer enviar do cliente, o que quer transmitir do servidor, e para quais channels seus clientes estão inscritos.</p>
<p>Para uma introdução mais detalhada, o próprio DHH publicou um <a href="https://www.youtube.com/watch?v=n0WUjGkDFS0"target="_blank" rel="noopener">screencast bem básico de Action Cable</a> que você deveria assistir só para ter uma ideia do porquê de toda essa empolgação. Se você já assistiu e tem alguma experiência em programação, talvez já tenha percebido o problema que menciono no título, então pula direto para a seção <a href="#the-problem">&ldquo;O Problema&rdquo;</a> abaixo para o TL;DR.</p>
<p>No final você acaba com uma base de código como a que reproduzi no meu repositório do Github até a <a href="https://github.com/akitaonrails/rails5-actioncable-demo/tree/end_of_dhh"target="_blank" rel="noopener">tag &ldquo;end_of_dhh&rdquo;</a>. Você vai ter um chat em tempo real (bem) básico de uma única sala para brincar com os principais componentes.</p>
<p>Vamos só listar os principais componentes aqui. Primeiro, você terá o servidor ActionCable montado no arquivo &ldquo;routes.rb&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/routes.rb</span>
</span></span><span class="line"><span class="cl"><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">routes</span><span class="o">.</span><span class="n">draw</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">root</span> <span class="ss">to</span><span class="p">:</span> <span class="s1">&#39;rooms#show&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Serve websocket cable requests in-process</span>
</span></span><span class="line"><span class="cl">  <span class="n">mount</span> <span class="no">ActionCable</span><span class="o">.</span><span class="n">server</span> <span class="o">=&gt;</span> <span class="s1">&#39;/cable&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Este é o componente principal do servidor, o channel:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/channels/room_channel.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RoomChannel</span> <span class="o">&lt;</span> <span class="no">ApplicationCable</span><span class="o">::</span><span class="no">Channel</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">subscribed</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream_from</span> <span class="s2">&#34;room_channel&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">unsubscribed</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Any cleanup needed when channel is unsubscribed</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">speak</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Message</span><span class="o">.</span><span class="n">create!</span> <span class="ss">content</span><span class="p">:</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;message&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois você tem o Javascript boilerplate:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/assets/javascripts/cable.coffee</span>
</span></span><span class="line"><span class="cl"><span class="c1">#= require action_cable</span>
</span></span><span class="line"><span class="cl"><span class="c1">#= require_self</span>
</span></span><span class="line"><span class="cl"><span class="c1">#= require_tree ./channels</span>
</span></span><span class="line"><span class="cl"><span class="c1">#</span>
</span></span><span class="line"><span class="cl"><span class="vi">@App</span> <span class="o">||=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="no">App</span><span class="o">.</span><span class="n">cable</span> <span class="o">=</span> <span class="no">ActionCable</span><span class="o">.</span><span class="n">createConsumer</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E os principais hooks de Websocket no lado do cliente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/assets/javascripts/channels/room.coffee</span>
</span></span><span class="line"><span class="cl"><span class="no">App</span><span class="o">.</span><span class="n">room</span> <span class="o">=</span> <span class="no">App</span><span class="o">.</span><span class="n">cable</span><span class="o">.</span><span class="n">subscriptions</span><span class="o">.</span><span class="n">create</span> <span class="s2">&#34;RoomChannel&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">connected</span><span class="p">:</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Called when the subscription is ready for use on the server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="ss">disconnected</span><span class="p">:</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Called when the subscription has been terminated by the server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="ss">received</span><span class="p">:</span> <span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="err">$</span><span class="p">(</span><span class="s1">&#39;#messages&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">append</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;message&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="ss">speak</span><span class="p">:</span> <span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@perform</span> <span class="s1">&#39;speak&#39;</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">message</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">$</span><span class="p">(</span><span class="n">document</span><span class="p">)</span><span class="o">.</span><span class="n">on</span> <span class="s2">&#34;keypress&#34;</span><span class="p">,</span> <span class="s2">&#34;[data-behavior~=room_speaker]&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">event</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">event</span><span class="o">.</span><span class="n">keyCode</span> <span class="n">is</span> <span class="mi">13</span>
</span></span><span class="line"><span class="cl">    <span class="no">App</span><span class="o">.</span><span class="n">room</span><span class="o">.</span><span class="n">speak</span> <span class="n">event</span><span class="o">.</span><span class="n">target</span><span class="o">.</span><span class="n">value</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">target</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">event</span><span class="o">.</span><span class="n">preventDefault</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O view template é um HTML básico só para amarrar um formulário simples e uma div para listar as mensagens:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/rooms/show.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Chat room<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;messages&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="err">&lt;</span>%= render @messages %&gt;
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;</span>Say something:<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">data-behavior</span><span class="o">=</span><span class="s">&#34;room_speaker&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><a name="the-problem"></a></p>
<h3>O Problema<span class="hx:absolute hx:-mt-20" id="o-problema"></span>
    <a href="#o-problema" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No &ldquo;RoomChannel&rdquo;, você tem o método &ldquo;<tt>speak</tt>&rdquo; que salva uma mensagem no banco de dados. Isso já é um sinal vermelho para uma ação de WebSocket, que deveria ter processamento curto e leve. Salvar no banco é considerado pesado, principalmente sob carga. Se isso for processado dentro do reactor loop do EventMachine, vai travar o loop e impedir que outro processamento concorrente aconteça até o banco soltar o lock.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/channels/room_channel.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RoomChannel</span> <span class="o">&lt;</span> <span class="no">ApplicationCable</span><span class="o">::</span><span class="no">Channel</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">speak</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">Message</span><span class="o">.</span><span class="n">create!</span> <span class="ss">content</span><span class="p">:</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;message&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Eu diria que tudo que entra dentro do channel deveria ser assíncrono!</p>
<p>Para piorar a situação, é isso que você tem dentro do próprio model &ldquo;Message&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Message</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl">  <span class="n">after_create_commit</span> <span class="p">{</span> <span class="no">MessageBroadcastJob</span><span class="o">.</span><span class="n">perform_later</span> <span class="nb">self</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Um callback de model (fuja desses como da peste!!) para transmitir a mensagem recebida para os clientes Websocket inscritos como um ActiveJob, que se parece com isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MessageBroadcastJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">ActionCable</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">broadcast</span>  <span class="s1">&#39;room_channel&#39;</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">render_message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">private</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">render_message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">ApplicationController</span><span class="o">.</span><span class="n">renderer</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="ss">partial</span><span class="p">:</span> <span class="s1">&#39;messages/message&#39;</span><span class="p">,</span> <span class="ss">locals</span><span class="p">:</span> <span class="p">{</span> <span class="ss">message</span><span class="p">:</span> <span class="n">message</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ele renderiza o trecho de HTML para mandar de volta para os clientes Websocket adicionarem ao DOM dos seus navegadores.</p>
<p>O DHH ainda chega a dizer <em>&ldquo;Eu queria mostrar isso porque é assim que a maioria dos apps vai acabar ficando.&rdquo;</em></p>
<p>E de fato, o <strong>problema</strong> é justamente que a maioria das pessoas vai seguir esse padrão e cair nessa armadilha. Então, qual a solução então?</p>
<h3>A Solução Correta<span class="hx:absolute hx:-mt-20" id="a-solução-correta"></span>
    <a href="#a-solu%c3%a7%c3%a3o-correta" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Só para os fins de um screencast simples, vamos fazer um conserto rápido.</p>
<p>Antes de tudo, se for possível, você quer que o código do seu channel bloqueie o mínimo possível. Esperar uma operação bloqueante no banco (gravação) definitivamente não é uma dessas operações. O Job está sendo subutilizado, ele deveria ser chamado direto do método &ldquo;speak&rdquo; do channel, assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/channels/room_channel.rb</span>
</span></span><span class="line"><span class="cl"> <span class="k">class</span> <span class="nc">RoomChannel</span> <span class="o">&lt;</span> <span class="no">ApplicationCable</span><span class="o">::</span><span class="no">Channel</span>
</span></span><span class="line"><span class="cl">   <span class="o">...</span>
</span></span><span class="line"><span class="cl">   <span class="k">def</span> <span class="nf">speak</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="no">Message</span><span class="o">.</span><span class="n">create!</span> <span class="ss">content</span><span class="p">:</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;message&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="no">MessageBroadcastJob</span><span class="o">.</span><span class="n">perform_later</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;message&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</span>
</span></span><span class="line"><span class="cl"> <span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Depois, movemos a gravação no model para dentro do próprio Job:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/jobs/message_broadcast_job.rb</span>
</span></span><span class="line"><span class="cl"> <span class="k">class</span> <span class="nc">MessageBroadcastJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">   <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="no">ActionCable</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">broadcast</span>  <span class="s1">&#39;room_channel&#39;</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">render_message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="n">message</span> <span class="o">=</span> <span class="no">Message</span><span class="o">.</span><span class="n">create!</span> <span class="ss">content</span><span class="p">:</span> <span class="n">data</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="no">ActionCable</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">broadcast</span> <span class="s1">&#39;room_channel&#39;</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">render_message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</span>
</span></span><span class="line"><span class="cl">   <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E finalmente, removemos aquele callback horrível do model e deixamos ele limpo de novo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/models/message.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Message</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso retorna rápido, joga o processamento para um job em background e deveria suportar mais concorrência de saída. A solução anterior, do DHH, tem um gargalo embutido no método speak e vai engasgar assim que o banco virar o gargalo.</p>
<p>Está longe de ser uma solução perfeita ainda, mas é menos terrível para um demo bem rápido e o código fica até mais simples. Você pode conferir esse código no <a href="https://github.com/akitaonrails/rails5-actioncable-demo/commit/0aaaaecc46ed14e98086bac5ce087df08d557456"target="_blank" rel="noopener">meu commit no Github</a>.</p>
<p>Posso estar errado na conclusão de que o channel vai bloquear ou se isso é mesmo prejudicial para a concorrência. Não medi as duas soluções, é só uma intuição vinda de feridas antigas. Se você tem mais conhecimento sobre a implementação do Action Cable, deixa um comentário aí embaixo.</p>
<p>Aliás, tenha cuidado antes de pensar em migrar seu app de Rails 4.2 para Rails 5 já. Por causa das dependências hard coded em Faye e Eventmachine, o Rails 5 hoje exclui o Unicorn (até o Thin parece estar tendo problema para subir). Também exclui JRuby e MRI no Windows por causa do Eventmachine.</p>
<p>Se você quer as capacidades do Action Cable sem precisar migrar, pode usar soluções como o <a href="http://developers.planningcenteronline.com/2014/09/23/live-updating-rails-with-react.js-and-pusher.html"target="_blank" rel="noopener">&ldquo;Pusher.com&rdquo;</a>, ou se quer sua própria solução in-house, acompanhe minha evolução no assunto com o meu <a href="http://www.akitaonrails.com/pusher"target="_blank" rel="noopener">mini clone do Pusher</a> escrito em Elixir.</p>
]]></content:encoded><category>rails</category><category>backend</category><category>sistemas-distribuidos</category></item><item><title>Usando ETAG e Memcached</title><link>https://akitaonrails.github.io/2010/07/06/usando-etag-e-memcached/</link><guid isPermaLink="true">https://akitaonrails.github.io/2010/07/06/usando-etag-e-memcached/</guid><pubDate>Tue, 06 Jul 2010 05:43:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;Atualização 02/07/2010:&lt;/strong&gt; Eu esqueci que o máximo de caracteres de uma chave de memcached é 250. Como estava gerando chaves dos posts usando os permalinks, obviamente em muitos casos vai dar mais de 250. Então o que eu fiz foi gerar as chaves normalmente, criar um digest SHA1 e truncar até 250. Isso deve resolver. Descobri isso porque estou usando o plugin &lt;a href="http://github.com/rails/exception_notification"target="_blank" rel="noopener"&gt;exception_notifier&lt;/a&gt; e hoje comecei a receber dezenas de e-mails com a exception &lt;tt&gt;Memcached::ABadKeyWasProvidedOrCharactersOutOfRange&lt;/tt&gt;. Fica a dica :-)&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Atualização 02/07/2010:</strong> Eu esqueci que o máximo de caracteres de uma chave de memcached é 250. Como estava gerando chaves dos posts usando os permalinks, obviamente em muitos casos vai dar mais de 250. Então o que eu fiz foi gerar as chaves normalmente, criar um digest SHA1 e truncar até 250. Isso deve resolver. Descobri isso porque estou usando o plugin <a href="http://github.com/rails/exception_notification"target="_blank" rel="noopener">exception_notifier</a> e hoje comecei a receber dezenas de e-mails com a exception <tt>Memcached::ABadKeyWasProvidedOrCharactersOutOfRange</tt>. Fica a dica :-)</p>
<p>Aproveitando o <a href="http://www.akitaonrails.com/2010/07/05/rubyconf-latin-america-derruba-akitaonrails-com"target="_blank" rel="noopener">episódio</a> de ontem da lentidão do meu site, resolvi fazer um pequeno ajuste adicionando memcached à equação.</p>
<p>Recordando, eu estou usando ETAGs para economizar processamento. Leia meu <a href="http://akitaonrails.com/2010/05/25/voce-ja-esta-usando-etags-certo"target="_blank" rel="noopener">artigo sobre ETAG</a> para entender do que isso se trata. Basicamente a primeira vez ele vai ao banco, busca os dados, abre o template ERB, gera o HTML e envia de volta ao usuário, o caminho padrão. Com ETAG, na segunda vez ele checa que o dado no banco não mudou e devolve apenas um cabeçalho “304 Not Modified”, evitando o processamento do template ERB e transporte do HTML. Só isso dá uma boa acelerada na requisição.</p>
<p>Porém, eu gero o ETAG usando o campo ‘updated_at’, ou seja, eu preciso acabar indo ao banco e buscar essa informação. Algo parecido com isso:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">show</span>  
</span></span><span class="line"><span class="cl"> <span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">find_by_permalink</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="o">[</span><span class="ss">:year</span><span class="p">,</span> <span class="ss">:month</span><span class="p">,</span> <span class="ss">:day</span><span class="p">,</span> <span class="ss">:slug</span><span class="o">].</span><span class="n">collect</span> <span class="p">{</span><span class="o">|</span><span class="n">x</span><span class="o">|</span> <span class="n">params</span><span class="o">[</span><span class="n">x</span><span class="o">]</span> <span class="p">}</span> <span class="o">&lt;&lt;</span> <span class="p">{</span><span class="ss">:include</span> <span class="o">=&gt;</span> <span class="o">[</span><span class="ss">:tags</span><span class="o">]</span><span class="p">}))</span>  
</span></span><span class="line"><span class="cl"> <span class="n">etag</span> <span class="o">=</span> <span class="vi">@post</span><span class="o">.</span><span class="n">updated_at</span><span class="o">.</span><span class="n">to_i</span>  
</span></span><span class="line"><span class="cl"> <span class="n">fresh_when</span><span class="p">(</span> <span class="ss">:etag</span> <span class="o">=&gt;</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:public</span> <span class="o">=&gt;</span> <span class="kp">true</span> <span class="p">)</span> <span class="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>  
</span></span><span class="line"><span class="cl"><span class="k">end</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O método <tt>find_by_permalink</tt> é específico do meu blog (veja o código do <a href="http://github.com/xaviershay/enki"target="_blank" rel="noopener">Enki</a> para referência). Daí uso o método <tt>fresh_when</tt> para gerar o cabeçalho caso necessário. O ponto é: ele vai executar o <tt>find</tt> ao banco em <strong>todas</strong> as requisições. No caso que aconteceu ontem, com muitas requisições simultâneas, isso pode se tornar um ponto de contenção crítico, mesmo se a query for muito rápida.</p>
<h2>Memcached<span class="hx:absolute hx:-mt-20" id="memcached"></span>
    <a href="#memcached" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A solução mais simples que eu pensei foi em colocar o memcached na frente disso. Num Ubuntu para instalar o daemon basta fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get install memcached libsasl2-dev  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<ul>
<li></li>
</ul>
<p>E no Mac, se você estiver usando o <a href="http://github.com/mxcl/homebrew"target="_blank" rel="noopener">Homebrew</a>, basta fazer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install Memcached</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Daí precisa instalar a gem:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gem install memcached  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora no <tt>config/environment.rb</tt> coloque:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ruby</span> <span class="n">config</span><span class="o">.</span><span class="n">gem</span> <span class="s1">&#39;memcached&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">…</span>  
</span></span><span class="line"><span class="cl"><span class="k">end</span>  
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;digest/sha1&#39;</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E no seu <tt>config/environments/production.rb</tt> coloque:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;memcached&#39;</span>  
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">cache_store</span> <span class="o">=</span> <span class="ss">:mem_cache_store</span><span class="p">,</span> <span class="no">Memcached</span><span class="o">::</span><span class="no">Rails</span><span class="o">.</span><span class="n">new</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso habilita o cache via Memcached. Em ambiente de desenvolvimento e teste, ele vai armazenar tudo em memória mesmo, e em produção vai usar o Memcached. Desde o Rails 2.3 o sistema de cache foi abstraído e você pode escolher diversos tipos de armazenadores como memória, arquivo, o próprio memcached e outros. Todos são gerenciados através de uma API única, a partir de <tt>Rails.cache</tt>.</p>
<p>Agora, basta fazer algo assim no controller:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">show</span>
</span></span><span class="line"><span class="cl"> <span class="n">cache_key</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">(</span><span class="s2">&#34;post_</span><span class="si">#{</span><span class="o">[</span><span class="ss">:year</span><span class="p">,</span> <span class="ss">:month</span><span class="p">,</span> <span class="ss">:day</span><span class="p">,</span> <span class="ss">:slug</span><span class="o">].</span><span class="n">collect</span> <span class="si">{</span><span class="o">|</span><span class="n">x</span><span class="o">|</span> <span class="n">params</span><span class="o">[</span><span class="n">x</span><span class="o">]</span> <span class="si">}</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s1">&#39;_&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"> <span class="n">etag</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">:public</span> <span class="o">=&gt;</span> <span class="kp">true</span> <span class="p">}</span>  
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">etag</span>  
</span></span><span class="line"><span class="cl"> <span class="n">fresh_when</span><span class="p">(</span> <span class="ss">:etag</span> <span class="o">=&gt;</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:public</span> <span class="o">=&gt;</span> <span class="kp">true</span> <span class="p">)</span> <span class="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>  
</span></span><span class="line"><span class="cl"> <span class="n">options</span> <span class="o">=</span> <span class="p">{}</span>  
</span></span><span class="line"><span class="cl"> <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">unless</span> <span class="n">request</span><span class="o">.</span><span class="n">fresh?</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"> <span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">find_by_permalink</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="o">[</span><span class="ss">:year</span><span class="p">,</span> <span class="ss">:month</span><span class="p">,</span> <span class="ss">:day</span><span class="p">,</span> <span class="ss">:slug</span><span class="o">].</span><span class="n">collect</span> <span class="p">{</span><span class="o">|</span><span class="n">x</span><span class="o">|</span> <span class="n">params</span><span class="o">[</span><span class="n">x</span><span class="o">]</span> <span class="p">}</span> <span class="o">&lt;&lt;</span> <span class="p">{</span><span class="ss">:include</span> <span class="o">=&gt;</span> <span class="o">[</span><span class="ss">:tags</span><span class="o">]</span><span class="p">}))</span>  
</span></span><span class="line"><span class="cl"> <span class="n">etag</span> <span class="o">=</span> <span class="vi">@post</span><span class="o">.</span><span class="n">updated_at</span><span class="o">.</span><span class="n">to_i</span>  
</span></span><span class="line"><span class="cl"> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:expires_in</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="o">.</span><span class="n">day</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"> <span class="n">fresh_when</span><span class="p">(</span> <span class="n">options</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="ss">:etag</span> <span class="o">=&gt;</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:last_modified</span> <span class="o">=&gt;</span> <span class="vi">@page</span><span class="o">.</span><span class="n">updated_at</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span> <span class="p">)</span> <span class="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>  
</span></span><span class="line"><span class="cl"> <span class="k">end</span>  
</span></span><span class="line"><span class="cl"><span class="k">end</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Existem 2 partes nessa lógica. Na primeira, montamos a chave do cache, que tem que ser única para cada elemento – no caso, um post – que você quer gerenciar. Em especial para meu blog eu monto uma chave baseada nos parâmetros que vem na requisição. Ou seja se o usuário mandar a URL “/2010/01/01/foo” ele vai montar a chave “post_2010_01_01_foo”. Daí ele faz um <tt>Rails.cache.read</tt> para ver se já existe um ETAG armazenado no memcached. Se já existir ele vai tentar chamar o <tt>fresh_when</tt> para ver se pode já só enviar o cabeçalho 304.</p>
<p>Na parte 2 ele checa <tt>request.fresh?(response)</tt>. Se voltar falso quer dizer que o navegador do usuário mandou um ETAG diferente do que temos armazenado no memcached, ou seja, provavelmente o post foi atualizado. Então temos que mandar uma versão nova. Daí ele faz a lógica normal de procurar pelo post, gerar o ETAG. Daí ele grava o ETAG novo no cache, pra garantir, e também manda esse novo ETAG no cabeçalho de volta ao navegador. Da próxima vez, o navegador vai mandar o novo ETAG e daí vai receber de volta apenas o cabeçalho 304. Além disso também estou configurando o cabeçalho “Last-Modified” para facilitar o cache da página.</p>
<p>Um pequeno detalhe é a hash <tt>options</tt>. Logo na parte 1 notem que eu faço isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">:public</span> <span class="o">=&gt;</span> <span class="kp">true</span> <span class="p">}</span>  
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">etag</span>  
</span></span><span class="line"><span class="cl"> <span class="n">fresh_when</span><span class="p">(</span> <span class="ss">:etag</span> <span class="o">=&gt;</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:public</span> <span class="o">=&gt;</span> <span class="kp">true</span> <span class="p">)</span> <span class="k">unless</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>  
</span></span><span class="line"><span class="cl"> <span class="n">options</span> <span class="o">=</span> <span class="p">{}</span>  
</span></span><span class="line"><span class="cl"><span class="k">end</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E mais abaixo eu faço isto:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">options</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="ss">:etag</span> <span class="o">=&gt;</span> <span class="n">etag</span><span class="p">,</span> <span class="ss">:last_modified</span> <span class="o">=&gt;</span> <span class="vi">@page</span><span class="o">.</span><span class="n">updated_at</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Isso porque na implementação do método <tt>fresh_when</tt> tem um trecho que é assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">options</span><span class="o">[</span><span class="ss">:public</span><span class="o">]</span>  
</span></span><span class="line"><span class="cl"> <span class="err">…</span>  
</span></span><span class="line"><span class="cl"> <span class="n">cache_control</span> <span class="o">&lt;&lt;</span> <span class="s2">&#34;public&#34;</span>  
</span></span><span class="line"><span class="cl"><span class="k">end</span>  </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ou seja, se eu chamar o método <tt>fresh_when</tt> múltiplas vezes com a opção <tt>:public =&gt; true</tt>, ele vai ficar adicionando na lista <tt>cache_control</tt> e daí no cabeçalho <tt>Cache-Control</tt> vai voltar uma string tipo <tt>public, public</tt>. Então, se o <tt>fresh_when</tt> já foi chamado no começo, na segunda vez eu tomo o cuidado de não passar o <tt>:public</tt> de novo.</p>
<p>Finalmente, no administrador de posts, eu invalido o cache caso eu atualize ou apague um post. Assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Admin</span><span class="o">::</span><span class="no">PostsController</span> <span class="o">&lt;</span> <span class="no">Admin</span><span class="o">::</span><span class="no">BaseController</span>  
</span></span><span class="line"><span class="cl"> <span class="n">after_filter</span> <span class="ss">:clean_cache</span><span class="p">,</span> <span class="ss">:only</span> <span class="o">=&gt;</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span>  
</span></span><span class="line"><span class="cl"> <span class="err">…</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kp">protected</span> <span class="k">def</span> <span class="nf">clean_cache</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">(</span><span class="s2">&#34;post_</span><span class="si">#{</span><span class="vi">@post</span><span class="o">.</span><span class="n">permalink</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="s2">&#34;_&#34;</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="s2">&#34;recent_posts_etag&#34;</span><span class="p">)</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No caso faço um <tt>after_filter</tt> que vai rodar só depois dos métodos <tt>create</tt>, <tt>update</tt> e <tt>destroy</tt>. No caso ele apaga o ETAG do post (método <tt>show</tt> do controller público) que tem aquele formato “post_2010_01_01_foo” (no caso eu criei um método chamado <tt>permalink</tt> no model <tt>Post</tt> que formata isso) e também deleta o ETAG no caso da página principal (que eu guardo com a chave <tt>recent_posts_etag</tt>), que busca vários posts. Eu assumo que se um post mudar, é melhor recriar a homepage inteira porque não sei se esse post aparece listado lá ou não. Poderia ter uma lógica melhor, mas considerando que eu não fico atualizando ou deletando posts o tempo todo, isso deve bastar.</p>
<p>Meu blog é bem simples, mas se o administrador fosse mais complexo e precisasse invalidar o cache a partir de múltiplos pontos, o melhor é criar um <a href="http://railsbox.org/2008/8/22/usando-o-observer-no-rails"target="_blank" rel="noopener">Observer</a> que centraliza a lógica e invalidação do cache de ETAGs.</p>
<h2>Sumarizando<span class="hx:absolute hx:-mt-20" id="sumarizando"></span>
    <a href="#sumarizando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não é um processo complicado mas precisa entender direito para que serve um ETAG e como funciona um cache. O uso do cache em si é bem simples, basicamente você lê (<tt>read</tt>) ou grava (<tt>write</tt>) nele a partir do objeto <tt>Rails.cache</tt>. Agora o importante é saber quando você invalida esse cache.</p>
<p>Logo que o servidor sobe, o cache está vazio, então ele precisa ir ao banco o tempo todo para cada requisição nova. Porém, é só a primeira vez porque a partir de agora ele vai armazenar o ETAG gerado no memcached e depois de 1 dia ou menos, a maioria dos meus posts já estarão com suas ETAGs no cache. Então o MySQL vai basicamente ficar sentado sem fazer nada – que é o ideal.</p>
<p>Então, minha otimização fez duas coisas: na primeira versão, que só gerava e processava ETAGs, eu economizei o tempo de processamento do ERB e envio do HTML. Agora estou economizando comunicação com o banco de dados, o tráfego dos dados entre o banco e o Rails e a geração dos ETAGs em si. Ou seja, o Rails está fazendo muito pouco, praticamente só servindo como um roteador mesmo.</p>
<p>Isso baixou o tempo de processamento médio de uma requisição do meu Rails de uns <strong>50ms</strong> , antes, para algo entre <strong>7ms</strong> e <strong>1ms</strong>!. Somado ao upgrade de memória de 512 para 768 MB de RAM e de 512 MB para 1 GB de swap, meu blog deve estar preparado para aguentar algumas ordens de grandeza mais tráfego simultâneo.</p>
]]></content:encoded><category>rails</category><category>performance</category><category>backend</category></item></channel></rss>