<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fernando</title>
    <description>The latest articles on DEV Community by Fernando (@fdocr).</description>
    <link>https://dev.to/fdocr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F19165%2Fefa51160-41cf-448d-909b-6ad82cec68d2.jpg</url>
      <title>DEV Community: Fernando</title>
      <link>https://dev.to/fdocr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fdocr"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Fri, 11 Apr 2025 14:28:43 +0000</pubDate>
      <link>https://dev.to/fdocr/-42pf</link>
      <guid>https://dev.to/fdocr/-42pf</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/miry/why-infrastructure-engineers-should-start-with-backend-development-34cf" class="crayons-story__hidden-navigation-link"&gt;Why Infrastructure Engineers Should Start with Backend Development&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/miry" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F99722%2F8c4f4baf-1623-40dd-91f9-523462892c24.jpeg" alt="miry profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/miry" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Michael Nikitochkin
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Michael Nikitochkin
                
              
              &lt;div id="story-author-preview-content-2396342" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/miry" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F99722%2F8c4f4baf-1623-40dd-91f9-523462892c24.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Michael Nikitochkin&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/miry/why-infrastructure-engineers-should-start-with-backend-development-34cf" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 10 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/miry/why-infrastructure-engineers-should-start-with-backend-development-34cf" id="article-link-2396342"&gt;
          Why Infrastructure Engineers Should Start with Backend Development
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/infrastructure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;infrastructure&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/miry/why-infrastructure-engineers-should-start-with-backend-development-34cf" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/miry/why-infrastructure-engineers-should-start-with-backend-development-34cf#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>programming</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Selfhost Timetagger on a Raspberry Pi</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Mon, 22 Apr 2024 11:00:00 +0000</pubDate>
      <link>https://dev.to/fdocr/selfhost-timetagger-on-a-raspberry-pi-26id</link>
      <guid>https://dev.to/fdocr/selfhost-timetagger-on-a-raspberry-pi-26id</guid>
      <description>&lt;p&gt;A few weeks ago I started looking into options for time tracking software. The goal is to be more aware of how I spend my time throughout the day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I could selfhost something with the Raspberry Pi I have lying around the house&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The intrusive thought prevailed, so here's a condensed/quick tutorial of how I made it work&lt;/p&gt;

&lt;h2&gt;
  
  
  Timetagger
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;aarch64&lt;/code&gt; architecture of the &lt;a href="https://www.raspberrypi.com/"&gt;Raspberry Pi 4&lt;/a&gt; I wanted to use didn't make this straightforward. I looked for open source projects written in Python for better/easier compatibility.&lt;/p&gt;

&lt;p&gt;I chose &lt;a href="https://timetagger.app/"&gt;timetagger.app&lt;/a&gt; (&lt;a href="https://github.com/almarklein/timetagger"&gt;github repo&lt;/a&gt;) and only a couple days after getting everything running I found the web app quite pleasant to use. I highly recommend it if interested in something like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Raspberry Pi

&lt;ul&gt;
&lt;li&gt;4 CPU + 8GB RAM runs the project like it's nothing so lower-end versions should also work&lt;/li&gt;
&lt;li&gt;I have a 64bit chip but installed 32bit OS (lite) because packages/tools on 64 bit OS were difficult to work with&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A domain with DNS managed on Cloudflare

&lt;ul&gt;
&lt;li&gt;Tunnels will allow you access your server publicly with a subdomain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll want to have your Raspberry Pi running in your local network. I won't cover the basics here, so I'm assuming you can &lt;code&gt;ssh user@host.local&lt;/code&gt; to your device at this point. Read &lt;a href="https://www.raspberrypi.com/documentation/computers/getting-started.html"&gt;getting started guides&lt;/a&gt; if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the server
&lt;/h2&gt;

&lt;p&gt;Timetagger has good docs and articules to help you get started with self hosting. Most of what I did I &lt;a href="https://timetagger.app/articles/selfhost/"&gt;learned it from this article&lt;/a&gt;. Rapid fire summary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure Python &amp;amp; pip are installed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python -m pip install -U timetagger&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create a dir to keep data &lt;code&gt;sudo mkdir /opt/timetagger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Change ownership (to yourself) &lt;code&gt;sudo chown -R [user]: /opt/timetagger/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You can now run the server using &lt;code&gt;python -m timetagger&lt;/code&gt; and you'll want to use ENV vars:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TIMETAGGER_CREDENTIALS=XXXX&lt;/code&gt; for username/password hash (&lt;a href="https://timetagger.app/cred"&gt;generate here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMETAGGER_LOG_LEVEL=info&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMETAGGER_BIND=0.0.0.0:8080&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMETAGGER_DATADIR=/opt/timetagger&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The web app should now be reachable in your local network on &lt;code&gt;host.local:8080&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Tunnels
&lt;/h2&gt;

&lt;p&gt;We want to access the app from anywhere in the world, not just from your WiFi at home.&lt;/p&gt;

&lt;p&gt;Kudos to the &lt;a href="https://pimylifeup.com/raspberry-pi-cloudflare-tunnel/"&gt;amazing tutorial from &lt;em&gt;pimylifeup.com&lt;/em&gt;&lt;/a&gt; which was the source of all I know related to the use of &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"&gt;Cloudflare tunnels&lt;/a&gt; on a Raspberry Pi. Be sure to check that one out for more details on each step below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sudo apt update &amp;amp; sudo apt upgrade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo apt install curl lsb-release&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-archive-keyring.gpg &amp;gt;/dev/null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo "deb [signed-by=/usr/share/keyrings/cloudflare-archive-keyring.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee  /etc/apt/sources.list.d/cloudflared.list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo apt install cloudflared&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudflared tunnel login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudflared tunnel create TUNNELNAME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudflared tunnel route dns TUNNELNAME DOMAINNAME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudflared tunnel run --url localhost:8080 TUNNELNAME&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used a subdomain under the &lt;a href="https://fdo.cr"&gt;&lt;code&gt;fdo.cr&lt;/code&gt; domain&lt;/a&gt; that I own. You should now be able to reach the web app outside your local network!&lt;/p&gt;

&lt;h2&gt;
  
  
  systemd services
&lt;/h2&gt;

&lt;p&gt;We want the server + &lt;code&gt;cloudflared&lt;/code&gt; to run in the background and start on boot. For this we'll use &lt;code&gt;systemd&lt;/code&gt; and luckily &lt;code&gt;cloduflared&lt;/code&gt; makes it simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a config file at &lt;code&gt;~/.cloudflared/config.yml&lt;/code&gt; (example below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo cloudflared --config ~/.cloudflared/config.yml service install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo systemctl enable cloudflared&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo systemctl start cloudflared&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tunnel: &lt;span class="o"&gt;[&lt;/span&gt;TUNNELNAME]
credentials-file: /home/[USERNAME]/.cloudflared/[UUID].json

ingress:
    - &lt;span class="nb"&gt;hostname&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;HOSTNAME]
      service: http://localhost:8080
    - service: http_status:404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll do the same for the timetagger server (manual &lt;code&gt;systemd&lt;/code&gt; setup). Start by creating &lt;code&gt;/user/local/bin/timetagger_boot.sh&lt;/code&gt; (example below). I used a sleep to delay boot and added ENV vars directly, a.k.a. the easiest/laziest solution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting 5s before startup..."&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting the server!"&lt;/span&gt;
&lt;span class="nv"&gt;TIMETAGGER_CREDENTIALS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;XXXXXXXXXX &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;TIMETAGGER_LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;TIMETAGGER_BIND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.0.0.0:8080 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;TIMETAGGER_DATADIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/timetagger &lt;span class="se"&gt;\&lt;/span&gt;
    python &lt;span class="nt"&gt;-m&lt;/span&gt; timetagger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create &lt;code&gt;/etc/systemd/timetagger.service&lt;/code&gt; (example below). Notice this example is using your user to run the service (not root).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Timetagger service
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network.target
&lt;span class="nv"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;timetagger_boot.sh
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always
&lt;span class="nv"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;USERNAME]

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally &lt;code&gt;systemctl daemon-reload&lt;/code&gt; or &lt;code&gt;sudo reboot&lt;/code&gt; to ensure the services are available on your public subdomain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Browsing the app (relatively &lt;em&gt;aggressively&lt;/em&gt;) while looking in &lt;code&gt;htop&lt;/code&gt; shows &lt;code&gt;0.12&lt;/code&gt; on load average which is basically nothing. I ran a quick benchmark with &lt;code&gt;ab -n 10000 -c 10 https://[SUBDOMAIN].fdo.cr/timetagger/app/demo&lt;/code&gt; and saw these results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fps2o5p613ykseqkrbgjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fps2o5p613ykseqkrbgjq.png" alt="Raspberry Pi htop" width="800" height="271"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Document Path:          /timetagger/app/demo
Document Length:        11559 bytes

Concurrency Level:      10
Time taken &lt;span class="k"&gt;for &lt;/span&gt;tests:   200.090 seconds
Complete requests:      10000
Failed requests:        7
   &lt;span class="o"&gt;(&lt;/span&gt;Connect: 0, Receive: 0, Length: 7, Exceptions: 0&lt;span class="o"&gt;)&lt;/span&gt;
Total transferred:      121439275 bytes
HTML transferred:       115516711 bytes
Requests per second:    49.98 &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="c"&gt;#/sec] (mean)&lt;/span&gt;
Time per request:       200.090 &lt;span class="o"&gt;[&lt;/span&gt;ms] &lt;span class="o"&gt;(&lt;/span&gt;mean&lt;span class="o"&gt;)&lt;/span&gt;
Time per request:       20.009 &lt;span class="o"&gt;[&lt;/span&gt;ms] &lt;span class="o"&gt;(&lt;/span&gt;mean, across all concurrent requests&lt;span class="o"&gt;)&lt;/span&gt;
Transfer rate:          592.70 &lt;span class="o"&gt;[&lt;/span&gt;Kbytes/sec] received

Connection Times &lt;span class="o"&gt;(&lt;/span&gt;ms&lt;span class="o"&gt;)&lt;/span&gt;
              min  mean[+/-sd] median   max
Connect:        0  112  56.5     97    1169
Processing:    44   87  38.4     79    1228
Waiting:       41   82  35.8     74    1227
Total:        109  200  69.7    180    1459

Percentage of the requests served within a certain &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;ms&lt;span class="o"&gt;)&lt;/span&gt;
  50%    180
  66%    196
  75%    212
  80%    225
  90%    278
  95%    329
  98%    402
  99%    458
 100%   1459 &lt;span class="o"&gt;(&lt;/span&gt;longest request&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are lots of caveats to note, i.e. the fact 10 concurrent users is 10x what this server will ever face, it doesn't resemble real user behavior and follow up requests come in immediately one after the other.&lt;/p&gt;

&lt;p&gt;Regardless of the caveats, a few of the noteworthy stats from the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The test transferred a total of 115 MB of HTML in 200s (~0.57 MB per second)&lt;/li&gt;
&lt;li&gt;Requests had a mean time of 200ms and a &lt;code&gt;P95&lt;/code&gt; of 329ms&lt;/li&gt;
&lt;li&gt;Load averge on &lt;code&gt;htop&lt;/code&gt; barely went over 1 (out of 4 CPUs on the device)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Considering it's running SQLite and the Python server connected publicly with a Cloudflare Tunnel (CPU hungry process) I can say the Raspberry Pi is impressively powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A $35-ish home server vs what a subscription service could cost per year&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I find that ^ comparison an interesting one. Proprietary SaaS offerings have delightful UI/UX and lots of extra integrations/tools, but could cost $50 or $100+ per year. Timetagger itself notably sells a &lt;a href="https://timetagger.app/#pricing"&gt;lifetime plan for €144&lt;/a&gt; which I would consider if I were to stop selfhosting.&lt;/p&gt;

&lt;p&gt;I saw generous Free tiers but they wipe out your historic data or have some other way to hook you into subscribing. No hate intended, it's business strategy.&lt;/p&gt;

&lt;p&gt;For my needs, the OSS project I can run on my server at home will be my preferred choice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A $35-ish home server vs $5-ish/month VPS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This ^ other comparison is easily won by the Raspberry Pi home server. $60/year will get you 1 CPU (shared), 1-2GB RAM and limited SSD on a cloud provider. A Raspberry Pi has 4x those specs with large SD card storage limits if needed (30GB are plenty and cheap though).&lt;/p&gt;

&lt;p&gt;You would benefit from the uptime and high bandwith + low latency connection of a datacenter when compared to your house WiFi, but that's about it. For a hobby project this will be perfect.&lt;/p&gt;

&lt;p&gt;Kudos to all the people working on OSS (and writing about it) that made this weird weekend project a reality. Also, hats off if you've made it this far reading and/or are willing to try this out for yourself. Pura Vida!&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Keeping up with my cat's 💩 using a RaspberryPi</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Tue, 23 May 2023 18:08:11 +0000</pubDate>
      <link>https://dev.to/fdocr/keeping-up-with-my-cats-using-a-raspberrypi-oia</link>
      <guid>https://dev.to/fdocr/keeping-up-with-my-cats-using-a-raspberrypi-oia</guid>
      <description>&lt;p&gt;As a former dog owner and first time cat dad I was amazed at how cats are "potty trained" practically from birth. I was prepared to deal with the smell when having to clean the litter box. However, I didn't expect their bowel movements (💩) to carry a punch that would stink up half my apartment. &lt;/p&gt;

&lt;p&gt;This might not be the case for everyone, but certainly for me, with an indoor cat in a 2 bedroom apartment without a naturally ventilated place to keep her litter box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hero/villain of the story
&lt;/h2&gt;

&lt;p&gt;Her name is &lt;em&gt;Dua&lt;/em&gt; and she is a cuddly &amp;amp; playful rescue &lt;a href="https://www.thesprucepets.com/tortoiseshell-cat-profile-554703" rel="noopener noreferrer"&gt;tortie cat&lt;/a&gt;. Dua loves to play with her mouse toys and &lt;strong&gt;adores&lt;/strong&gt; wet food, the latter of which is likely the reason I'm writing this post 😵‍💫&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9djsnopvk4t5890qsf7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9djsnopvk4t5890qsf7.png" alt="Dua sitting on a puff sofa"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have her litter box in my second bathroom's shower. The bathroom has an extractor fan that runs when the bathroom light is on, but she &lt;del&gt;refuses to&lt;/del&gt; hasn't figured out how to turn it on and off each time she goes #2... Annoying, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the extractor fan
&lt;/h2&gt;

&lt;p&gt;To mitigate the smell I wanted the lights to turn on when Dua goes in her litter box. To do this I put together a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero-w/" rel="noopener noreferrer"&gt;RaspberryPi Zero W&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;WiFi support was the goal&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://learn.adafruit.com/pir-passive-infrared-proximity-motion-sensor/how-pirs-work" rel="noopener noreferrer"&gt;PIR motion sensor&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Placed on the bathroom wall with "velcro stickers"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Smart switch for bathroom lights (any brand will do) paired with an Alexa&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://voicemonkey.io/" rel="noopener noreferrer"&gt;Voicemonkey&lt;/a&gt; webhook to trigger an &lt;a href="https://www.amazon.com/alexa-routines/b?ie=UTF8&amp;amp;node=21442922011&amp;amp;ref=hp_hub_rout" rel="noopener noreferrer"&gt;Alexa routine&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Alexa, turn on the switch for 5 minutes"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The software that calls the webhook (which in turn triggers the Alexa routine) can be found here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/fdocr" rel="noopener noreferrer"&gt;
        fdocr
      &lt;/a&gt; / &lt;a href="https://github.com/fdocr/pir_trigger" rel="noopener noreferrer"&gt;
        pir_trigger
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Script that connects a PIR Sensor to a webhook
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;PIR Trigger&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Script that connects a PIR Sensor to a webhook.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Usage&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Clone the repo in a folder, install dependencies and then run in background&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Install requirements&lt;/span&gt;
pip install -r requirements.txt

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Run in background&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; TODO: Find/Document a better way to do this&lt;/span&gt;
TRIGGER_URL=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&amp;lt;webhook_url&amp;gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; python main.py &lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Otherwise add &lt;code&gt;TRIGGER_URL = "&amp;lt;webhook_url&amp;gt;"&lt;/code&gt; to an &lt;code&gt;.env&lt;/code&gt; file and the script will pick it up.&lt;/p&gt;
&lt;p&gt;The script writes its own PID to &lt;code&gt;pid.txt&lt;/code&gt; so it can be used. Examples:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Follow output of background process&lt;/span&gt;
tail -f /proc/&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;cat pid.txt&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;/fd/1

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Kill process&lt;/span&gt;
&lt;span class="pl-c1"&gt;kill&lt;/span&gt; -9 &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;cat pid.txt&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sensor to board connections&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://projects-static.raspberrypi.org/projects/physical-computing/248971027a596f3437da45bafd2bd8a8cc35cb95/en/images/pir_wiring.png" rel="nofollow noopener noreferrer"&gt;Cable diagram here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The script was inspired by &lt;a href="https://projects.raspberrypi.org/en/projects/physical-computing/11" rel="nofollow noopener noreferrer"&gt;this Raspberry Pi Foundation article&lt;/a&gt; and uses their suggested example layout. The sensor needs 5v (Vcc) and Ground (Gnd), so PIN 2 and PIN 6 work well. Connect the sensor's output (Out) to…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/fdocr/pir_trigger" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  It works!
&lt;/h2&gt;

&lt;p&gt;Here's what the hardware looks like in action&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0eubgtr928l520rk71wc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0eubgtr928l520rk71wc.png" alt="RPi and motion sensor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;RPi and motion sensor&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ny3gemxu79stq2kw3b5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ny3gemxu79stq2kw3b5.gif" alt="Awful quality GIF of our hero/villain"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;Awful quality GIF of our hero/villain&lt;/small&gt;&lt;/center&gt;

&lt;h2&gt;
  
  
  💩 Stats
&lt;/h2&gt;

&lt;p&gt;With all of this in place I went a step further and added Opentelemetry to track the stats of how often the routine was being triggered on &lt;a href="https://honeycomb.io" rel="noopener noreferrer"&gt;Honeycomb&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wanted to know if I was turning on the bathroom lights over false positives from the motion sensor, but after some tests it simply serves the purpose of telling how often she goes in her litter box.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31ar5lng2amk8egr1z1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31ar5lng2amk8egr1z1c.png" alt="Last 7 days 💩 activity"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;Last 7 days 💩 activity&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9zm970xy3n9tvu7x3pk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9zm970xy3n9tvu7x3pk.png" alt="Last 24 hours 💩 activity"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;Last 24 hours 💩 activity&lt;/small&gt;&lt;/center&gt;

&lt;center&gt; &lt;/center&gt;

&lt;p&gt;Interestingly, I can tell she goes in her litter box (# of motion sensor triggers) on average ~8.5 times per day. I don't think many cat owners can say they know this about their feline friends. I do remember and took inspiration from &lt;a href="https://twitter.com/tenderlove/status/823341842586419200" rel="noopener noreferrer"&gt;Aaron Patterson&lt;/a&gt; doing something similar a long time ago though.&lt;/p&gt;

&lt;p&gt;Anyways, that's it. Pura vida!&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>Background jobs for Kemal server in Crystal lang</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Thu, 02 Mar 2023 12:00:00 +0000</pubDate>
      <link>https://dev.to/fdocr/background-jobs-for-kemal-server-in-crystal-lang-53j5</link>
      <guid>https://dev.to/fdocr/background-jobs-for-kemal-server-in-crystal-lang-53j5</guid>
      <description>&lt;p&gt;Yet another post about the &lt;a href="https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj"&gt;Battlesnake project&lt;/a&gt; I've been working on while diving in &lt;a href="https://crystal-lang.org/" rel="noopener noreferrer"&gt;Crystal lang&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a quick continuation of &lt;a href="https://dev.to/fdocr/database-for-kemal-server-in-crystal-lang-561p"&gt;yesterday's post&lt;/a&gt; about storing data in a DB. I mentioned why I took a performance hit and the solution implemented was to persist the data to DB from a background job. The idea is that enqueuing the jobs to Redis would ideally perform better than waiting for a DB write.&lt;/p&gt;

&lt;p&gt;Again, FYI the code is &lt;a href="https://github.com/fdocr/CrystalSnake" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mosquito
&lt;/h2&gt;

&lt;p&gt;The Crytal Sidekiq port was a tempting option for a background task runner, but I went with &lt;a href="https://github.com/mosquito-cr/mosquito" rel="noopener noreferrer"&gt;&lt;code&gt;mosquito-cr/mosquito&lt;/code&gt;&lt;/a&gt;. My initializer looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"mosquito"&lt;/span&gt;

&lt;span class="no"&gt;Mosquito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REDIS_URL"&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"redis://localhost:6379"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since initializers are required by &lt;code&gt;src/app.cr&lt;/code&gt; (more about this &lt;a href="https://dev.to/fdocr/database-for-kemal-server-in-crystal-lang-561p"&gt;on a previous post&lt;/a&gt;) I can now enqueue the job where I once persisted directly to DB. Snippets from all of this below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Replaces `Turn.create(...)`&lt;/span&gt;
&lt;span class="no"&gt;PersistTurnJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;context_json: &lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/jobs/persist_turn_job.cr&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"mosquito"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PersistTurnJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_json&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trace_perform&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snakes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
    &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;game_id: &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;snake_id: &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="n"&gt;context_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;dead: &lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to have OpenTelemetry tracing of the jobs I inherit from my &lt;code&gt;ApplicationJob&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/jobs/application_job.cr&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"mosquito"&lt;/span&gt;

&lt;span class="c1"&gt;# Base class for jobs in the app. Overrides `perform` so that jobs can&lt;/span&gt;
&lt;span class="c1"&gt;# implement `trace_perform` instead. This will allow for OpenTelemetry tracing&lt;/span&gt;
&lt;span class="c1"&gt;# if available, otherwise the job will be executed as it would if it overrides&lt;/span&gt;
&lt;span class="c1"&gt;# `perform` (mosquito standard). If someone does override `perform` on the job&lt;/span&gt;
&lt;span class="c1"&gt;# it will also have no behavior effect, other than tracing not taking place.&lt;/span&gt;
&lt;span class="n"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Mosquito&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QueuedJob&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HONEYCOMB_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt;
      &lt;span class="no"&gt;OpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trace&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;internal&lt;/span&gt;
        &lt;span class="n"&gt;trace_perform&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;trace_perform&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Worker
&lt;/h2&gt;

&lt;p&gt;Until now the app produced only one executable, which was the API server. The worker that runs these background jobs is &lt;code&gt;src/worker.cr&lt;/code&gt; file and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./battle_snake/**"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./strategy/**"&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"dotenv"&lt;/span&gt;
&lt;span class="no"&gt;Dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;".env"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./initializers/**"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./models/**"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./jobs/**"&lt;/span&gt;

&lt;span class="no"&gt;Mosquito&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It requires all dependencies and starts the mosquito runner, but how to work on it locally?&lt;/p&gt;

&lt;p&gt;I have a &lt;a href="https://github.com/imdrasil/sam.cr" rel="noopener noreferrer"&gt;sam.cr&lt;/a&gt; task in place that helps me with local develpment. I tweaked &lt;code&gt;make sam dev&lt;/code&gt; to spin up two &lt;a href="https://github.com/samueleaton/sentry" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt; runners, and it continues to work seamlessly with both development executables + livereload (livecompile?) again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile
&lt;/h2&gt;

&lt;p&gt;I use a Docker deployment on DigitalOcean's App Platform (&lt;a href="https://dev.to/fdocr/deploy-a-crystal-app-with-docker-and-opentelemetry-24cp"&gt;previous post about this&lt;/a&gt;), so I need to change the Dockerfile so that both executables are compiled &amp;amp; included there. This allows to execute either one from the same image passing in the command to override the default &lt;code&gt;ENTRYPOINT&lt;/code&gt; (&lt;a href="https://docs.docker.com/engine/reference/builder/#entrypoint" rel="noopener noreferrer"&gt;reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Build image&lt;/span&gt;
&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crystallang&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;crystal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;alpine&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
&lt;span class="no"&gt;WORKDIR&lt;/span&gt; &lt;span class="sr"&gt;/opt
# Cache dependencies
COPY ./s&lt;/span&gt;&lt;span class="n"&gt;hard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yml&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="n"&gt;shard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt; &lt;span class="sr"&gt;/opt/&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="c1"&gt;# Build a binary&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;crystal&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cr&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;crystal&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cr&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;crystal&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;money_hack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cr&lt;/span&gt;
&lt;span class="c1"&gt;# ===============&lt;/span&gt;
&lt;span class="c1"&gt;# Result image with one layer&lt;/span&gt;
&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;alpine&lt;/span&gt;&lt;span class="ss"&gt;:latest&lt;/span&gt;
&lt;span class="no"&gt;WORKDIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="sr"&gt;/opt/&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="sr"&gt;/opt/&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="sr"&gt;/opt/mone&lt;/span&gt;&lt;span class="n"&gt;y_hack&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;ENTRYPOINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./money_hack"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's now compiling &lt;code&gt;src/app.cr&lt;/code&gt;, &lt;code&gt;src/worker.cr&lt;/code&gt;, and... &lt;code&gt;src/money_hack.cr&lt;/code&gt;? Well, I already mentioned I'm not trying to spend more money than needed on this project.&lt;/p&gt;

&lt;p&gt;My solution was to run both the server and the worker on the same node/droplet. There are many disadvantages to this so I don't recommend this practice as a rule of thumb (hence the name &lt;code&gt;src/money_hack.cr&lt;/code&gt;). Managing this with Kemal/Mosquito configs or independent horizontal scaling as needed are likely better solutions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;#! /usr/bin/env crystal&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Runs both the server and worker executables in separate fibers to avoid&lt;/span&gt;
&lt;span class="c1"&gt;# independent deployments. Motivation is saving costs&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Nil&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

&lt;span class="n"&gt;spawns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./app -p 8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"./worker"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;spawn&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;system&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resource consumption &amp;amp; performance
&lt;/h2&gt;

&lt;p&gt;First of all, there's now two executables running on the same droplet that used to only run the API server. These are the insights of the small instance running &lt;code&gt;512 MB RAM | 1 vCPU x 1&lt;/code&gt; (last 7 days).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fscvcfj3uk6uksjyjowt0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fscvcfj3uk6uksjyjowt0.png" alt="Snake resource consumption graph"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I marked in red the (approximate) regions to explain them better.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Memory average&lt;/th&gt;
&lt;th&gt;CPU average&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;0%-1%&lt;/td&gt;
&lt;td&gt;Pre-data persistance (no DB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;development (debug/errors/deployments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;1%-2%&lt;/td&gt;
&lt;td&gt;sync DB persistance (&lt;a href="https://dev.to/database-for-kemal-server-in-crystal-lang"&gt;prev post&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;development (debug/errors/deployments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;1%-2%&lt;/td&gt;
&lt;td&gt;Background job persistance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In region 1 there's ~13% memory usage and that remains unchanged during region 3. With the worker processing jobs region 4 did consume ~16% memory, which is very little difference IMO. CPU usage in region 1 was 0% and 1% at times (5min granularity). With both persitance implementations on region 3 and 5 it bumped up to 1% and 2% values.&lt;/p&gt;

&lt;p&gt;All of this tells me that we could (in theory) refactor &lt;code&gt;money_hack.cr&lt;/code&gt; to have many workers in parallel before getting close to maxing out the hardware capabilities. Definitely not necessary for now, specially since the bottleneck is DB/Redis hardware. Just a funny situation to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telemetry comparison
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;P50&lt;/code&gt;, &lt;code&gt;P95&lt;/code&gt;, and &lt;code&gt;P99&lt;/code&gt; of traces across the last few days below&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE DB PERSISTANCE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwe7yo9bhn2ne6uv16oc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwe7yo9bhn2ne6uv16oc.png" alt="Server times before DB persist implemented"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SYNC DB PERSISTANCE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkllfb1zjbc75avrcddj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkllfb1zjbc75avrcddj.png" alt="Server times with DB persist implemented"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ASYNC WORKER DB PERSISTANCE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonba6lk7xe0xmfudmf6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonba6lk7xe0xmfudmf6a.png" alt="Server times with background job implemented"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's great! ~2x better in &lt;code&gt;P50&lt;/code&gt;, ~2.5x better in &lt;code&gt;P95&lt;/code&gt;, and ~5x better in &lt;code&gt;P99&lt;/code&gt;. Nothing compares to raw Crystal code execution with sub-millisecond response times, but this isn't bad in any way considering we have everything integrated in the project.&lt;/p&gt;

&lt;p&gt;Below is the telemetry on the job execution too. An alternative would be aggregating/querying logs I guess, but with the existing integration it's an easy way to measure them too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadd9kkvivmjybejzb2i9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadd9kkvivmjybejzb2i9.png" alt="Background job times"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It makes sense to see the jobs perform similar to the &lt;em&gt;sync DB persistance&lt;/em&gt; telemetry data. Reassures the assumption (widely known/used approach) that DB requests are slower than Redis in this scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;The app is starting to look sturdy to me, as in there are a few moving pieces now and it performs nicely (fast and reliable). It's also a joy to work with Crystal code.&lt;/p&gt;

&lt;p&gt;I have one more post in mind for the series, at least for a while, so I'm excited about sharing that sometime soon.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>docker</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Database for Kemal server in Crystal lang</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Wed, 01 Mar 2023 13:00:00 +0000</pubDate>
      <link>https://dev.to/fdocr/database-for-kemal-server-in-crystal-lang-561p</link>
      <guid>https://dev.to/fdocr/database-for-kemal-server-in-crystal-lang-561p</guid>
      <description>&lt;p&gt;This is another post about the &lt;a href="https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj"&gt;Battlesnake project&lt;/a&gt; I've been working on while diving in &lt;a href="https://crystal-lang.org/"&gt;Crystal lang&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This time I'm looking into persisting the game data to a database for analysis. This will allow me to review games from each game played by the bot and hopefully improve my spot on the leaderboards by tweaking my strategy.&lt;/p&gt;

&lt;p&gt;FYI the code is &lt;a href="https://github.com/fdocr/CrystalSnake"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jennifer.cr
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/imdrasil/jennifer.cr"&gt;&lt;code&gt;imdrasil/jennifer.cr&lt;/code&gt;&lt;/a&gt; is my ORM of choice, but I'm no longer following the recommended &lt;code&gt;config&lt;/code&gt; directory in the root of the project. I'm just stuffing initializers to be required from the main app files (i.e. &lt;code&gt;src/app.cr&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This is what the database initializer can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/initializers/database.cr&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"kemal"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"jennifer"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"jennifer/adapter/postgres"&lt;/span&gt;

&lt;span class="no"&gt;Jennifer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Debug&lt;/span&gt;
  &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DB_POOL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://imdrasil.github.io/jennifer.cr/docs/"&gt;Jennifer docs&lt;/a&gt; are great. I found a couple of difficulties along the way, mostly related to configuration details, but made it work with help from kind folks on GitHub (shoutout to &lt;a class="mentioned-user" href="https://dev.to/seesethcode"&gt;@seesethcode&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration &amp;amp; Model
&lt;/h2&gt;

&lt;p&gt;It's not quite like ActiveRecord (maybe nothing is/will be 😅), but IMO it's great and has lots of incredibly valuable features (i.e. built in migration management, query dsl, etc). Here's what the migration and model look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateTurns&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Jennifer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:turns&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:game_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:snake_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt; &lt;span class="ss"&gt;:dead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:null&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:game_id&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;down&lt;/span&gt;
    &lt;span class="n"&gt;drop_table&lt;/span&gt; &lt;span class="ss"&gt;:turns&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;table_exists?&lt;/span&gt; &lt;span class="ss"&gt;:turns&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is a DB record representation of a request from a game for either&lt;/span&gt;
&lt;span class="c1"&gt;# start/move/end request.&lt;/span&gt;
&lt;span class="c1"&gt;# &lt;/span&gt;
&lt;span class="c1"&gt;# NOTE: https://imdrasil.github.io/jennifer.cr/docs/model_mapping&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Turn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Jennifer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;with_timestamps&lt;/span&gt;

  &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="no"&gt;Primary64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;game_id: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;snake_id: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;dead: &lt;/span&gt;&lt;span class="no"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all of this in place I added this macro to &lt;code&gt;src/app.cr&lt;/code&gt; and called it from within &lt;code&gt;/start&lt;/code&gt;, &lt;code&gt;/move&lt;/code&gt;, &lt;code&gt;/end&lt;/code&gt; endpoints so the turn data gets persisted in Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nf"&gt;persist_turn!&lt;/span&gt;
  &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snakes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
  &lt;span class="no"&gt;Turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;game_id: &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;snake_id: &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;dead: &lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I left this run through Monday's leaderboard matches and it worked. You can check out the most recent games persisted by my snake in &lt;a href="https://snake.fdo.cr/games"&gt;&lt;code&gt;https://snake.fdo.cr/games&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance impact
&lt;/h2&gt;

&lt;p&gt;Now that we're storing (inserting) data to PostgreSQL, which is currently hosted in a low cost server, response times should've slowed down a noticeable amount.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opentelemetry.io/"&gt;OpenTelemetry&lt;/a&gt; data on &lt;a href="https://www.honeycomb.io/"&gt;Honeycomb&lt;/a&gt; is able to tell me exactly how it performed (read &lt;a href="https://dev.to/fdocr/deploy-a-crystal-app-with-docker-and-opentelemetry-24cp"&gt;my post about this here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BEFORE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--riatkNl8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwe7yo9bhn2ne6uv16oc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--riatkNl8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rwe7yo9bhn2ne6uv16oc.png" alt="Server times before DB persist implemented" width="880" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AFTER&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q_v83mUP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkllfb1zjbc75avrcddj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q_v83mUP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkllfb1zjbc75avrcddj.png" alt="Server times with DB persist implemented" width="880" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's indeed a penalty in persisting data, likely coming from network latency and the low cost DB server performance being much lower than ideal... I'm saving $$ with that pet (as in &lt;a href="https://traefik.io/blog/pets-vs-cattle-the-future-of-kubernetes-in-2022/"&gt;"not caddle"&lt;/a&gt;) server (droplet on &lt;a href="https://www.digitalocean.com/"&gt;DO&lt;/a&gt;) hosting Redis+Postgres maintained by me. This hack saves me ~$25/month and could merit a post in and of itself 😆&lt;/p&gt;

&lt;p&gt;Anyways, Battlesnake rules usually work with a &lt;code&gt;500ms&lt;/code&gt; timeout, which means we have time to spare for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to recover/regain performance
&lt;/h2&gt;

&lt;p&gt;I'm not going to start spending money on high performing tiers of hosted Postgres services anytime soon, so I could think of ways to avoid such a costly penatly. One option is to store the &lt;code&gt;Turn&lt;/code&gt; data asynchronously.&lt;/p&gt;

&lt;p&gt;Redis is better performing than Postgres, so queuing the data to be persisted to DB by a background job instead of synchronously when the request is made might help improve response times again.&lt;/p&gt;

&lt;p&gt;I actually already have this implemented with the &lt;a href="https://github.com/mosquito-cr/mosquito"&gt;mosquito shard&lt;/a&gt;, but that will be covered in the next post of this series. I'm waiting for at least a day's worth of leaderboard match data to reliably compare with the results above, so stay tuned if interested I guess.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>database</category>
      <category>api</category>
    </item>
    <item>
      <title>Algorithms and standard library modules in my Battlesnake</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Thu, 23 Feb 2023 12:00:00 +0000</pubDate>
      <link>https://dev.to/fdocr/algorithms-and-standard-library-modules-in-my-battlesnake-4a2c</link>
      <guid>https://dev.to/fdocr/algorithms-and-standard-library-modules-in-my-battlesnake-4a2c</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj"&gt;first post&lt;/a&gt; in the series &lt;em&gt;walked through&lt;/em&gt; the code of my current strategy. This one will share a few of the learnings from writing the utility algorithms in the project (as examples for) where I used Crystal's standard library modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing JSON in Crystal
&lt;/h2&gt;

&lt;p&gt;This is a common task that has to be dealt with on every language. Crystal provides the &lt;code&gt;JSON::Serializable&lt;/code&gt; module for you to include in your class. That way you get &lt;code&gt;to_json&lt;/code&gt; and &lt;code&gt;from_json&lt;/code&gt; methods based on properties &amp;amp; annotations. The &lt;a href="https://crystal-lang.org/api/1.7.2/JSON/Serializable.html#usage"&gt;module's usage instructions (docs)&lt;/a&gt; are great.&lt;/p&gt;

&lt;p&gt;It might be cumbersome to work through a nested object hierarchy when first setting it up, but it's pretty neat to parse Battlesnake's context and then work with all objects (i.e. &lt;code&gt;@context.board.snakes.head&lt;/code&gt;, &lt;code&gt;@context.board.width&lt;/code&gt;, etc). Pretty neat to have instant feedback against typos when working with these thanks to the compiler thereafter too.&lt;/p&gt;

&lt;p&gt;The classes in the &lt;a href="https://github.com/fdocr/CrystalSnake/tree/main/src/battle_snake"&gt;&lt;code&gt;/src/battle_snake&lt;/code&gt; directory&lt;/a&gt; are the representation of the game that comes in as JSON payload on each turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  A* Search Algorithm
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;a_star.cr&lt;/code&gt; is the utility method (&lt;a href="https://fdocr.github.io/CrystalSnake/Strategy/Utils.html#a_star%28a%3ABattleSnake%3A%3APoint%2Cb%3ABattleSnake%3A%3APoint%2Ccontext%3ABattleSnake%3A%3AContext%29-class-method"&gt;docs reference&lt;/a&gt;) that will find the shortest path from a point A to a point B. In this context, a point is a &lt;code&gt;BattleSnake::Point&lt;/code&gt; that represents a coordinate &lt;code&gt;(x, y)&lt;/code&gt; on the board.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The history and how it works is explained &lt;a href="https://en.wikipedia.org/wiki/A*_search_algorithm"&gt;in wikipedia&lt;/a&gt;. My implementation &lt;a href="https://github.com/fdocr/CrystalSnake/blob/main/src/strategy/utils/a_star.cr"&gt;is on GitHub (here)&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm sure there are potential optimizations, but I did my best to make it efficient with things like using the recommended priority queue data structure with &lt;a href="https://github.com/spider-gazelle/priority-queue"&gt;&lt;code&gt;spider-gazelle/priority-queue&lt;/code&gt;&lt;/a&gt;. After all the server needs to respond quick (&lt;code&gt;&amp;lt;500ms&lt;/code&gt;) for it to play the game and we need to trust we can call utility methods many times on a single turn to make a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing points (distance)
&lt;/h2&gt;

&lt;p&gt;In order to compare and sort custom classes on data structures Crystal provides the &lt;a href="https://crystal-lang.org/api/1.7.2/Comparable.html"&gt;&lt;code&gt;Comparable&lt;/code&gt; module/mixin&lt;/a&gt;. The formula to know the distance between two &lt;code&gt;BattleSnake::Point&lt;/code&gt; looks like this (&lt;a href="https://github.com/fdocr/CrystalSnake/blob/main/src/battle_snake/point.cr"&gt;class code here&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;y&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Known as Manhattan distance and by a few other names, this is the basis of how we can tell how far away two points are from each other. This is key in being able to implement A* on a board, which could be thought of as a graph with equal distance (1) from each node.&lt;/p&gt;

&lt;p&gt;Now our class automatically gets the conventional comparison operators (&lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;==&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;, and &lt;code&gt;&amp;gt;&lt;/code&gt;) populated based off it and data structures like arrays &lt;a href="https://crystal-lang.org/api/1.7.2/Array.html#sort%3AArray%28T%29-instance-method"&gt;can sort them&lt;/a&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flood fill
&lt;/h2&gt;

&lt;p&gt;This is an algorithm that determines all the possible area that can be reached, as oppossed to what all the empty points on the board because a section might be blocked off and unreachable (&lt;a href="https://fdocr.github.io/CrystalSnake/Strategy/Utils.html#flood_fill%28a%3ABattleSnake%3A%3APoint%2Ccontext%3ABattleSnake%3A%3AContext%29-class-method"&gt;docs reference&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;My own (summarized) way of explaining it would be &lt;em&gt;"an iterative processing of all valid nearby spaces"&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;More about flood fill &lt;a href="https://en.wikipedia.org/wiki/Flood_fill"&gt;on wikipedia&lt;/a&gt;. My implementation is &lt;a href="https://github.com/fdocr/CrystalSnake/blob/main/src/strategy/utils/flood_fill.cr"&gt;on GitHub (here)&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This comes in handy when making a decision between different valid moves on a strategy. Consider if you had to choose bewteen &lt;em&gt;(a)&lt;/em&gt; move to the right and end up with a flood fill area of 3, or &lt;em&gt;(b)&lt;/em&gt; move to the left and end up with a flood fill area of 12. Option &lt;em&gt;(a)&lt;/em&gt; sounds like the best one.&lt;/p&gt;

&lt;p&gt;That's how &lt;code&gt;CautiousCarol&lt;/code&gt; decides to take a left instead of running into an enclosed area (dead end): By calculating what those flood fill results are and picking the largest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;There are other modules/mixins that can be included or used to extend classes (i.e. &lt;a href="https://crystal-lang.org/api/1.7.2/Enumerable.html"&gt;Enumerable&lt;/a&gt;), so these are just two situations applied to this specific project.&lt;/p&gt;

&lt;p&gt;I had flashbacks to university memories when working on these. Nostalgic and fun. The cool thing is that now every new strategy I can work on will be able to use any of these if need be.&lt;/p&gt;

&lt;p&gt;Kudos to the BattleSnake team for the resources they upload, like the &lt;a href="https://www.youtube.com/watch?v=XyptXbHxZ0w&amp;amp;t=2918s"&gt;Deep Learning: Useful Battlesnake Algorithms&lt;br&gt;
&lt;/a&gt;YouTube video, where I learned a lot about how these and other algorithms can be applied in BattleSnake.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Documenting a Crystal open source project</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Tue, 21 Feb 2023 12:00:00 +0000</pubDate>
      <link>https://dev.to/fdocr/documenting-a-crystal-open-source-project-3ike</link>
      <guid>https://dev.to/fdocr/documenting-a-crystal-open-source-project-3ike</guid>
      <description>&lt;p&gt;This post is a quick overview of how Crystal lang built-in documentation features work and an easy setup to host them for free for Open Source projects. A compilation of things I've seen across Crystal repos and applied on mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documenting code
&lt;/h2&gt;

&lt;p&gt;Crystal maintains a &lt;code&gt;crystal docs&lt;/code&gt; command that processes your project's codebase and exports a website with the README &amp;amp; inline comments. IMO it's awesome to encourage documentation to live within the codebase itself.&lt;/p&gt;

&lt;p&gt;The generated site will reference/link files (i.e. &lt;code&gt;Module::Class&lt;/code&gt; mentions are automatically resolved and converted into links to the respective feature), Admonitions (i.e. support for &lt;code&gt;NOTE&lt;/code&gt;, &lt;code&gt;TODO&lt;/code&gt;, etc notes), Inheriting Documentation (based on class inheritance), amongst others.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crystal-lang.org/reference/1.7/syntax_and_semantics/documenting_code.html"&gt;Official Crystal documenting code guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions ➡ GitHub Pages
&lt;/h2&gt;

&lt;p&gt;Automating becomes possible when putting GitHub actions &amp;amp; pages together, something I jumped into as soon as I realized it would be cool to ensure the docs are always up-to-date with the &lt;code&gt;main&lt;/code&gt; branch. Here's what my &lt;code&gt;docs.yml&lt;/code&gt; GitHub action looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload docs&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout 🛎️&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Crystal&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crystal-lang/install-crystal@v1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build docs&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shards &amp;amp;&amp;amp; crystal docs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy 🚀&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JamesIves/github-pages-deploy-action@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docs&lt;/span&gt; &lt;span class="c1"&gt;# The folder the action should deploy.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are generated and committed to &lt;code&gt;gh-pages&lt;/code&gt; branch, so once enabled in the repo's settings (screenshot below) you'll get &lt;code&gt;https://&amp;lt;username&amp;gt;.github.io/&amp;lt;repo-name&amp;gt;&lt;/code&gt; website hosted (for free).&lt;/p&gt;

&lt;p&gt;I didn't enable a custom domain because I'm okay with GitHub's subdomain, but that should be able to work out with the proper &lt;a href="https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site"&gt;DNS configuration (tutorial)&lt;/a&gt; if you rather have the site hosted on a subdomain of yours.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yun2kbXI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwafxb4en1xcr87u5mnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yun2kbXI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwafxb4en1xcr87u5mnj.png" alt="GitHub repo pages configuration" width="880" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The landing pages will be the README, and you can structure that however you prefer. This is what the &lt;a href="https://github.com/fdocr/CrystalSnake#crystal-snake"&gt;current README&lt;/a&gt; of the battlesnake project looks like compared to &lt;a href="https://fdocr.github.io/CrystalSnake/"&gt;the hosted site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's great to be able to dive deeper into each model class (i.e. &lt;a href="https://fdocr.github.io/CrystalSnake/BattleSnake/Context.html"&gt;BattleSnake::Context&lt;/a&gt;) or an existing Strategy (i.e. &lt;a href="https://fdocr.github.io/CrystalSnake/Strategy/CautiousCarol.html"&gt;Strategy::CautiousCarol&lt;/a&gt;) from the README references or the navigation on the left side.&lt;/p&gt;

&lt;p&gt;A small problem I noticed is that markdown tables (from README) aren't supported yet. I still find this documentation hosting awesome for either when the repo is a shard (for others to reference), or when you/team need to keep up docs (context) for reference on the project down the road.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>crystal</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploy a Crystal app with Docker and Opentelemetry</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Wed, 15 Feb 2023 12:30:00 +0000</pubDate>
      <link>https://dev.to/fdocr/deploy-a-crystal-app-with-docker-and-opentelemetry-24cp</link>
      <guid>https://dev.to/fdocr/deploy-a-crystal-app-with-docker-and-opentelemetry-24cp</guid>
      <description>&lt;p&gt;This is the continuation of the previous post in this series about a Battlesnake server.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/fdocr" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F19165%2Fefa51160-41cf-448d-909b-6ad82cec68d2.jpg" alt="fdocr"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Learning Crystal with Battlesnake&lt;/h2&gt;
      &lt;h3&gt;Fernando ・ Feb 14 '23&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#crystal&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#opensource&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#api&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;This one will talk about what the deploy process looks like and some Opentelemetry data collected from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digitalocean App Platform with Dockerfile
&lt;/h2&gt;

&lt;p&gt;I've been a Digitalocean customer/user for a while now so I gave their &lt;a href="https://www.digitalocean.com/products/app-platform" rel="noopener noreferrer"&gt;App Platform&lt;/a&gt; PaaS a try. They use buildpacks or detect a &lt;code&gt;Dockerfile&lt;/code&gt; which will build and run your app, and I went with the latter (not sure if the Crystal buildpack is supported somewhow).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jtway.co/dockerfile-for-a-crystal-application-1e9db24efbc2" rel="noopener noreferrer"&gt;Michael Nikitochkin's article&lt;/a&gt; taught me ~95% of what I needed to come up with an efficient container deployment with a small footprint image (alpine).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;crystallang/crystal:1.7.2-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /opt&lt;/span&gt;
&lt;span class="c"&gt;# Cache dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./shard.yml ./shard.lock /opt/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;shards &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# Build a binary&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /opt/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;crystal build &lt;span class="nt"&gt;--static&lt;/span&gt; &lt;span class="nt"&gt;--release&lt;/span&gt; ./src/app.cr
&lt;span class="c"&gt;# ===============&lt;/span&gt;
&lt;span class="c"&gt;# Result image with one layer&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:latest&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /opt/app .&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["./app", "-p", "8080"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is deployed with every commit to &lt;code&gt;main&lt;/code&gt; and cached layers are nice. The entire process takes ~2min40s on average when a docker image needs to be built (with cached dependencies) and when the deploy was triggered by a config update (i.e. ENV variable update) it takes under 2min on averge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opentelemetry
&lt;/h2&gt;

&lt;p&gt;To understand what's going on I added &lt;a href="https://github.com/jgaskins/opentelemetry" rel="noopener noreferrer"&gt;Opentelemetry&lt;/a&gt; tracing with &lt;a href="https://www.honeycomb.io/" rel="noopener noreferrer"&gt;Honeycomb.io&lt;/a&gt;. Some cool stats from a few days of ranking on the leaderboards:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13y5stbue72hijvzqyyx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13y5stbue72hijvzqyyx.png" alt="honeycomb opentelemetry stats" width="800" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can tell leaderboards run on specific hours and the server sits there without much activity about half the time. My favorite stat there is &lt;code&gt;P99&lt;/code&gt; on ~3.1 milliseconds with all that logic running (from &lt;a href="https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj"&gt;previous post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Lots of responses manage to respond in microseconds though, as seen in the runtime logs (below) and the &lt;code&gt;P50&lt;/code&gt; stat on Honeycomb (above):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94hciqkm5den8vspadgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94hciqkm5den8vspadgw.png" alt="microsecond response times" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All in all pretty sweet ⚡️&lt;/p&gt;

&lt;p&gt;More about other learnings from this project to come soon.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Learning Crystal with Battlesnake</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Tue, 14 Feb 2023 16:47:32 +0000</pubDate>
      <link>https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj</link>
      <guid>https://dev.to/fdocr/learning-crystal-with-battlesnake-3chj</guid>
      <description>&lt;p&gt;Recently I've been interested in &lt;a href="https://crystal-lang.org/" rel="noopener noreferrer"&gt;Crystal lang&lt;/a&gt; so I worked on a Battlesnake implementation to get more practice under my belt. I'm sharing an overview of it in this post and the code is &lt;a href="https://github.com/fdocr/crystalsnake" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://play.battlesnake.com/" rel="noopener noreferrer"&gt;Battlesnake&lt;/a&gt; is a multiplayer game where a small server you write plays a &lt;em&gt;survival-style&lt;/em&gt; snake game paired with snakes implemented by others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;I'm using &lt;a href="https://github.com/fdocr/CrystalSnake" rel="noopener noreferrer"&gt;Kemal&lt;/a&gt; for framework. The simplicity (Ruby/Sinatra similarity) won me over when paired with Crystal performance. This is what the entire Battlenake API implementation looks like at the time of this writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"apiversion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"fdocr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SNAKE_COLOR"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"#e3dada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"head"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SNAKE_HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"tail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SNAKE_TAIL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\{\{&lt;/span&gt; &lt;span class="sb"&gt;`shards version "&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;__DIR__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chomp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt; &lt;span class="p"&gt;\}\}&lt;/span&gt;
  &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"/start"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"/move"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"STRATEGY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s2"&gt;"RandomValid"&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"RandomValid"&lt;/span&gt;
    &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RandomValid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"ChaseClosestFood"&lt;/span&gt;
    &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ChaseClosestFood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"ChaseRandomFood"&lt;/span&gt;
    &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ChaseRandomFood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"CautiousCarol"&lt;/span&gt;
    &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CautiousCarol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RandomValid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"shout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Moving &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"/end"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quite Ruby-like so I hope it's easy to follow along whether you've written Crystal before or not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/start&lt;/code&gt; + &lt;code&gt;/end&lt;/code&gt; aren't actually doing anything other than parsing the payload provided and &lt;code&gt;/&lt;/code&gt; responds with the snake's skin customizations. &lt;code&gt;/move&lt;/code&gt; picks from the existing strategies to respond based on an ENV variable.&lt;/p&gt;

&lt;p&gt;The devil is in the details since the bulk of the logic lives in the models (parse game context from request payload, house a few utility methods, etc), strategies to respond with a move and utility algorithms. They all feel easy to follow along though.&lt;/p&gt;

&lt;p&gt;IMO notes/thoughts so far:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I haven't felt the need to work with macros yet so it's been very similar to a Ruby project.&lt;/p&gt;

&lt;p&gt;Reworking data structures on the fly (while working/re-working strategies) took some new time/energy to get right compared to Ruby. There's been close to 0 debugging time related to exceptions I would've likely had to catch when putting together a Ruby script because of the compiler (maybe? likely?).&lt;/p&gt;

&lt;p&gt;I have a clunky &lt;code&gt;env.params.json.to_json&lt;/code&gt; up there that bothers me a bit. I think that could be cleaned up by figuring out the way to get the raw (String) payload instead of the Kemal parsed (Hash/Object) parameter though. That's a &lt;code&gt;TODO&lt;/code&gt; for now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Strategies &amp;amp; Algorithms implemented
&lt;/h2&gt;

&lt;p&gt;Since Crystal is object oriented I created an abstract/virtual class that all strategies inherit from.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RandomValid&lt;/code&gt; considers all the valid moves for your snake on the current context and picks one at random

&lt;ul&gt;
&lt;li&gt;Takes into account walls and other snakes' current position&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;ChaseClosestFood&lt;/code&gt; and &lt;code&gt;ChaseRandomFood&lt;/code&gt; both aim towards food on the board and differ in how they pick their target (Closest vs Random)

&lt;ul&gt;
&lt;li&gt;They re-use &lt;code&gt;RandomValid&lt;/code&gt; in some scenarios (i.e. can't reach food)&lt;/li&gt;
&lt;li&gt;They use &lt;a href="https://en.wikipedia.org/wiki/A*_search_algorithm" rel="noopener noreferrer"&gt;A* search algorithm&lt;/a&gt; to find the best route towards a target (food in this case but can be used on any Point on the board)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The re-usability of strategies makes for a cool way to mix &amp;amp; match them, meaning I can build on top of each other or existing algorithms. The food chaser ones won all the challenges (not sure if challenges are still a thing after the recent UI revamp) so that started to show some potential.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CautiousCarol&lt;/code&gt; is a heuristic strategy I implemented and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight crystal"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Strategy that chases the closest available food from the board with caution&lt;/span&gt;
&lt;span class="c1"&gt;# against head-to-head collisions. When a potentially dangerous move is in the&lt;/span&gt;
&lt;span class="c1"&gt;# way it analyzes the other valid moves available and picks the one with the &lt;/span&gt;
&lt;span class="c1"&gt;# most open area of the board to avoid enclosed spaces.&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CautiousCarol&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Strategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;move&lt;/span&gt;
    &lt;span class="n"&gt;valid_moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid_moves&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;RandomValid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valid_moves&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:moves&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;

    &lt;span class="c1"&gt;# Check for head-to-head collision possibilities&lt;/span&gt;
    &lt;span class="n"&gt;dangerous_moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Point&lt;/span&gt;
    &lt;span class="n"&gt;enemies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snakes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;enemies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;snake&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;snake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
      &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;snake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;

      &lt;span class="c1"&gt;# Check if we share valid moves (meeting point for collision)&lt;/span&gt;
      &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid_moves&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="ss"&gt;:neighbors&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;meeting_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;valid_moves&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:neighbors&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;meeting_point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
        &lt;span class="n"&gt;dangerous_moves&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Attempt to chase closest food unless dangerous move is detected&lt;/span&gt;
    &lt;span class="n"&gt;closest_food&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ChaseClosestFood&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;
    &lt;span class="n"&gt;target_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closest_food&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;safe_move&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dangerous_moves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;target_point&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;closest_food&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;safe_move&lt;/span&gt;

    &lt;span class="c1"&gt;# Leftover valid moves (not chasing closest food anymore) &amp;amp; fallback to&lt;/span&gt;
    &lt;span class="c1"&gt;# RandomValid if no other moves are available (likely run into own death)&lt;/span&gt;
    &lt;span class="n"&gt;safe_moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;valid_moves&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:moves&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;closest_food&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;RandomValid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;safe_moves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt;

    &lt;span class="c1"&gt;# Use flood fill to pick the valid move with more space to spare as an &lt;/span&gt;
    &lt;span class="c1"&gt;# attempt to avoid small areas&lt;/span&gt;
    &lt;span class="n"&gt;flood_fills&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;Int32&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;
    &lt;span class="n"&gt;contexts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;BattleSnake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Context&lt;/span&gt;
    &lt;span class="n"&gt;safe_moves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;safe_moves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;area_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flood_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;
      &lt;span class="n"&gt;flood_fills&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;area_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;move&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Pick the direction with the largest available area&lt;/span&gt;
    &lt;span class="n"&gt;flood_fills&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;flood_fills&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's likely a lot to take in, but if interested in comparing it to Ruby or other languages give it a try and ask me about it. I'm not that experienced in Crystal so there are likely details that could improve this.&lt;/p&gt;

&lt;p&gt;Overall there's model manipulation (i.e. using the &lt;code&gt;snakes&lt;/code&gt; from the &lt;code&gt;board&lt;/code&gt; of the current &lt;code&gt;context&lt;/code&gt;), utility method usage from the models (i.e. calls to &lt;code&gt;@context.valid_moves&lt;/code&gt;), reusing of &lt;code&gt;RandomValid&lt;/code&gt; &amp;amp; &lt;code&gt;ChaseClosestFood&lt;/code&gt; strategies, and &lt;code&gt;Utils.flood_fill&lt;/code&gt;, which is my implementation of &lt;a href="https://en.wikipedia.org/wiki/Flood_fill" rel="noopener noreferrer"&gt;Flood Fill algorithm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My first version of this strategy was a clunky attempt to brute-force a "look ahead" simulation of all possible scenarios. I scrapped that idea in favor of the above for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaderboard results
&lt;/h2&gt;

&lt;p&gt;That first version of &lt;code&gt;CautiosCarol&lt;/code&gt; performed &lt;em&gt;"alright"&lt;/em&gt;. I was surprised it earned points and ranked on the board overall since I knew it wasn't great. After a couple of days on the Standard leaderboard (4 players on 11x11 board) it ranked like this out of ~100 snakes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7qbb271kwvc65kua920.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7qbb271kwvc65kua920.png" alt="Standard leaderboard first run" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the fix with the code above on &lt;code&gt;CautiousCarol&lt;/code&gt; and another couple of days on the same leaderboard I saw some improvement:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuczczn72byra3ckrscha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuczczn72byra3ckrscha.png" alt="Standard leaderboard first run" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also realized that same snake could rank on the Duels leaderboard too (1v1 on 11x11 board). Apparently &lt;code&gt;CautiousCarol&lt;/code&gt; performs better there (in ranking position but fewer points so I'm not sure how much better really 😅)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg52ey42xrys67zus8584.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg52ey42xrys67zus8584.png" alt="Duels leaderboard run" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Crystal has felt easy to read and write for me, quite nice to use overall and very similar to Ruby in lots of ways (specially at this level without diving into macros). The project &lt;a href="https://github.com/fdocr/CrystalSnake" rel="noopener noreferrer"&gt;is open source on GitHub&lt;/a&gt; if interested in checking it out.&lt;/p&gt;

&lt;p&gt;I hope this is the first of a few posts on other specific Crystal learnings I had while experimenting here. Also might update if I come up with new/better strategies.&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>featurerequest</category>
      <category>ai</category>
    </item>
    <item>
      <title>Using Chart.js plugins with webpack</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Sun, 03 Jul 2022 12:57:02 +0000</pubDate>
      <link>https://dev.to/fdocr/using-chartjs-plugins-with-webpack-3774</link>
      <guid>https://dev.to/fdocr/using-chartjs-plugins-with-webpack-3774</guid>
      <description>&lt;p&gt;This post shares the steps I took to include &lt;a href="https://www.chartjs.org/" rel="noopener noreferrer"&gt;Chart.js&lt;/a&gt; with candlestick charts (using a plugin) on my &lt;em&gt;weekend/pet project&lt;/em&gt;. I'm using &lt;a href="https://webpack.js.org/" rel="noopener noreferrer"&gt;webpack&lt;/a&gt; to bundle its JavaScript code &amp;amp; dependencies.&lt;/p&gt;

&lt;p&gt;I'm trying to write this in a framework agnostic way, but the fact is I'm using webpack in a &lt;a href="https://rubyonrails.org/" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt; app (via &lt;a href="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&gt;&lt;code&gt;jsbundling-rails&lt;/code&gt;&lt;/a&gt;), so there will be some specifics that may differ from someone else's approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;I'm using the &lt;a href="https://www.npmjs.com/package/chartjs-chart-financial" rel="noopener noreferrer"&gt;chartjs-chart-financial&lt;/a&gt; plugin so, first step is to install Chart.js and all necessary dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add chart.js luxon chartjs-adapter-luxon chartjs-chart-financial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're using React or Vue there's a good chance you'll be better off installing and using a &lt;a href="https://github.com/reactchartjs/react-chartjs-2" rel="noopener noreferrer"&gt;React wrapper&lt;/a&gt; or &lt;a href="https://github.com/apertureless/vue-chartjs" rel="noopener noreferrer"&gt;Vue wrapper&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initialize
&lt;/h2&gt;

&lt;p&gt;Add this to &lt;code&gt;application.js&lt;/code&gt; (or similar entrypoint to the app).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Chart&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chart.js/auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;luxon&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chartjs-adapter-luxon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;finChart&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chartjs-chart-financial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Register the plugin's custom controllers&lt;/span&gt;
&lt;span class="nx"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finChart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CandlestickController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finChart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CandlestickElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Ensure the Chart class is loaded in the global context&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Chart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;I'm on Rails and using &lt;a href="https://hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt;, so my JavaScript code lives in Stimulus controllers. &lt;/p&gt;

&lt;p&gt;I can use the &lt;code&gt;Chart&lt;/code&gt; class from a &lt;code&gt;connect()&lt;/code&gt; method in my controller because it was loaded globally. Each page visit includes the controller and the chart's data it needs in the HTML itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// `this.canvasTarget` references the Chart.js canvas HTML element&lt;/span&gt;
&lt;span class="c1"&gt;// this might differ depending on your framework (or lack thereof)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvasTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// `this.dataTarget` references the JSON data embedded in the HMTL&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Traditional Chart.js usage but with a plugin's custom Chart type&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;myChart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;candlestick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- data goes here&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.chartjs.org/docs/latest/" rel="noopener noreferrer"&gt;Official docs here for Chart.js specifics&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  That's it
&lt;/h2&gt;

&lt;p&gt;With some data and options added to the chart you're all set. The example below has line charts overlayed with the Candlestick chart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5utfbsaecursqfg7e4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5utfbsaecursqfg7e4u.png" alt="Chart.js candlestick chart hodl"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural choices &amp;amp; tradeoffs (RoR specific)
&lt;/h2&gt;

&lt;p&gt;Embedding a &lt;em&gt;large-ish JSON dataset&lt;/em&gt; in the HTML, times the number of charts in each page makes the HTTP response sizes larger than with an alternative approach (i.e. async API request). This problem likely increases the perceived response times, more so in slower connections.&lt;/p&gt;

&lt;p&gt;Concious of this performance penalty I decomposed the charts in separate endpoints using &lt;a href="https://turbo.hotwired.dev/handbook/frames#lazy-loading-frames" rel="noopener noreferrer"&gt;lazy loaded frames&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is also nifty for caching purposes since the chart's dataset is cached with (inside) the HTML response itself. Chart.js is initialized only once in the app's lifetime and is ready to use as soon as the controller renders on screen, ideally via cached network request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kudos
&lt;/h2&gt;

&lt;p&gt;I found help and inspiration in &lt;a href="https://dev.to/wanderingsoul/rails-6-webpacker-and-chartjs-2kek"&gt;@wanderingsoul's Chart.js post&lt;/a&gt;, Chart.js/Hotwire's docs, the plugin's GitHub docs, and lots of trial &amp;amp; error 🙃&lt;/p&gt;

&lt;p&gt;Pura vida.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webpack</category>
      <category>rails</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Integrating the Passport with the Forem Ecosystem</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Mon, 06 Dec 2021 17:18:57 +0000</pubDate>
      <link>https://dev.to/devteam/integrating-the-passport-with-the-forem-ecosystem-42ea</link>
      <guid>https://dev.to/devteam/integrating-the-passport-with-the-forem-ecosystem-42ea</guid>
      <description>&lt;p&gt;This is the second post of a series that covers the new &lt;a href="https://passport.forem.com/"&gt;Forem Passport&lt;/a&gt; service provider, which integrates with the Forem open source software behind DEV and &lt;a href="https://discover.forem.com"&gt;other communities&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Upcoming Forem Passport Projects&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This service provider is such an exciting project in my view, because of the power and flexibility baked into the OAuth protocol. Not only does it help solve a problem we had (&lt;em&gt;i.e. compliance and support across different platforms&lt;/em&gt;), it also interacts with many different core and ecosystem projects.&lt;/p&gt;

&lt;p&gt;Some of the projects that relate to Forem Passport in one way or another are:&lt;/p&gt;

&lt;h3&gt;
  
  
  Android Mobile App
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;We just released the first few versions of our beta Android app!&lt;/p&gt;

&lt;p&gt;Are you interested in being part of the closed beta? Do you want to be notified when the open beta rolls out? Drop a comment below and we’ll contact you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  iOS Mobile App
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Version &lt;code&gt;1.2.2&lt;/code&gt; includes the Passport integration already! &lt;a href="https://apps.apple.com/us/app/forem/id1536933197"&gt;Try it out now&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Better Tooling for Creators to Fight Abuse
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;In a network of Forem sites, smaller instances might benefit from abuse control measures taken in bigger instances, like DEV. This could include propagation of banished users (at least to flag as likely abusers) throughout the network, making everyone safer.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;em&gt;Abuse in this case refers to anything that doesn’t adhere to the Forem's code of conduct or terms and conditions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Please note that this is still in ideation and hasn’t yet been discussed in great depth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Better Integration Between the Passport and the &lt;code&gt;/admin&lt;/code&gt; Dash for Creators
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;We can automate the process of enabling the Forem authentication for a creator (directly from their &lt;code&gt;/admin&lt;/code&gt; dashboard) a lot more than its current form. &lt;/p&gt;

&lt;p&gt;This idea has definitely been discussed and it’s actually a high priority goal. We want to make it as easy as possible for creators to enable the Forem passport integration so more users can benefit from it across the ecosystem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Push Notifications for Mobile Apps on Self-Hosted Forems
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;We can’t openly give out our PN certificates to deliver push notifications to self hosted Forems. So, in order to overcome this we could rely on the authorization and not authentication aspect of the OAuth protocol.&lt;/p&gt;

&lt;p&gt;The objective of this project is to implement a mechanism relay PN delivery to the Passport via API calls if the certificates aren’t available (self hosted sites).&lt;/p&gt;

&lt;p&gt;This would only work for users that have connected their accounts with the Passport, because that’s the key part of how we would avoid abuse from any bad actor if they happen to spam their users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Allowing for Social login within Forem Passport itself
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;We heard you like service providers, so we’re planning to integrate service providers on our service provider 😆&lt;/p&gt;

&lt;p&gt;In all seriousness, since we have more control over Forem Passport, we could implement many customized authentication providers and make them compatible with our whole ecosystem (i.e. mobile apps). &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Easier account management across many Forem sites
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;You could &lt;em&gt;in theory&lt;/em&gt; propagate a new profile picture across many Forem sites at once, directly from Forem Passport. &lt;/p&gt;

&lt;p&gt;This isn’t implemented and hasn’t even been discussed in depth either, but OAuth should allows us to make some of these integrations possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Open sourcing the Passport codebase
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The core Forem software has been open source for a while now, and we want to open source this project as well in the near future for many reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our intention is to solidify the Passport project a bit more (i.e. have better abuse control) and other details sorted out before publishing the repo. We’ll be sure to communicate when this happens!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Invitations &amp;amp; Closing Note
&lt;/h2&gt;

&lt;p&gt;Phew! We've covered a lot in this series so far. 😄&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We’re interested in your feedback on all of the above, so please share your thoughts with us in the comments below.&lt;/li&gt;
&lt;li&gt;Do you already have an account on &lt;a href="https://passport.forem.com"&gt;https://passport.forem.com&lt;/a&gt;? Please leave us a comment below! What are your thoughts? Which upcoming project from the list above interests you most? Any other crazy ideas you might think would be cool to integrate with the Passport project?&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apps.apple.com/us/app/forem/id1536933197"&gt;Try it out Version &lt;code&gt;1.2.2&lt;/code&gt; of the Forem iOS app&lt;/a&gt;, which includes the Passport integration&lt;/li&gt;
&lt;li&gt;Reminder that you can comment below if you'd like to be part of our closed beta test for the Forem Android app &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Closing Note:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We’re actively working on Forem Passport (i.e. design overhaul, abuse control features, new integrations, etc) so expect changes, big and small, to roll out on a weekly basis.&lt;/p&gt;




&lt;p&gt;In the next post in this series, I’m going to explain how we managed to implement the Passport project from a technical perspective. Keep an eye out for it in the next few days.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Forem’s Approach to Decentralized Authentication and Authorization</title>
      <dc:creator>Fernando</dc:creator>
      <pubDate>Mon, 06 Dec 2021 17:18:40 +0000</pubDate>
      <link>https://dev.to/devteam/forems-approach-to-decentralized-authentication-and-authorization-49a1</link>
      <guid>https://dev.to/devteam/forems-approach-to-decentralized-authentication-and-authorization-49a1</guid>
      <description>&lt;p&gt;This is the first part in a series that will cover the new &lt;a href="https://passport.forem.com/" rel="noopener noreferrer"&gt;Forem Passport&lt;/a&gt; service provider, which integrates with the Forem open source software behind DEV and &lt;a href="https://discover.forem.com" rel="noopener noreferrer"&gt;other communities&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief History of Our Authentication Options
&lt;/h2&gt;

&lt;p&gt;Since its early days, DEV relied solely on authentication providers to set up and access user accounts. There are pros and cons with that decision, but I’d say it’s working well, overall, with over 756,805 registered users (and counting!) today.&lt;/p&gt;

&lt;p&gt;After DEV became part of the larger Forem umbrella, we’ve continued to expand from GitHub and Twitter authentication &lt;a href="https://admin.forem.com/docs/advanced-customization/config/authentication" rel="noopener noreferrer"&gt;to allowing many other use cases&lt;/a&gt; like invite only (private) or email+password registration, to name a few.&lt;/p&gt;

&lt;p&gt;Despite these new authentication options, our backend implementation of them hasn’t changed much. This is because we rely on &lt;a href="https://auth0.com/intro-to-iam/what-is-oauth-2/" rel="noopener noreferrer"&gt;OAuth&lt;/a&gt; (an open-standard authorization protocol/framework) and a &lt;a href="https://github.com/forem/forem/tree/main/app/services/authentication/providers" rel="noopener noreferrer"&gt;polymorphic approach&lt;/a&gt;, which allows our team to add new providers that adhere to the OAuth protocol.&lt;/p&gt;

&lt;p&gt;Below, I'll explain how these authentication factors connect with external influences (like Apple and Facebook), leading us to build our own authentication provider. If you’re more interested in the implementation (&lt;em&gt;show me the code&lt;/em&gt;), check out the next part in this series, coming soon!&lt;/p&gt;

&lt;h2&gt;
  
  
  How Forem is Decentralized
&lt;/h2&gt;

&lt;p&gt;Empowering community by giving ownership to creators (decentralization) is the bedrock of Forem's mission. Anyone that hosts their own Forem site becomes part of the world wide web (WWW), just like any other website does. It becomes a bit more interesting when we look at the &lt;a href="https://discover.forem.com" rel="noopener noreferrer"&gt;network of Forem sites&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In case you missed it, a while ago we announced how you can now self-host your own Forem. Starting your own community with data ownership and the transparency of the open source (much like DEV) is finally possible 🌱 &lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/devteam" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1%2Fd908a186-5651-4a5a-9f76-15200bc6801f.jpg" alt="The DEV Team"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1%2Ff451a206-11c8-4e3d-8936-143d0a7e65bb.png" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/devteam/forem-self-host-is-now-officially-supported-16h0" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Forem Self-Host is Now Officially Supported&lt;/h2&gt;
      &lt;h3&gt;Ben Halpern for The DEV Team ・ Jul 20 '21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#forem&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#opensource&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#news&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#meta&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Since we now have a network of sites that run Forem's open source community software, we need to work on the ecosystem around it. So we shipped the Forem iOS app with this in mind.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/ben" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1%2Ff451a206-11c8-4e3d-8936-143d0a7e65bb.png" alt="ben"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/ben/forem-s-approach-to-decentralized-social-media-on-mobile-2e1p" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Forem's Approach to Decentralized Social Media on Mobile&lt;/h2&gt;
      &lt;h3&gt;Ben Halpern ・ Jun 3 '21&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#meta&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#forem&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#ios&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#mobile&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Forem for iOS means that you can browse many different Forem communities in one centralized app on the go. Since the days of our DEV iOS app (soon to be sunsetted), we have enhanced the mobile browser experience with Push Notifications and many more features. Together, we've accomplished all of this despite relying on a WebView-based implementation, which might be a sensitive topic at times... but that’s a post for another day 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Hurdles: Apple and Facebook
&lt;/h2&gt;

&lt;p&gt;Since a Creator has full ownership of their Forem, we can’t control the authentication methods they choose to enable (this is the point). Apple, on the other hand, requires everyone using their authentication to comply with their guidelines.&lt;/p&gt;

&lt;p&gt;We actually &lt;del&gt;ranted&lt;/del&gt; joked a little about the “SIWA rule” in a &lt;a href="https://www.youtube.com/watch?v=Q0LVviE5gB4" rel="noopener noreferrer"&gt;DEV Twitch Stream&lt;/a&gt; a while ago, along with other mobile/ecosystem related conversations. SIWA is the acronym we use for &lt;code&gt;Sign In With Apple&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Because Apple requires every iOS app that allows for social logins to have SIWA enabled, the Forem iOS app is being rejected by AppStore reviewers when they find some Forems listed in the app &lt;em&gt;don’t&lt;/em&gt; have SIWA enabled by choice.&lt;/p&gt;

&lt;p&gt;Despite being less than ideal, we had to hide all social authentication providers if the Forem site doesn’t have SIWA enabled within the &lt;code&gt;ForemWebView&lt;/code&gt; context (which means it’s being rendered in a mobile app). &lt;a href="https://github.com/forem/forem/pull/14260" rel="noopener noreferrer"&gt;This is the PR for that&lt;/a&gt; in case you’re curious.&lt;/p&gt;

&lt;p&gt;Later, Facebook &lt;a href="https://developers.facebook.com/blog/post/2021/06/28/deprecating-support-fb-login-authentication-android-embedded-browsers/" rel="noopener noreferrer"&gt;shut down their OAuth flow protocol in Android WebView contexts&lt;/a&gt;. According to them, this was a decision made “for security reasons”. &lt;a href="https://github.com/forem/forem/issues/14681" rel="noopener noreferrer"&gt;This is the issue&lt;/a&gt; where some of this conversation has taken place. Similar to the SIWA situation, we’re not able to show the Facebook auth option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication on a Decentralized Network of Forem Communities
&lt;/h2&gt;

&lt;p&gt;With these challenges led us back to a big picture idea discussed over year ago: an SSO solution that would work across Forems. The goal is to make authenticating with any Forem site easier — on any platform or context. Enter &lt;strong&gt;&lt;strong&gt;&lt;a href="https://passport.forem.com" rel="noopener noreferrer"&gt;Forem Passport&lt;/a&gt;&lt;/strong&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;OAuth is incredibly powerful and even somewhat democratic, in the sense that each individual user chooses to authorize or revoke the permissions they’ve given through the OAuth protocol.&lt;/p&gt;

&lt;p&gt;The iOS mobile app now integrates directly with Forem Passport and we’ve started to reach out to creators so they can enable the Forem authentication provider to allow for this better experience across platforms.&lt;/p&gt;

&lt;p&gt;On some Forem sites like DEV, &lt;a href="https://community.codenewbie.org/" rel="noopener noreferrer"&gt;CodeNewbie&lt;/a&gt; and &lt;a href="https://forem.dev/" rel="noopener noreferrer"&gt;forem.dev&lt;/a&gt;, you can already connect your Forem Passport account from your settings or directly register a new account using the Forem auth provider.&lt;/p&gt;




&lt;p&gt;In the next post in this series, I’m going to share the ongoing and upcoming projects we're working on, all of which integrate with the Passport project in one way or another. &lt;a href="https://dev.to/devteam/integrating-the-passport-with-the-forem-ecosystem-42ea"&gt;Read all about it here&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
