<?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: julianrubisch</title>
    <description>The latest articles on DEV Community by julianrubisch (@julianrubisch).</description>
    <link>https://dev.to/julianrubisch</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%2F139689%2Fee4d25c8-810a-4fc9-b644-186b7cd7600c.jpg</url>
      <title>DEV Community: julianrubisch</title>
      <link>https://dev.to/julianrubisch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julianrubisch"/>
    <language>en</language>
    <item>
      <title>Deploying a Rails + SQLite App to a Synology NAS</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Fri, 13 Mar 2026 09:43:26 +0000</pubDate>
      <link>https://dev.to/julianrubisch/deploying-a-rails-sqlite-app-to-a-synology-nas-2l59</link>
      <guid>https://dev.to/julianrubisch/deploying-a-rails-sqlite-app-to-a-synology-nas-2l59</guid>
      <description>&lt;p&gt;I built a small CRM for my consulting business using &lt;a href="https://avohq.io/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; on Rails 8. It tracks contacts, companies, deals, and follow-ups — nothing fancy, but shaped exactly to how I work. Single user. No team access needed.&lt;/p&gt;

&lt;p&gt;The question was where to run it. A $7/month VPS felt wrong for a tool only I use. I don't need uptime guarantees or global availability — I need it accessible from my laptop, phone, and tablet, wherever I am.&lt;/p&gt;

&lt;p&gt;The answer was already sitting in my office: a Synology NAS on my Tailscale network. Rails 8's Solid stack (Cache, Queue, Cable — all SQLite-backed) means the entire app is one process with one database file. That's NAS-friendly. And Tailscale means every device I own can reach it without exposing anything to the public internet.&lt;/p&gt;

&lt;p&gt;Here's how I set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synology DS918+&lt;/strong&gt; — Intel Celeron J3455 (x86_64, quad-core 1.5GHz), 4GB RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSM 7.2.2-72806&lt;/strong&gt; — Container Manager ships Docker daemon 24.0.2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt; installed from Package Center&lt;/li&gt;
&lt;li&gt;SSH access enabled (Control Panel → Terminal &amp;amp; SNMP)&lt;/li&gt;
&lt;li&gt;A Rails app with a working &lt;code&gt;Dockerfile&lt;/code&gt; (Rails 7.1+ generates one by default)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails + Ruby in a container typically consumes 200–400MB. DSM uses around 1–1.5GB. With 4GB total, there's plenty of headroom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Prepare the NAS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create a deploy user
&lt;/h3&gt;

&lt;p&gt;Create a dedicated user via Control Panel → User &amp;amp; Group → Create.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add to the &lt;code&gt;administrators&lt;/code&gt; group (required for SSH access on Synology)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared folders:&lt;/strong&gt; Read/Write on &lt;code&gt;docker&lt;/code&gt; only, No Access on everything else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application permissions:&lt;/strong&gt; Deny all — this user only needs SSH, which comes from the &lt;code&gt;administrators&lt;/code&gt; group membership&lt;/li&gt;
&lt;/ul&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%2Fgo3tmfy9r3xj6260d3qk.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%2Fgo3tmfy9r3xj6260d3qk.png" alt="Permissions for the  raw `docker` endraw  user" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up SSH key authentication
&lt;/h3&gt;

&lt;p&gt;Enable home directories first — without this, &lt;code&gt;ssh-copy-id&lt;/code&gt; fails because there's nowhere to put &lt;code&gt;authorized_keys&lt;/code&gt;. Go to &lt;strong&gt;Control Panel → User &amp;amp; Group → Advanced&lt;/strong&gt; tab, check &lt;strong&gt;Enable user home service&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id deploy@hal.local
ssh deploy@hal.local  &lt;span class="c"&gt;# verify passwordless login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix the Docker PATH problem
&lt;/h3&gt;

&lt;p&gt;This is the single biggest Synology gotcha. Non-interactive SSH commands (how deployment scripts work) get a limited PATH: &lt;code&gt;/usr/bin:/bin:/usr/sbin:/sbin&lt;/code&gt;. Docker lives at &lt;code&gt;/usr/local/bin/docker&lt;/code&gt;. It won't be found.&lt;/p&gt;

&lt;p&gt;SSH into your NAS interactively:&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;# Edit sshd_config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/ssh/sshd_config
&lt;span class="c"&gt;# Find PermitUserEnvironment and set it to: PermitUserEnvironment PATH&lt;/span&gt;

&lt;span class="c"&gt;# Save the full interactive PATH to your SSH environment file&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;PATH | &lt;span class="nb"&gt;tee&lt;/span&gt; ~/.ssh/environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reboot the NAS (simplest way to restart sshd cleanly), then verify from your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local docker &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# Should output: Docker version 24.0.2, build 610b8d0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; DSM updates can reset &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;. After any major update, re-verify with &lt;code&gt;ssh deploy@hal.local docker -v&lt;/code&gt; and re-apply if needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create directories for your app
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /volume1/docker/myapp/db
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /volume1/docker/myapp/storage

&lt;span class="c"&gt;# UID 1000 matches the rails user inside the container&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /volume1/docker/myapp/db /volume1/docker/myapp/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable passwordless sudo for Docker commands
&lt;/h3&gt;

&lt;p&gt;The deploy script runs Docker commands over non-interactive SSH, which can't prompt for a password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "deploy ALL=(ALL) NOPASSWD: /usr/local/bin/docker-compose, /usr/local/bin/docker" &amp;gt; /etc/sudoers.d/deploy'&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;440 /etc/sudoers.d/deploy
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limits passwordless sudo to just Docker commands — no blanket access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up a local Docker registry
&lt;/h3&gt;

&lt;p&gt;A local registry means images never leave your network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local &lt;span class="s2"&gt;"sudo mkdir -p /volume1/docker/registry/data"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the compose file on the NAS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local &lt;span class="s2"&gt;"cat &amp;gt; /volume1/docker/registry/docker-compose.yml &amp;lt;&amp;lt; 'EOF'
services:
  registry:
    image: registry:2
    container_name: registry
    ports:
      - &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;5050:5000&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;
    volumes:
      - /volume1/docker/registry/data:/var/lib/registry
    restart: unless-stopped
EOF"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 5000 is used by DSM, so the registry maps to 5050. Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local &lt;span class="s2"&gt;"cd /volume1/docker/registry &amp;amp;&amp;amp; sudo docker-compose up -d"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://&amp;lt;nas-tailscale-ip&amp;gt;:5050/v2/
&lt;span class="c"&gt;# Should return: {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now configure your local Docker client to trust this HTTP registry. The registry runs plain HTTP, but Docker defaults to HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OrbStack&lt;/strong&gt; — edit &lt;code&gt;~/.orbstack/config/docker.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"insecure-registries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;nas-tailscale-ip&amp;gt;:5050"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker Desktop&lt;/strong&gt; — Settings → Docker Engine, add the same entry.&lt;/p&gt;

&lt;p&gt;Restart after the change. This is safe — traffic runs over Tailscale, which is already encrypted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirm Tailscale is working
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note your NAS's Tailscale IP. That's the address you'll use to access the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Prepare Your Rails App
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configure the database for persistent storage
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;config/database.yml&lt;/code&gt;, point the production database to a path that will be mounted from the host:&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;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/production.sqlite3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/data&lt;/code&gt; as an absolute path (not relative to &lt;code&gt;/rails&lt;/code&gt;) ensures the database lives on the mounted volume and survives container replacements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the Dockerfile
&lt;/h3&gt;

&lt;p&gt;The Rails-generated Dockerfile needs a few modifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the build stage&lt;/strong&gt;, add Node.js and Yarn for asset compilation (skip this if you're not using Vite/esbuild):&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="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      build-essential git libyaml-dev pkg-config nodejs npm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; yarn &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;COPY . .&lt;/code&gt;, install JS dependencies:&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="k"&gt;RUN &lt;/span&gt;yarn &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In the final stage&lt;/strong&gt;, create the &lt;code&gt;/data&lt;/code&gt; directory &lt;em&gt;before&lt;/em&gt; switching to the non-root user. This is the part that tripped me up — &lt;code&gt;mkdir /data&lt;/code&gt; and &lt;code&gt;chown&lt;/code&gt; must happen while still root:&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="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1000 rails &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    useradd rails &lt;span class="nt"&gt;--uid&lt;/span&gt; 1000 &lt;span class="nt"&gt;--gid&lt;/span&gt; 1000 &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;mkdir&lt;/span&gt; /data &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown &lt;/span&gt;rails:rails /data

&lt;span class="c"&gt;# Copy built artifacts BEFORE switching user&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=rails:rails --from=build /rails /rails&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 1000:1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;USER 1000:1000&lt;/code&gt; comes before &lt;code&gt;mkdir /data&lt;/code&gt;, the non-root user won't have permission to create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and push the image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build for linux/amd64 (DS918+ uses Intel Celeron)&lt;/span&gt;
docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;nas-tailscale-ip&amp;gt;:5050/myapp:latest &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Push to the local registry&lt;/span&gt;
docker push &amp;lt;nas-tailscale-ip&amp;gt;:5050/myapp:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--platform linux/amd64&lt;/code&gt; flag is essential if you're building on Apple Silicon. Without it, you'll get an &lt;code&gt;exec format error&lt;/code&gt; when the container starts on the NAS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Deploy to the NAS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The compose file
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;docker-compose.production.yml&lt;/code&gt; in your repo:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:5050/myapp:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
      &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${RAILS_MASTER_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;RAILS_SERVE_STATIC_FILES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;HTTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/myapp/db:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/myapp/storage:/rails/storage&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The port mapping:&lt;/strong&gt; Rails 8 includes Thruster, a small Go proxy that handles asset caching and gzip. Inside the container, Thruster listens on &lt;code&gt;HTTP_PORT&lt;/code&gt; (8080) and proxies to Puma on 3000. Docker maps external 3000 to Thruster's 8080. Thruster can't use its default port 80 because the container runs as a non-root user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The image address:&lt;/strong&gt; &lt;code&gt;localhost:5050&lt;/code&gt; because the NAS pulls from its own registry. Your Mac pushes to the Tailscale IP — same registry, different address. Docker trusts &lt;code&gt;localhost&lt;/code&gt; by default, so no insecure registry config is needed on the NAS side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the .env file on the NAS
&lt;/h3&gt;

&lt;p&gt;One-time setup — this file stays on the NAS and is never committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@hal.local &lt;span class="s2"&gt;"echo 'RAILS_MASTER_KEY=&amp;lt;your-master-key&amp;gt;' &amp;gt; /volume1/docker/myapp/.env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The deploy script
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;bin/deploy&lt;/code&gt; to your repo:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;NAS_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"deploy@hal.local"&lt;/span&gt;
&lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;nas-tailscale-ip&amp;gt;:5050"&lt;/span&gt;
&lt;span class="nv"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;&lt;span class="s2"&gt;/myapp:latest"&lt;/span&gt;
&lt;span class="nv"&gt;APP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/volume1/docker/myapp"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; Building image..."&lt;/span&gt;
docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$IMAGE&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; Pushing to local registry..."&lt;/span&gt;
docker push &lt;span class="nv"&gt;$IMAGE&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; Copying compose file to NAS..."&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;docker-compose.production.yml | ssh &lt;span class="nv"&gt;$NAS_HOST&lt;/span&gt; &lt;span class="s2"&gt;"cat &amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/docker-compose.yml"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; Pulling image and restarting..."&lt;/span&gt;
ssh &lt;span class="nv"&gt;$NAS_HOST&lt;/span&gt; &lt;span class="s2"&gt;"cd &lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; sudo docker-compose pull &amp;amp;&amp;amp; sudo docker-compose up -d"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; Done! App is live."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x bin/deploy
bin/deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, the Docker entrypoint runs &lt;code&gt;db:prepare&lt;/code&gt; automatically — creates the database and runs migrations. Subsequent deploys run pending migrations on container startup.&lt;/p&gt;

&lt;p&gt;Confirm that the container is up and running in Container Manager:&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%2F2981rjakdjndgfhvc5o6.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%2F2981rjakdjndgfhvc5o6.png" alt="DSM Container Manager showing running Rails containers" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;http://&amp;lt;nas-tailscale-ip&amp;gt;:3000&lt;/code&gt; from any device on your Tailscale network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups
&lt;/h2&gt;

&lt;p&gt;This is where running on a NAS pays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  HyperBackup
&lt;/h3&gt;

&lt;p&gt;The database lives at &lt;code&gt;/volume1/docker/myapp/db/&lt;/code&gt; as a regular file on the NAS filesystem. Include it in any HyperBackup task — to an external drive, another NAS, or a cloud destination.&lt;/p&gt;

&lt;p&gt;This is a real advantage over Docker named volumes, which are hidden in &lt;code&gt;/volume1/@docker/volumes/&lt;/code&gt; and invisible to File Station and backup tools.&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%2F3pbugpc2bogz9ahasmc9.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%2F3pbugpc2bogz9ahasmc9.png" alt="File Station showing the SQLite database file at /docker/myapp/db/ — visible and accessible like any regular file on the NAS, unlike Docker named volumes" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Btrfs Snapshots
&lt;/h3&gt;

&lt;p&gt;The DS918+ supports Btrfs. Use Snapshot Replication for point-in-time recovery — especially useful as a safeguard before running migrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /volume1/docker/myapp/db/production.sqlite3 &lt;span class="se"&gt;\&lt;/span&gt;
       /volume1/docker/myapp/db/production.sqlite3.backup.&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;The whole deploy cycle is &lt;code&gt;bin/deploy&lt;/code&gt; — build, push to the local registry, restart the container. No CI pipeline, no cloud provider, no monthly bill. The NAS handles backups through the same tools you're already using for everything else on it.&lt;/p&gt;

&lt;p&gt;If you're building small Rails apps for yourself or a handful of users, this is a setup worth considering. The Solid stack made the architecture possible. The NAS makes the operations trivial.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>sqlite</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>You Know Hotwire Basics. Now What?</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Tue, 10 Mar 2026 16:00:26 +0000</pubDate>
      <link>https://dev.to/julianrubisch/you-know-hotwire-basics-now-what-1gmh</link>
      <guid>https://dev.to/julianrubisch/you-know-hotwire-basics-now-what-1gmh</guid>
      <description>&lt;p&gt;There's no shortage of "Getting Started with Hotwire" content. Install Turbo, add a &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;, watch the page update without a full reload. Great. You've done the tutorial.&lt;/p&gt;

&lt;p&gt;But then you're back in your app, staring at a real problem — optimistic UI updates, race conditions on rapid clicks, progressive image loading — and the tutorial didn't cover any of that.&lt;/p&gt;

&lt;p&gt;That gap is why I started the &lt;a href="https://www.hotwire.club" rel="noopener noreferrer"&gt;Hotwire Club&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Is
&lt;/h2&gt;

&lt;p&gt;A biweekly coding challenge series. Every two weeks, a new challenge goes up. Each one isolates a single problem — the kind you'd actually run into building a production app — and strips it down to first principles.&lt;/p&gt;

&lt;p&gt;No video courses. No 40-part curriculum. Just a focused problem, a live coding environment on StackBlitz, and your brain.&lt;/p&gt;

&lt;p&gt;We've published 45+ challenges since April 2023, covering Turbo Drive, Turbo Frames, Turbo Streams, and Stimulus.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Challenge Looks Like
&lt;/h2&gt;

&lt;p&gt;Every challenge follows the same structure: &lt;strong&gt;Premise&lt;/strong&gt;, &lt;strong&gt;Starting Point&lt;/strong&gt;, &lt;strong&gt;Challenge&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Premise&lt;/strong&gt; frames the problem. Take our most popular challenge, &lt;a href="https://www.hotwire.club/blog/2024-03-26-optimistic-ui-with-turbo-8-morphs/" rel="noopener noreferrer"&gt;Optimistic UI with Turbo 8 Morphs&lt;/a&gt;. It opens with a bit of napkin math:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's assume our server lives in Chicago, and the client is in Amsterdam. That's 6,604 km. Taking the speed of light into account, the lowest possible latency is &lt;strong&gt;22ms, one way&lt;/strong&gt;. Round it up to 50ms. Where does that put us?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User clicks a button → 50ms until it reaches the server&lt;/li&gt;
&lt;li&gt;Server calculates response → 100ms (fast)&lt;/li&gt;
&lt;li&gt;Answer is returned → again 50ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 200ms already. Way beyond the "perceived as instant" limit of 100ms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;Starting Point&lt;/strong&gt; gives you a pre-built scaffold on StackBlitz — a working app with a deliberate gap. You don't build from scratch. You fill in the missing piece.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Challenge&lt;/strong&gt; tells you exactly what to implement. In this case: render an inline &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; to swap the UI immediately on click, then let Turbo 8's morph reconcile the DOM when the server responds. The favorite button updates instantly. The server catches up in the background.&lt;/p&gt;

&lt;p&gt;Another popular one: &lt;a href="https://www.hotwire.club/blog/2024-04-23-stimulus-progressive-image-loading-blurhash/" rel="noopener noreferrer"&gt;Progressive Image Loading with Blurhash&lt;/a&gt;. It takes a real performance problem — images tanking your Largest Contentful Paint — and turns it into a Stimulus controller challenge. Paint a blurhash to a canvas, fade in the real image when it loads. Practical, visual, and you walk away with a pattern you can drop into a real app.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Rails Required
&lt;/h2&gt;

&lt;p&gt;Here's a thing that gets lost in the discourse: Turbo and Stimulus are &lt;strong&gt;JavaScript libraries&lt;/strong&gt;. They're not welded to Rails.&lt;/p&gt;

&lt;p&gt;All our challenges run on StackBlitz with a Node.js backend. You don't need a Ruby environment. You don't need to set up a Rails app. Open the link, read the challenge, write code. If you use Django, Laravel, Phoenix, or plain HTML — the patterns still apply.&lt;/p&gt;

&lt;p&gt;We do use Rails conventions as reference points (you have to imagine that the Node handler is a Rails controller), but the actual code you write is JavaScript and HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who It's For
&lt;/h2&gt;

&lt;p&gt;You already know what &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; does. You've read the Stimulus handbook. You want to go deeper.&lt;/p&gt;

&lt;p&gt;As Teddy Valente, CTO at Wobee, put it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The Hotwire Club is a fantastic resource for anyone who already has a good knowledge of Hotwire and wants to deepen their mastery. The exercises are always interesting and very well written. The answers are always detailed and to the point. You can even find exercises that will be useful in your work and for new features."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If that sounds like where you're at, this is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free Challenges, Optional Extras
&lt;/h2&gt;

&lt;p&gt;Every challenge is free. The write-up, the StackBlitz environment, the problem — all open. About 2/3 of all solutions are free too.&lt;/p&gt;

&lt;p&gt;If you want sample solutions and access to a private Discord where people discuss approaches, there's a &lt;a href="https://www.patreon.com/TheHotwireClub" rel="noopener noreferrer"&gt;Patreon&lt;/a&gt; starting at $5/month. But the challenges themselves? Those are yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try One
&lt;/h2&gt;

&lt;p&gt;Pick a challenge and see if it clicks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.hotwire.club/blog/2024-03-26-optimistic-ui-with-turbo-8-morphs/" rel="noopener noreferrer"&gt;Optimistic UI with Turbo 8 Morphs&lt;/a&gt; — make a favorite button feel instant using inline Turbo Streams and morph reconciliation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hotwire.club/blog/2024-04-23-stimulus-progressive-image-loading-blurhash/" rel="noopener noreferrer"&gt;Progressive Image Loading with Blurhash&lt;/a&gt; — improve LCP with placeholder blurhashes and a Stimulus controller&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.hotwire.club" rel="noopener noreferrer"&gt;Browse all challenges&lt;/a&gt; — 45+ and counting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See you in the club.&lt;/p&gt;

</description>
      <category>hotwire</category>
      <category>turbo</category>
      <category>stimulus</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Measuring the Impact of Feature Flags in Ruby on Rails with AppSignal</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 16 Oct 2024 14:07:00 +0000</pubDate>
      <link>https://dev.to/appsignal/measuring-the-impact-of-feature-flags-in-ruby-on-rails-with-appsignal-4mk7</link>
      <guid>https://dev.to/appsignal/measuring-the-impact-of-feature-flags-in-ruby-on-rails-with-appsignal-4mk7</guid>
      <description>&lt;p&gt;Feature flags are a powerful tool in software development, allowing developers to control the behavior of an application at runtime without deploying new code. They enable teams to test new features, perform A/B testing, and roll out changes gradually.&lt;/p&gt;

&lt;p&gt;In Ruby on Rails, feature flags can be managed using diverse tools, the most popular being the Flipper gem. This article will explore implementing and measuring the impact of feature flags in a Solidus storefront using Flipper and AppSignal's custom metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Feature Flags in Rails, Again?
&lt;/h2&gt;

&lt;p&gt;If you are looking for an introduction to the subject, &lt;a href="https://blog.appsignal.com/2022/06/08/add-feature-flags-in-ruby-on-rails-with-flipper.html" rel="noopener noreferrer"&gt;check out the post Add Feature Flags in Ruby on Rails with Flipper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a nutshell, though, feature flags are a way to influence how your application behaves &lt;strong&gt;at runtime&lt;/strong&gt;, without having to deploy new code. The simplest type of feature flags are environment variables. Every Ruby on Rails application uses them out of the box. One example is the configuration of application server concurrency using &lt;code&gt;ENV['WEB_CONCURRENCY']&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, there are other ways to manage feature flags, such as using a persistence layer like ActiveRecord or Redis. A comprehensive way to do this is offered by the &lt;a href="https://www.flippercloud.io/docs" rel="noopener noreferrer"&gt;Flipper gem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The following snippet exemplifies how the &lt;code&gt;performance_improvement&lt;/code&gt; feature flag is evaluated for a given user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:performance_improvement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we will set up a &lt;a href="https://github.com/solidusio/solidus" rel="noopener noreferrer"&gt;Solidus&lt;/a&gt; storefront to start experimenting with feature flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Example App: A Solidus Storefront
&lt;/h2&gt;

&lt;p&gt;To measure the impact of feature flags in a somewhat realistic scenario, let's quickly bootstrap a Solidus store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new coder_swag_store &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;coder_swag_store
bundle add solidus
bin/rails g solidus:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generator will guide you through the process and ask you a few setup questions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Choose the &lt;em&gt;starter&lt;/em&gt; frontend when queried for the frontend type.&lt;/li&gt;
&lt;li&gt; Skip setting up a payment method.&lt;/li&gt;
&lt;li&gt; Choose to mount your Solidus application at &lt;code&gt;/&lt;/code&gt;, since we are using it as a standalone app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Afterward, run &lt;code&gt;bin/dev&lt;/code&gt; from your terminal and you should be good to go. When you go to &lt;code&gt;http://localhost:3000&lt;/code&gt;, you'll see this screen:&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%2F2npnh3z1p1o4nc95u8el.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%2F2npnh3z1p1o4nc95u8el.png" alt="Solidus sample store" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement Feature Flags with Flipper
&lt;/h2&gt;

&lt;p&gt;Now let's implement two exemplary use cases for feature flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A performance improvement.&lt;/li&gt;
&lt;li&gt;An attempt at conversion rate optimization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First of all, though, we have to add the &lt;code&gt;flipper&lt;/code&gt; gem along with its &lt;code&gt;active_record&lt;/code&gt; storage adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add flipper
bundle add flipper-active_record
bin/rails g flipper:setup
bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will set up the required database tables to look up Flipper "gates", i.e., concrete conditionals to evaluate when checking a feature flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing a Performance Improvement
&lt;/h3&gt;

&lt;p&gt;To assess this scenario, we will simulate a slow request in the storefront by adding a &lt;code&gt;sleep 1&lt;/code&gt; call for the unoptimized case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@searcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_searcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;include_images: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@searcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_products&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:performance_improvement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 🚅&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&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;Now, we will use a &lt;a href="https://www.flippercloud.io/docs/features/percentage-of-time" rel="noopener noreferrer"&gt;"percentage of time" strategy&lt;/a&gt; to roll out the optimization across a random set of requests. Open a Rails console and key in the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_percentage_of_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:performance_improvement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;a href="https://github.com/hatoo/oha/" rel="noopener noreferrer"&gt;oha&lt;/a&gt; load testing tool, we can confirm that indeed half of the requests take one second longer than the others:&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="nv"&gt;$ &lt;/span&gt;oha http://localhost:3000/products &lt;span class="nt"&gt;-z&lt;/span&gt; 30s &lt;span class="nt"&gt;-q&lt;/span&gt; 2
Summary:
  Success rate: 100.00%
  Total:        30.0037 secs
  Slowest:      1.2662 secs
  Fastest:      0.1049 secs
  Average:      0.6260 secs
  Requests/sec: 2.0664

  Total data:   2.59 MiB
  Size/request: 44.24 KiB
  Size/sec:     88.47 KiB

Response &lt;span class="nb"&gt;time &lt;/span&gt;histogram:
  0.105 &lt;span class="o"&gt;[&lt;/span&gt;1]  |■
  0.221 &lt;span class="o"&gt;[&lt;/span&gt;24] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.337 &lt;span class="o"&gt;[&lt;/span&gt;8]  |■■■■■■■■■■
  0.453 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  0.569 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  0.686 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  0.802 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  0.918 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  1.034 &lt;span class="o"&gt;[&lt;/span&gt;0]  |
  1.150 &lt;span class="o"&gt;[&lt;/span&gt;6]  |■■■■■■■■
  1.266 &lt;span class="o"&gt;[&lt;/span&gt;21] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing Conversion Rate Optimization
&lt;/h3&gt;

&lt;p&gt;When dealing with user-facing features, for example, changes in the UI, it's often advisable to use a &lt;a href="https://www.flippercloud.io/docs/features/percentage-of-actors" rel="noopener noreferrer"&gt;"percentage of actors" strategy&lt;/a&gt; to roll out flags. This way, every user is consistently offered the same experience.&lt;/p&gt;

&lt;p&gt;So to start, we'll create two users for our e-commerce application. Fire up a Rails console and issue the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Spree&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;User&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;email: &lt;/span&gt;&lt;span class="s2"&gt;"test1@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;login: &lt;/span&gt;&lt;span class="s2"&gt;"test1@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password: &lt;/span&gt;&lt;span class="s2"&gt;"super_safe_password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password_confirmation: &lt;/span&gt;&lt;span class="s2"&gt;"super_safe_password"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Spree&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;User&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;email: &lt;/span&gt;&lt;span class="s2"&gt;"test2@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;login: &lt;/span&gt;&lt;span class="s2"&gt;"test2@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password: &lt;/span&gt;&lt;span class="s2"&gt;"super_safe_password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;password_confirmation: &lt;/span&gt;&lt;span class="s2"&gt;"super_safe_password"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_percentage_of_actors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:conversion_rate_optimization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two sample users and ensures that the feature flag is &lt;strong&gt;consistently&lt;/strong&gt; enabled for one of them.&lt;/p&gt;

&lt;p&gt;To simulate a feature attempting to drive conversion rates up, we'll make the checkout button pulsate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/carts/_cart_footer.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cart-footer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s1"&gt;'carts/cart_adjustments'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cart-footer__total flex justify-between mb-3 text-body-20 p-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spree.total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;: &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-sans-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_total&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cart-footer__primary-action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;order_form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spree.checkout'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"button-primary w-full &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="s1"&gt;'animate-pulse'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:conversion_rate_optimization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spree_current_user&lt;/span&gt;&lt;span class="p"&gt;)&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="ss"&gt;id: &lt;/span&gt;&lt;span class="s1"&gt;'checkout-link'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;name: :checkout&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we log in with both users and arrange the browser windows side by side, &lt;a href="https://blog.appsignal.com/images/blog/2024-10/cro.mp4" rel="noopener noreferrer"&gt;we can observe that indeed the effect is active for one (the left) user&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use AppSignal Custom Metrics to Measure the Impact of Feature Flags
&lt;/h2&gt;

&lt;p&gt;The best feature flag system is useless if there's no way to evaluate its impact. In our example scenario, we simply want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has the performance improvement led to a significant latency reduction?&lt;/li&gt;
&lt;li&gt;Has our pulsating checkout button led to a significantly higher conversion rate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will use &lt;a href="https://docs.appsignal.com/metrics/custom.html" rel="noopener noreferrer"&gt;AppSignal's custom metrics&lt;/a&gt; to measure the pay-off of these optimizations.&lt;/p&gt;

&lt;p&gt;First of all, create a new application in your AppSignal organization and connect it to your app by following the instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add appsignal
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;appsignal &lt;span class="nb"&gt;install &lt;/span&gt;YOUR_APPSIGNAL_UUID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Measuring Latency with a Measurement Metric
&lt;/h3&gt;

&lt;p&gt;We have verified how effective our improvement is with the &lt;code&gt;oha&lt;/code&gt; CLI above, but to make valid judgments we'll install server-side telemetry that reports latency to AppSignal. A &lt;a href="https://docs.appsignal.com/metrics/custom.html#measurement" rel="noopener noreferrer"&gt;measurement metric&lt;/a&gt; allows for exactly that: we will send over response times in milliseconds, and add a &lt;a href="https://docs.appsignal.com/metrics/custom.html#metric-tags" rel="noopener noreferrer"&gt;metric tag&lt;/a&gt; indicating whether our performance optimization was active for a specific request.&lt;/p&gt;

&lt;p&gt;There's a small gotcha here: because we're employing the "Percentage of Time" metric, we have to capture the flag's state in an instance variable so that the same value is used for execution and for reporting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;StoreController&lt;/span&gt;
  &lt;span class="n"&gt;around_action&lt;/span&gt; &lt;span class="ss"&gt;:measure_response_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: :index&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@searcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_searcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;include_images: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@searcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_products&lt;/span&gt;

    &lt;span class="vi"&gt;@performance_improvement_enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:performance_improvement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@performance_improvement_enabled&lt;/span&gt;
      &lt;span class="c1"&gt;# 🚅&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&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;# ...&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;measure_response_time&lt;/span&gt;
    &lt;span class="n"&gt;response_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realtime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="no"&gt;Appsignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_distribution_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"products_response_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;performance_improvement_enabled: &lt;/span&gt;&lt;span class="vi"&gt;@performance_improvement_enabled&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;Now let's repeat the local load testing from above:&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="nv"&gt;$ &lt;/span&gt;oha http://localhost:3000/products &lt;span class="nt"&gt;-z&lt;/span&gt; 30s &lt;span class="nt"&gt;-q&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll look at charting and evaluating this metric in a bit. Before that, let's turn to our second feature flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tallying Conversions with a Count Metric
&lt;/h3&gt;

&lt;p&gt;We'll use a &lt;a href="https://docs.appsignal.com/metrics/custom.html#counter" rel="noopener noreferrer"&gt;counter metric&lt;/a&gt; to count conversions. This is a great choice if all you want to do is just keep a tally of an event.&lt;/p&gt;

&lt;p&gt;To do this, we'll have to open &lt;code&gt;CartsController&lt;/code&gt;, and, for demonstration purposes, add an &lt;code&gt;increment_counter&lt;/code&gt; call if the checkout button is clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/carts_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="n"&gt;authorize!&lt;/span&gt; &lt;span class="ss"&gt;:update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:guest_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

    &lt;span class="no"&gt;Appsignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"checkout_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;optimization_active: &lt;/span&gt;&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:conversion_rate_optimization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spree_current_user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;action: :show&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;Now let's test this by manually opening respective browser windows and clicking the "Checkout" button 3 times, and in another case only once. In this way, we can see if the optimization flag is active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up Custom Dashboards in AppSignal
&lt;/h2&gt;

&lt;p&gt;Our final step is to create informative graphics to make data-informed business decisions. We'll use &lt;a href="https://docs.appsignal.com/metrics/dashboards.html" rel="noopener noreferrer"&gt;AppSignal's dashboards&lt;/a&gt; to achieve this. Let's go through this step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; In the left sidebar, click "Add dashboard" and name it "Feature Flag Evaluation".&lt;/li&gt;
&lt;/ol&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%2Fqsxaxq77h8k9jg0f56mg.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%2Fqsxaxq77h8k9jg0f56mg.png" alt="Add dashboard" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Click "Add Graph" and the &lt;code&gt;products_response_time&lt;/code&gt; metric. Select "mean" to display only averages and apply the &lt;code&gt;performance_improvement_enabled&lt;/code&gt; tag.&lt;/li&gt;
&lt;/ol&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%2Feusdvxfnhs2h1b4nrk9m.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%2Feusdvxfnhs2h1b4nrk9m.png" alt="Add graph — mean" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Click "Add new Graph" to add a chart for the checkout counts. Again, apply the &lt;code&gt;optimization_active&lt;/code&gt; tag.&lt;/li&gt;
&lt;/ol&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%2Fgqbcjvrocgey0ffybdsg.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%2Fgqbcjvrocgey0ffybdsg.png" alt="Add new graph" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now your custom dashboard is ready. In the line graph on the left, you can assert that your performance improvement was effective. On the right, observe how the higher count of checkouts in the optimized case was recorded.&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%2Fmkrsiof8opiw8k39dvp8.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%2Fmkrsiof8opiw8k39dvp8.png" alt="Custom dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We've seen how feature flags offer a flexible and efficient way to manage and deploy new features in a Ruby on Rails application. By using tools like the Flipper gem and AppSignal's custom metrics, developers can not only control feature rollouts, but also measure their impact on performance and user behavior.&lt;/p&gt;

&lt;p&gt;This approach ensures that new features are thoroughly tested and optimized before being fully deployed, ultimately leading to a more stable and user-friendly application. Finally, it can lead to more informed business decisions when gauging the effectiveness of alternative approaches.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>An Introduction to HTTP Caching in Ruby On Rails</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 28 Aug 2024 11:12:14 +0000</pubDate>
      <link>https://dev.to/appsignal/an-introduction-to-http-caching-in-ruby-on-rails-2bda</link>
      <guid>https://dev.to/appsignal/an-introduction-to-http-caching-in-ruby-on-rails-2bda</guid>
      <description>&lt;p&gt;It's 2024, and the HyperText Transfer Protocol (HTTP) is 35 years old. The fact that the vast majority of web traffic still relies on this simple, stateless form of communication is a marvel in itself.&lt;/p&gt;

&lt;p&gt;A first set of content retrieval optimizations were added to the protocol when v1.0 was &lt;a href="https://datatracker.ietf.org/doc/html/rfc2616#section-9.3" rel="noopener noreferrer"&gt;published in 1996&lt;/a&gt;. These include the infamous caching instructions (aka headers) that the client and server use to negotiate whether content needs refreshing.&lt;/p&gt;

&lt;p&gt;28 years later, many web developers still avoid using it like the plague. A great deal of traffic and server load (and thus, latency) can be avoided, though, by using the built-in caching mechanism of the web. Not only does this result in a greatly improved user experience, it can also be a powerful lever to trim down your server bill. In this post, we'll take a look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The basic concepts around HTTP caching.&lt;/li&gt;
&lt;li&gt;What cache layers we can leverage.&lt;/li&gt;
&lt;li&gt;How web caching is configured and controlled.&lt;/li&gt;
&lt;li&gt;How simple it is to cache in Ruby on Rails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Concepts
&lt;/h2&gt;

&lt;p&gt;Before we dive deep into the mechanics of web caching, let's get some definitions out of the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fresh Vs. Stale
&lt;/h3&gt;

&lt;p&gt;Let's assume we have a shared cache server in place that stores responses from our app server for reuse. Later, a client issues a request to our app server. The stored response in our cache is now in one of two states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fresh:&lt;/strong&gt; The response is still valid (we'll see what that means in a second) and will be used to fulfill the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale:&lt;/strong&gt; The response isn't valid anymore. A new response has to be calculated and served by the upstream server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HTTP Cache Layers
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://httpwg.org/specs/rfc9111.html" rel="noopener noreferrer"&gt;HTTP Caching spec&lt;/a&gt; defines two types of cache — &lt;em&gt;shared&lt;/em&gt; and &lt;em&gt;private&lt;/em&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Shared Caches
&lt;/h4&gt;

&lt;p&gt;Shared caches store responses that are reusable among a group of users. Typically, shared caches are implemented at &lt;em&gt;intermediaries&lt;/em&gt;, e.g., Nginx, Caddy, or other reverse proxies. Of course, commercial edge caches like Cloudflare or Fastly also implement shared caches.&lt;/p&gt;

&lt;p&gt;Another flavor of shared caches can be implemented in service workers using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Cache" rel="noopener noreferrer"&gt;Cache API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We can control which responses are eligible for caching with the &lt;code&gt;Cache-Control&lt;/code&gt; header.&lt;/p&gt;

&lt;h4&gt;
  
  
  Private Caches
&lt;/h4&gt;

&lt;p&gt;Private caches are allotted to a single user. You are most likely to encounter a private cache as a &lt;em&gt;browser&lt;/em&gt; component. The main feature here is that the stored responses are not shared with other clients. Any content that the server renders containing personalized data must &lt;em&gt;only&lt;/em&gt; be stored in a private cache. In general, this will be the case for any authenticated route in your Rails app, but there might be exceptions.&lt;/p&gt;

&lt;p&gt;You can force a response to be cached privately using the &lt;code&gt;Cache-Control: private&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling Caching Via the &lt;code&gt;Cache-Control&lt;/code&gt; Header
&lt;/h2&gt;

&lt;p&gt;Although, historically, other headers have been in use (like &lt;code&gt;Expires&lt;/code&gt; or &lt;code&gt;Pragma&lt;/code&gt;), we will focus on the &lt;code&gt;Cache-Control&lt;/code&gt; header here. The &lt;a href="https://httpwg.org/specs/rfc9111.html#field.cache-control" rel="noopener noreferrer"&gt;specification&lt;/a&gt; has an exhaustive description, but we'll focus on the salient parts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Cache-Control&lt;/code&gt; header sets one or several comma-separated &lt;em&gt;directives&lt;/em&gt; that can be further divided into &lt;em&gt;request&lt;/em&gt; and &lt;em&gt;response&lt;/em&gt; directives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request Directives
&lt;/h3&gt;

&lt;p&gt;When requesting a resource from the server, the client (browser) can specify one of the following directives to negotiate how caching should behave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-cache&lt;/code&gt; - Advises the cache to revalidate the response against the origin server. Typically this happens when you force reload a page or disable caching in the browser's developer tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no-store&lt;/code&gt; - Indicates that no cache must store any part of this request or any response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-age&lt;/code&gt; - In seconds, indicates the timespan for which a response from the server is considered fresh. For example, &lt;code&gt;Cache-Control: max-age=3600&lt;/code&gt; would tell the server that any response older than an hour cannot be reused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Response Directives
&lt;/h3&gt;

&lt;p&gt;Essentially, response directives consist of what's above, with different semantics, plus a few more. A cache must obey these directives coming from the origin server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-cache&lt;/code&gt; - A bit counterintuitively, this directive &lt;em&gt;does not&lt;/em&gt; imply that the response cannot be cached. Rather, each cache &lt;em&gt;must revalidate&lt;/em&gt; the response with the origin server for each reuse. This is the &lt;a href="https://github.com/rails/rails/blob/967fc62a9432aaf2f6ed5a92bf1ed1c9c7310651/actionpack/lib/action_controller/metal/live.rb#L179" rel="noopener noreferrer"&gt;normal case&lt;/a&gt; for a response from a Rails application.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no-store&lt;/code&gt; - No cache of any type (shared or private) must store this response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-age&lt;/code&gt; - Indicates the period into the future for which the response should count as &lt;em&gt;fresh&lt;/em&gt;. So, &lt;code&gt;Cache-Control: max-age=3600&lt;/code&gt; would specify a period of one hour from the moment of generation on the origin server. Afterwards, a cache cannot reuse it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;private&lt;/code&gt; - This directive specifies that the response can only be stored in a &lt;em&gt;private&lt;/em&gt; cache (i.e., the browser). This should be set for any user-personalized content, especially when a session cookie is required to access a resource.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public&lt;/code&gt; - Indicates that a response can be stored in a shared cache.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;must-revalidate&lt;/code&gt; - An edge case worth noting: HTTP allows the reuse of stale responses when caches are disconnected from the origin server. This directive prevents that and forces a fresh response or a 504 gateway timeout error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  (In)validation
&lt;/h2&gt;

&lt;p&gt;Let's say a response has become &lt;em&gt;stale&lt;/em&gt; for some reason (it's expired, for example). Even so, there's a chance that it is still &lt;em&gt;valid&lt;/em&gt;, but we have to ask the origin server if this is the case. This process is called &lt;strong&gt;validation&lt;/strong&gt; and is performed using a &lt;strong&gt;conditional request&lt;/strong&gt; in one of two ways: by expiration or based on the response content.&lt;/p&gt;

&lt;h3&gt;
  
  
  By Expiration
&lt;/h3&gt;

&lt;p&gt;The client sends an &lt;code&gt;If-Modified-Since&lt;/code&gt; header in the request, and the cache uses this header to determine if the respective response has to be revalidated. Let's consider this response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
...
Date: Fri, 29 Mar 2024 10:30:00 GMT
Last-Modified: Fri, 29 Mar 2024 10:00:00 GMT
Cache-Control: max-age=3600
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was retrieved at 10:30 and is stored on the client. The combination of &lt;code&gt;Last-Modified&lt;/code&gt; and &lt;code&gt;max-age&lt;/code&gt; tells us that this response becomes stale at 11:30. If the client sends a request at 11:31, it includes an &lt;code&gt;If-Modified-Since&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Fri, 29 Mar 2024 10:00:00 GMT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server now calculates the content of the response and, if it hasn't changed, sends a &lt;code&gt;304 Not Modified&lt;/code&gt; response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 304 Not Modified
...
Date: Fri, 29 Mar 2024 11:31:00 GMT
Last-Modified: Fri, 29 Mar 2024 10:00:00 GMT
Cache-Control: max-age=3600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's a redirect, this response has no payload. It merely tells the client that the stale response has been &lt;strong&gt;revalidated&lt;/strong&gt; and can revert to a &lt;em&gt;fresh&lt;/em&gt; state again. Its new expiry time is now 12:31.&lt;/p&gt;

&lt;h3&gt;
  
  
  Based On the Response Content
&lt;/h3&gt;

&lt;p&gt;Timing is a problematic matter: servers and clients can drift out of sync or file system timestamps may not be appropriate. This is solved by using an &lt;code&gt;ETag&lt;/code&gt; header. This can be an arbitrary value, but most frequently, it is a digest of the response body. Picking up the example from above, we swap &lt;code&gt;Last-Modified&lt;/code&gt; for an &lt;code&gt;ETag&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
...
Date: Fri, 29 Mar 2024 10:30:00 GMT
ETag: "12345678"
Cache-Control: max-age=3600
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this response is stored in a private cache and becomes stale, the client now uses the last known &lt;code&gt;ETag&lt;/code&gt; value and asks the server to revalidate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "12345678"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the server will return a &lt;code&gt;304 Not Modified&lt;/code&gt; response if the values of a freshly computed &lt;code&gt;ETag&lt;/code&gt; and the requested &lt;code&gt;If-None-Match&lt;/code&gt; header match. Otherwise, it will respond with &lt;code&gt;200 OK&lt;/code&gt; and a new version of the content.&lt;/p&gt;

&lt;p&gt;I will not go into the details here, but &lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#strong-v-s-weak-etags" rel="noopener noreferrer"&gt;the Rails docs explain weak vs. strong ETags&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation In Rails
&lt;/h2&gt;

&lt;p&gt;We're now ready to implement this knowledge in a Rails app. The relevant module is wired into &lt;code&gt;ActionController&lt;/code&gt; and is called &lt;a href="https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html" rel="noopener noreferrer"&gt;&lt;code&gt;ActionController::ConditionalGet&lt;/code&gt;&lt;/a&gt;. Let's examine the interface it uses to emit the &lt;code&gt;Cache-Control&lt;/code&gt; directives discussed above.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;expires_in&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This method sets the &lt;code&gt;max-age&lt;/code&gt; directive, overwriting all others. You can pass it &lt;code&gt;ActiveSupport::Duration&lt;/code&gt; and &lt;a href="https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-expires_in" rel="noopener noreferrer"&gt;options such as &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;must_revalidate&lt;/code&gt;&lt;/a&gt;, which will set the respective directives.&lt;/p&gt;

&lt;p&gt;When would you want to use this? Typically, when you need to balance cache effectiveness and freshness. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="c1"&gt;# Set the response to expire in 15 minutes&lt;/span&gt;
    &lt;span class="n"&gt;expires_in&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;public: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&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;This exemplifies the compromise you might make between the probability that a new product is added, changed, or removed in the course of 15 minutes and somebody seeing a stale response. Every application will have its own limitations here, but it's a good idea to have application monitoring like &lt;a href="https://www.appsignal.com/ruby" rel="noopener noreferrer"&gt;AppSignal for Ruby&lt;/a&gt; built into your production environment. This will enable you to query how often an endpoint is accessed by the same user vs the data manipulation frequency.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;expires_now&lt;/code&gt; and &lt;code&gt;no_store&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This will set &lt;code&gt;Cache-Control&lt;/code&gt; to &lt;code&gt;no-cache&lt;/code&gt; and &lt;code&gt;no-store&lt;/code&gt;, respectively. Look above for the implications.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;http_cache_forever&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;http_cache_forever&lt;/code&gt; sets a &lt;code&gt;max-age&lt;/code&gt; of 100 years internally and caches the response. It will consult &lt;code&gt;stale?&lt;/code&gt;, though (see below), to determine if a fresh response should be rendered. You call it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StaticController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;
    &lt;span class="n"&gt;http_cache_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;public: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# Your logic to render the about page goes here&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:about&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;p&gt;This renders the &lt;code&gt;about&lt;/code&gt; view and allows intermediary shared caches to store it (because &lt;code&gt;public&lt;/code&gt; is set in &lt;code&gt;Cache-Control&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;fresh_when&lt;/code&gt; and &lt;code&gt;stale?&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;These methods are siblings and are both concerned with setting appropriate &lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;Last-Modified&lt;/code&gt; headers. Thus, they form the heart of Rails' conditional GET implementation. Let's look at each of them now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;fresh_when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@article&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;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="n"&gt;fresh_when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@articles&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;The &lt;code&gt;show&lt;/code&gt; action exhibits the simplest way to enable conditional GET requests on an endpoint. If it's passed an ActiveRecord instance, it will extract the &lt;code&gt;update_at&lt;/code&gt; timestamp, reuse it as &lt;code&gt;Last-Modified&lt;/code&gt;, and &lt;code&gt;ETag&lt;/code&gt; will be computed from the record as a &lt;a href="https://github.com/rails/rails/blob/30e37387249632cce382d7b7b7adc619855dc7d9/actionpack/lib/action_dispatch/http/cache.rb#L137-L138" rel="noopener noreferrer"&gt;hex digest&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When working with relations, like in the &lt;code&gt;index&lt;/code&gt; action, &lt;code&gt;fresh_when&lt;/code&gt; will check for the most recent &lt;code&gt;updated_at&lt;/code&gt; in the collection using the &lt;code&gt;maximum&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;If desired, you can override this behavior using &lt;a href="https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-fresh_when" rel="noopener noreferrer"&gt;explicit options&lt;/a&gt;. These match the directives discussed above (the only outlier being the &lt;code&gt;template&lt;/code&gt; option, which allows you to specify the template used to calculate the &lt;code&gt;ETag&lt;/code&gt; digest). This is useful when your controller action uses a different template than the default.&lt;/p&gt;

&lt;p&gt;In either case, before rendering a response, &lt;code&gt;fresh_when&lt;/code&gt; will &lt;a href="https://github.com/rails/rails/blob/30e37387249632cce382d7b7b7adc619855dc7d9/actionpack/lib/action_controller/metal/conditional_get.rb#L149-L150" rel="noopener noreferrer"&gt;check if a stored response is still fresh&lt;/a&gt; and return a 304 in this case.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;code&gt;stale?&lt;/code&gt; is a predicate method that falls back to &lt;code&gt;fresh_when&lt;/code&gt; internally and returns &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; based on its evaluation. You can use it to guard against expensive method calls, analogously to the above examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;expires_in&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stale?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh_counters!&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="vi"&gt;@article&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;p&gt;In this example, since it uses the same internal semantics as &lt;code&gt;fresh_when&lt;/code&gt;, it would automatically send a &lt;code&gt;304 Not Modified&lt;/code&gt; response unless the article is stale. If it is stale, though (i.e., if 15 minutes have passed since it was generated), the counters are refreshed and return a fresh response. Updating low-priority data in your responses only infrequently, based on a "timeout", is a typical use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;etag&lt;/code&gt; Class Method
&lt;/h3&gt;

&lt;p&gt;How can caches, even private ones, deal with personalized information? The answer is that data identifying the session in some way has to be included in the &lt;code&gt;ETag&lt;/code&gt;. This is exactly what the &lt;code&gt;etag&lt;/code&gt; controller class method does: it provides the necessary information to differentiate between private responses to individual users.&lt;/p&gt;

&lt;p&gt;Continuing with the example above, imagine that some users are administrators who are served "edit" buttons for the articles in the UI. You don't want that information to spill over to an anonymous user, so you can prevent that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;fresh_when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@article&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;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="n"&gt;fresh_when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@articles&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;Assuming that &lt;code&gt;current_user&lt;/code&gt; is a method returning the currently logged-in user, his/her id is now added to the &lt;code&gt;ETag&lt;/code&gt; before digesting, preventing the leak of sensitive data to unauthorized visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Given all we have learned above, do you feel confident adding HTTP caching to your controllers? I would cautiously assume that you still have some leftover anxiety regarding the leakage of sensitive data. But all in all, using Turbo Frames and personalizing a UI can provide some valuable insights.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rigorously Decompose into Turbo Frames
&lt;/h3&gt;

&lt;p&gt;If you are using Turbo in your frontend, you can leverage eager or lazy-loaded &lt;a href="https://turbo.hotwired.dev/reference/frames#eager-loaded-frame" rel="noopener noreferrer"&gt;Turbo Frames&lt;/a&gt;. A lot of applications comprise personalized sections of the UI, while others do not. By detaching the non-personalized parts into separate endpoints that employ HTTP caching individually, you not only gain performance benefits but also a clearer application architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply Personalized Bits of the UI After the Fact
&lt;/h3&gt;

&lt;p&gt;Another way to deal with user-dependent data is to apply JavaScript after a page has loaded. This is only viable if the amount of personalized data on a page is small. In essence, you'd query an endpoint for a current user's data in JSON format, then use a Stimulus controller to apply it to the relevant bits of the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;In this post, we demystified HTTP caching. We looked at some basic concepts, cache layers, and configuration, including how to use the &lt;code&gt;Cache-Control&lt;/code&gt; header and validation. We also examined the elegant solution that Rails provides in the form of the &lt;code&gt;ActionController::ConditionalGet&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;Until next time, happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>The Crumbling Codebase</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Thu, 11 Apr 2024 06:53:40 +0000</pubDate>
      <link>https://dev.to/julianrubisch/the-crumbling-codebase-km0</link>
      <guid>https://dev.to/julianrubisch/the-crumbling-codebase-km0</guid>
      <description>&lt;p&gt;Does the following sound familiar?&lt;/p&gt;

&lt;h2&gt;
  
  
  🤔
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Back when I extracted this module, it seemed like a good idea. What did I think I was doing?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🤹
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The models in this namespace are so tightly coupled, it feels like I can't move them an inch. Where's Houdini when you need him?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🩺
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The team that developed this feature is gone, and with them all the knowledge about it. Where do I start cleaning it up before I add more functionality?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If any of these quotes resonate with you, you are not alone. Here's a harsh truth: &lt;strong&gt;Over time things tend to degrade.&lt;/strong&gt; Memory, structure, cohesion are all subject to decay and attrition.&lt;/p&gt;

&lt;p&gt;I've covered the subject of &lt;a href="https://dev.to/julianrubisch/how-to-reduce-tech-debt-to-keep-your-ruby-on-rails-application-effortlessly-adaptable-1mg5"&gt;entropy&lt;/a&gt; elsewhere, and why you cannot escape it. The real question is, if disorder is inevitable, why bother?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;🤕 For one, it &lt;em&gt;hurts&lt;/em&gt;. It slows you down and can directly make the difference between success and failure of your business.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💸 On the other hand, upkeep incurs a &lt;em&gt;cost&lt;/em&gt;. This is often not directly quantifiable, but a hidden item in your payroll costs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tension between having to keep everything shipshape in order to stay agile, and the real, monetary cost it causes maneuvers many decision makers into a difficult situation. How many developer hours should we allocate to dealing with tech debt? How can we measure how well we are faring? What percentage of our opportunity costs is devoted to maintenance? Where do we even start?&lt;/p&gt;

&lt;p&gt;Let me share the good news last: &lt;a href="https://useattr.actor" rel="noopener noreferrer"&gt;Attractor&lt;/a&gt; can help answer many of these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a transparent &lt;strong&gt;scoring system&lt;/strong&gt; allows you to track your progress as you're restoring order. You'll have data available to gauge the development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;alarm bells ring&lt;/strong&gt; when a part of your codebase is degrading faster than the others. No more worries about "tech debt creep".&lt;/li&gt;
&lt;li&gt;analysis tools identify the &lt;strong&gt;worst offending modules&lt;/strong&gt; where repair has the highest impact on the health of your application. No more stabbing in the darkness of code complexity.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>techdebt</category>
    </item>
    <item>
      <title>How To Reduce Tech Debt to Keep Your Ruby on Rails Application Effortlessly Adaptable</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 27 Mar 2024 21:20:28 +0000</pubDate>
      <link>https://dev.to/julianrubisch/how-to-reduce-tech-debt-to-keep-your-ruby-on-rails-application-effortlessly-adaptable-1mg5</link>
      <guid>https://dev.to/julianrubisch/how-to-reduce-tech-debt-to-keep-your-ruby-on-rails-application-effortlessly-adaptable-1mg5</guid>
      <description>&lt;p&gt;Learn how the Second Law of Thermodynamics affects your codebase, and what tidying your workshop has to do with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entropy, or Why the Pile in the Workshop Keeps Growing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Entropy&lt;/strong&gt; measures the level of disorder or randomness within a system. It's a fundamental concept in physics that explains why things tend to &lt;strong&gt;move from order to disorder over time&lt;/strong&gt;, without external intervention. Picture a workshop: this principle manifests as the inevitable clutter that accumulates---tools left out, materials strewn around, each item slightly out of place.&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%2F4868tfnpyl96jtofj5b1.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%2F4868tfnpyl96jtofj5b1.png" alt="Illustration of a messy workshop" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like in a workshop, in software development, particularly with Ruby on Rails, &lt;strong&gt;technical debt&lt;/strong&gt; represents this drift towards disorder. It's the buildup of quick fixes and outdated code that, if not managed, makes our applications harder to adapt and maintain. Managing this technical debt is as crucial as keeping a workshop tidy for efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Law of Thermodynamics and Your Codebase
&lt;/h2&gt;

&lt;p&gt;The Second Law of Thermodynamics states that, in any closed system, entropy, or disorder, will always increase over time &lt;strong&gt;if no energy is applied to maintain order&lt;/strong&gt;. This law, a cornerstone of physics, can be applied to software development as well. A codebase, much like any system, naturally tends towards disorder. Without deliberate effort to refactor, update, and clean up, it accumulates &lt;strong&gt;technical debt&lt;/strong&gt;---outdated practices, redundant code, and inefficiencies.&lt;/p&gt;

&lt;p&gt;A critical insight for every engineering manager and CTO can be extrapolated: This buildup is a &lt;strong&gt;fundamental principle&lt;/strong&gt; at play. Just as energy must be invested to keep a system in order and counteract entropy, consistent effort is required to maintain and improve a codebase, keeping it efficient and adaptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tidy Up Your Workshop: Strategies for Reducing Code Complexity
&lt;/h2&gt;

&lt;p&gt;If we revisit the workshop metaphor from above, it becomes clear that once entropy kicks in, our tools become harder and harder to find. We have to pick up every gadget, look at it and decide where to put it. This is a tedious process that very closely resembles what we have to do to reduce tech debt and improve adaptability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sift through classes for unnecessary coupling and blurred responsibilities&lt;/li&gt;
&lt;li&gt;divide them into more fitting abstractions if applicable, and write tests to verify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But where do you start? This is where &lt;a href="https://useattr.actor" rel="noopener noreferrer"&gt;Attractor&lt;/a&gt; can help you with its approachable complexity visualizations and &lt;a href="https://useattr.actor/features" rel="noopener noreferrer"&gt;code vital signs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But there's more. One epiphany that struck me while recently cleaning up my own workshop is: The solution to &lt;strong&gt;finding things&lt;/strong&gt; is owning &lt;strong&gt;less stuff&lt;/strong&gt;. So I went ahead and threw away any excess material and leftover components that I saved for "later use" (which never happened)---until my workbench was clean:&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%2Fa8xjpj7adowljsbubtft.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%2Fa8xjpj7adowljsbubtft.png" alt="Illustration of a tidy workshop" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does that translate to code? Well, one proxy metric to measure the amount of clutter is &lt;strong&gt;code complexity&lt;/strong&gt;. Use it as a lens to find the most salient places in your codebase to clean up. It's the dust cover on that pile of junk over there---right in front of your favorite sonic screwdriver that you've been missing for so long.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can Attractor Do For You?
&lt;/h2&gt;

&lt;p&gt;In a nutshell, Attractor covers three distinct use cases:&lt;/p&gt;

&lt;h3&gt;
  
  
  Ongoing Accompaniment of Development
&lt;/h3&gt;

&lt;p&gt;This is the marquee scenario: By providing continuous reporting and flagging tech debt as it emerges, it helps establish a steady &lt;strong&gt;culture of grooming&lt;/strong&gt;. You reap the profits in the form of a sustained development velocity 🚀.&lt;/p&gt;

&lt;p&gt;That requires an already pretty tidy environment, though. It might be that you need help cleaning up first 👇.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gradual Tech Debt Removal
&lt;/h3&gt;

&lt;p&gt;Chances are, you went the "ship fast, clean up later" route. That's fair! It doesn't make much sense to polish an app before it makes money, I get it.&lt;/p&gt;

&lt;p&gt;Slowly, though, you start to feel growing pain. Your domain model feels wrong &lt;em&gt;in this spot&lt;/em&gt;, because you can't add an association &lt;em&gt;here&lt;/em&gt;. This form over there feels clunky, but what can you do about it?&lt;/p&gt;

&lt;p&gt;Attractor points you to the most painful spots to get a bird's-eye view of where to start weeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Whole-App Refactoring Guide
&lt;/h3&gt;

&lt;p&gt;Maybe you're a consultant being tasked with tidying up the mess that others made. Don't fret!&lt;/p&gt;

&lt;p&gt;Attractor helps you split up your refactoring endeavor into manageable chunks and spotlights the most powerful levers to get your app in shape again.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Full-Text Search for Ruby on Rails with Litesearch</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 28 Feb 2024 14:34:40 +0000</pubDate>
      <link>https://dev.to/appsignal/full-text-search-for-ruby-on-rails-with-litesearch-44ip</link>
      <guid>https://dev.to/appsignal/full-text-search-for-ruby-on-rails-with-litesearch-44ip</guid>
      <description>&lt;p&gt;In this post, we'll turn to the last piece of the puzzle in LiteStack: Litesearch.&lt;/p&gt;

&lt;p&gt;As an example, we will equip a prompts index page with a search bar to query a database for certain prompts. We will generate a couple of fake records to test our search functionality against.&lt;/p&gt;

&lt;p&gt;Let's get to it!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Litesearch?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/oldmoe/litestack#litesearch" rel="noopener noreferrer"&gt;Litesearch&lt;/a&gt;&lt;/strong&gt; is a convenience wrapper built around &lt;a href="https://sqlite.org/fts5.html" rel="noopener noreferrer"&gt;FTS5&lt;/a&gt;, SQLite's virtual table-based &lt;strong&gt;full-text search&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;We'll dive into the mechanics a bit later. For now, we will assume that Litesearch is a Ruby module providing a simple API to perform text searches against a SQLite database. This works in standalone mode, but we will focus on the ActiveRecord integration, of course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure the &lt;code&gt;Prompt&lt;/code&gt; Model for Litesearch
&lt;/h2&gt;

&lt;p&gt;The single addition we must make to our prompt model is a &lt;strong&gt;search index schema definition&lt;/strong&gt;. To do this, we have to include the &lt;code&gt;Litesearch::Model&lt;/code&gt; module in our model and call the &lt;code&gt;litesearch&lt;/code&gt; class method to add fields to the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  class Prompt &amp;lt; ApplicationRecord
    include AccountScoped
&lt;span class="gi"&gt;+   include Litesearch::Model
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    # ...
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   litesearch do |schema|
+     schema.fields [:title]
+   end
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    # ...
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also target associations like so, and change the tokenizer used for indexing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;litesearch&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;schema&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt; &lt;span class="ss"&gt;:account_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"accounts.name"&lt;/span&gt;
  &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokenizer&lt;/span&gt; &lt;span class="ss"&gt;:porter&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;strong&gt;Note:&lt;/strong&gt; Currently, ActionText fields are not supported.&lt;/p&gt;

&lt;p&gt;Let's quickly try this out in the Rails console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"teddy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Prompt&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.4&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;prompts_search_idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rank&lt;/span&gt; &lt;span class="no"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;search_rank&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="s2"&gt;"prompts"&lt;/span&gt; &lt;span class="no"&gt;INNER&lt;/span&gt; &lt;span class="no"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;prompts_search_idx&lt;/span&gt; &lt;span class="no"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;prompts&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;prompts_search_idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rowid&lt;/span&gt; &lt;span class="no"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="no"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;prompts_search_idx&lt;/span&gt; &lt;span class="no"&gt;MATCH&lt;/span&gt; &lt;span class="s1"&gt;'teddy'&lt;/span&gt; &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="s2"&gt;"prompts"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"account_id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="no"&gt;ORDER&lt;/span&gt; &lt;span class="no"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;prompts_search_idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rank&lt;/span&gt;  &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"account_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="c1"&gt;#&amp;lt;Prompt:0x0000000105f80fb0&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"A cute teddy bear"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;prompt_image: &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;...&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;account_id: &lt;/span&gt;&lt;span class="mi"&gt;1&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;Fri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="no"&gt;Jan&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;08.604031000&lt;/span&gt; &lt;span class="no"&gt;UTC&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&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;Fri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="no"&gt;Jan&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;41.321896000&lt;/span&gt; &lt;span class="no"&gt;UTC&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="s2"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;search_rank: &lt;/span&gt;&lt;span class="mf"&gt;1.0e-06&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to set &lt;code&gt;Current.account&lt;/code&gt;, because our prompt model is scoped to an account, otherwise we get an empty result set.&lt;/p&gt;

&lt;p&gt;Impressive! By changing only 4 lines of code, we already have a crude working version of full-text search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Typeahead Search Bar to Our Ruby on Rails Application
&lt;/h2&gt;

&lt;p&gt;Next up, we'll combine a few of the techniques we've reviewed to implement snappy typeahead searching. Before we do that, though, let's generate more sample data. I will use the popular &lt;a href="https://github.com/faker-ruby/faker" rel="noopener noreferrer"&gt;faker&lt;/a&gt; gem to do that:&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="nv"&gt;$ &lt;/span&gt;bundle add faker &lt;span class="nt"&gt;--group&lt;/span&gt; development
&lt;span class="nv"&gt;$ &lt;/span&gt;bin/rails console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop into a Rails console and create 50 sample prompts. I'm re-using the first prompt's image data here. Also, note that I'm again setting the &lt;code&gt;Current.account&lt;/code&gt; first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="no"&gt;Prompt&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;title: &lt;/span&gt;&lt;span class="s2"&gt;"a &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Adjective&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Faker&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Creature&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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="ss"&gt;content_type: &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;prompt_image: &lt;/span&gt;&lt;span class="no"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt_image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;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;To prepare our user interface for reactive searching, we will wrap the prompts grid in a Turbo frame. This frame will be replaced every time the search query changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/index.html.erb --&amp;gt;
  &amp;lt;h1&amp;gt;Prompts&amp;lt;/h1&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;%= turbo_frame_tag :prompts, class: "grid" do %&amp;gt;
&lt;/span&gt;&lt;span class="gd"&gt;- &amp;lt;div id="prompts" class="grid"&amp;gt;
&lt;/span&gt;    &amp;lt;% @prompts.each do |prompt| %&amp;gt;
      &amp;lt;%= link_to prompt do %&amp;gt;
        &amp;lt;%= render "index", prompt: prompt %&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;% end %&amp;gt;
&lt;span class="gi"&gt;+ &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;span class="gd"&gt;- &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;%= link_to "New prompt", new_prompt_path %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;PromptsController&lt;/code&gt; needs to be updated to filter prompts if a &lt;code&gt;query&lt;/code&gt; parameter is passed in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/controllers/prompts_controller.rb
  class PromptsController &amp;lt; ApplicationController
    # ...
&lt;span class="err"&gt;
&lt;/span&gt;    def index
      @prompts = Prompt.all
&lt;span class="gi"&gt;+
+     @prompts = @prompts.search(params[:query]) if params[:query].present?
&lt;/span&gt;    end
&lt;span class="err"&gt;
&lt;/span&gt;    # ...
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's rig up the search bar in the prompt index view. For this, we'll use a shoelace input component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/index.html.erb --&amp;gt;
  &amp;lt;h1&amp;gt;Prompts&amp;lt;/h1&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;section&amp;gt;
+   &amp;lt;sl-input name="search" type="search" placeholder="Search for a prompt title" clearable&amp;gt;
+     &amp;lt;sl-icon name="search" slot="suffix"&amp;gt;&amp;lt;/sl-icon&amp;gt;
+   &amp;lt;/sl-input&amp;gt;
+ &amp;lt;/section&amp;gt;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;div id="prompts" class="grid"&amp;gt;
    &amp;lt;% @prompts.each do |prompt| %&amp;gt;
      &amp;lt;%= link_to prompt do %&amp;gt;
        &amp;lt;%= render "index", prompt: prompt %&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;% end %&amp;gt;
  &amp;lt;/div&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;%= link_to "New prompt", new_prompt_path %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;To implement typeahead searching, we must add a bit of custom JavaScript to &lt;code&gt;app/javascript/application.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  // app/javascript/application.js
&lt;span class="err"&gt;
&lt;/span&gt;  // Entry point for the build script in your package.json
  import "@hotwired/turbo-rails";
  import "./controllers";
  import "trix";
  import "@rails/actiontext";
&lt;span class="err"&gt;
&lt;/span&gt;  import { setBasePath } from "@shoelace-style/shoelace";
&lt;span class="err"&gt;
&lt;/span&gt;  setBasePath("/");
&lt;span class="gi"&gt;+
+ document
+   .querySelector("sl-input[name=search]")
+   .addEventListener("keyup", (event) =&amp;gt; {
+     document.querySelector(
+       "#prompts"
+     ).src = `/prompts?query=${encodeURIComponent(event.target.value)}`;
+   });
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tiny JavaScript snippet does little more than place a &lt;code&gt;keyup&lt;/code&gt; listener on our search field, and update the Turbo Frame's &lt;code&gt;src&lt;/code&gt; attribute afterward. The input's &lt;code&gt;value&lt;/code&gt; is added as the &lt;code&gt;query&lt;/code&gt; parameter. Turbo Frame's default behavior performs the rest of the magic: reloading when the &lt;code&gt;src&lt;/code&gt; attribute changes, with the updated content fetched from the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/images/blog/2024-02/typeahead-search.mp4" rel="noopener noreferrer"&gt;Here's what this looks like&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Excursus: Highlighting Search Results Using a Turbo Event in Rails
&lt;/h1&gt;

&lt;p&gt;Currently, Litesearch doesn't feature a native highlighting solution like &lt;a href="https://github.com/Casecommons/pg_search?tab=readme-ov-file#highlight" rel="noopener noreferrer"&gt;pg_search&lt;/a&gt;, but it is pretty easy to build this ourselves using the &lt;code&gt;before-frame-render&lt;/code&gt; event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  // Entry point for the build script in your package.json
  import "@hotwired/turbo-rails";
  import "./controllers";
  import "trix";
  import "@rails/actiontext";
&lt;span class="err"&gt;
&lt;/span&gt;  import { setBasePath } from "@shoelace-style/shoelace";
&lt;span class="err"&gt;
&lt;/span&gt;  setBasePath("/");
&lt;span class="err"&gt;
&lt;/span&gt;  document
    .querySelector("sl-input[name=search]")
    .addEventListener("keyup", (event) =&amp;gt; {
      document.querySelector(
        "#prompts"
      ).src = `/prompts?query=${encodeURIComponent(event.target.value)}`;
    });
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ document
+   .querySelector("turbo-frame#prompts")
+   .addEventListener("turbo:before-frame-render", (event) =&amp;gt; {
+     event.preventDefault();
+
+     const newHTML = event.detail.newFrame.innerHTML;
+
+     const query = document.querySelector("sl-input[name=search]").value;
+     if (!!query) {
+       event.detail.newFrame.innerHTML = newHTML.replace(
+         new RegExp(`(${query})`, "ig"),
+         "&amp;lt;em&amp;gt;$1&amp;lt;/em&amp;gt;"
+       );
+     }
+
+     event.detail.resume();
+   });
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leverages a nifty, somewhat hidden Turbo feature: intercepting rendering. The &lt;a href="https://turbo.hotwired.dev/reference/events" rel="noopener noreferrer"&gt;Turbo &lt;code&gt;before-render&lt;/code&gt; and &lt;code&gt;before-frame-render&lt;/code&gt; events&lt;/a&gt; support pausing rendering and mangling returned HTML from the server. Here, we use this to &lt;a href="https://blog.appsignal.com/images/blog/2024-02/typeahead-search-highlight.mp4" rel="noopener noreferrer"&gt;wrap each occurrence of a search query&lt;/a&gt; in an &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood: Litesearch for Ruby on Rails Explained
&lt;/h2&gt;

&lt;p&gt;We've covered the basics of activating and configuring Litesearch for your LiteStack-powered Ruby on Rails application. As you might have guessed, there's a lot more potential hidden here.&lt;/p&gt;

&lt;p&gt;So let's briefly examine how Litesearch wraps around and leverages SQLite's built-in full-text search module, &lt;a href="https://sqlite.org/fts5.html" rel="noopener noreferrer"&gt;FTS5&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Virtual Tables in SQLite
&lt;/h3&gt;

&lt;p&gt;First, let's discuss the notion of &lt;strong&gt;&lt;a href="https://sqlite.org/vtab.html" rel="noopener noreferrer"&gt;virtual tables&lt;/a&gt;&lt;/strong&gt; in SQLite. Since there's no direct counterpart in the PostgreSQL or MySQL realm, it pays off to learn about these.&lt;/p&gt;

&lt;p&gt;From the vantage point of a user issuing an SQL statement against the database, a virtual table is a transparent proxy that adheres to the interface of a table. In the background, however, every query or manipulation invokes a callback of the virtual table structure instead of writing to disk.&lt;/p&gt;

&lt;p&gt;In short, a virtual table is something you reach for when you want to access "foreign" data without leaving the domain of your database connection. Apart from full-text search, other examples include &lt;a href="https://sqlite.org/rtree.html" rel="noopener noreferrer"&gt;geospatial indices&lt;/a&gt; or accessing a different file format, such as CSV.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite's FTS5 Full-Text Search Extension
&lt;/h3&gt;

&lt;p&gt;At its core, SQLite's full-text search engine is a virtual table.&lt;/p&gt;

&lt;p&gt;The table definition used by Litesearch in ActiveRecord mode looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="s2"&gt;"CREATE VIRTUAL TABLE &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; USING FTS5(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;col_names&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, content='', contentless_delete=1, tokenize='&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tokenizer_sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;name&lt;/code&gt; is the index name (it defaults to &lt;code&gt;"#{table_name}_search_idx"&lt;/code&gt;), and &lt;code&gt;col_names&lt;/code&gt; are the fields we set in our Litesearch schema definition.&lt;/p&gt;

&lt;p&gt;We will now briefly look at &lt;strong&gt;tokenizers&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokenizers
&lt;/h2&gt;

&lt;p&gt;To allow for efficient indexing, a full-text search engine employs a helper utility to split the payload into tokens: a &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Lexical_analysis#Tokenization" rel="noopener noreferrer"&gt;tokenizer&lt;/a&gt;&lt;/strong&gt;. &lt;a href="https://sqlite.org/fts5.html#tokenizers" rel="noopener noreferrer"&gt;FTS5 has three built-in tokenizers&lt;/a&gt; you can choose from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;unicode61&lt;/strong&gt; (default): All punctuation and whitespace characters (i.e. ",", "." etc.) are considered separators. Text is split at those characters, and the resulting list of connected characters (usually, words) are the tokens. In the wild, you might encounter the &lt;code&gt;remove_diacritics&lt;/code&gt; option. This option specifies how to treat glyphs added to letters, like "á", "à", etc. The default is to remove these "diacritics", so these characters are regarded as equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ascii&lt;/strong&gt;: Similar to unicode61, but all non-ASCII characters are always considered token characters. There is no &lt;code&gt;remove_diacritics&lt;/code&gt; option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;porter&lt;/strong&gt;: A tokenizer that employs &lt;a href="https://tartarus.org/martin/PorterStemmer/" rel="noopener noreferrer"&gt;porter stemming&lt;/a&gt; for tokenization. This essentially means that you can do similarity searches, i.e., "search", "searchable", and "searching" will be considered related.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FTS5 Search Interface
&lt;/h3&gt;

&lt;p&gt;To enable a convenient experience, Litesearch exposes a &lt;a href="https://github.com/oldmoe/litestack/blob/92be782b681b3476e30b2fb216829728576bae33/lib/litestack/litesearch/model.rb#L123-L124" rel="noopener noreferrer"&gt;&lt;code&gt;search&lt;/code&gt; class method&lt;/a&gt;. Essentially, this method joins the model's table to the associated search index and issues a &lt;code&gt;MATCH&lt;/code&gt; query. Results are then ordered according to the search rank and returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&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;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table_name&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="nf"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"INNER JOIN &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ON &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.id = &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.rowid AND rank != 0 AND &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; MATCH "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Arel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"'&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;term&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.rank AS search_rank"&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;Arel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.rank"&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;Currently, Litesearch doesn't expose more of FTS5's search syntax, but you can learn more about it &lt;a href="https://sqlite.org/fts5.html#full_text_query_syntax" rel="noopener noreferrer"&gt;in FTS5's documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up Next: Deployment and Monitoring of your LiteStack-powered Ruby on Rails Application
&lt;/h2&gt;

&lt;p&gt;In this post, we discovered Litesearch, the full-text search engine built into LiteStack. We learned how to configure an ActiveRecord model to expose search fields and other options to an SQLite text search index.&lt;/p&gt;

&lt;p&gt;We then flexed our Hotwire muscles to build a simple reactive search interface into our UI.&lt;/p&gt;

&lt;p&gt;Finally, we explored some of the inner workings of full-text search in SQLite to get a better understanding of what powers it, its benefits, and its limitations.&lt;/p&gt;

&lt;p&gt;Our upcoming article marks the conclusion of this seven-part journey through LiteStack, the unique and powerful way to build a single-machine Rails application. We will deal with deployment, reliability, and monitoring using built-in and external tools.&lt;/p&gt;

&lt;p&gt;Until then, happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Configuring Incoming Webhook Queues in Bullet Train</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Fri, 16 Feb 2024 17:11:55 +0000</pubDate>
      <link>https://dev.to/julianrubisch/configuring-incoming-webhook-queues-in-bullet-train-22i5</link>
      <guid>https://dev.to/julianrubisch/configuring-incoming-webhook-queues-in-bullet-train-22i5</guid>
      <description>&lt;p&gt;&lt;a href="https://useattr.actor" rel="noopener noreferrer"&gt;Attractor&lt;/a&gt; is a pretty background-job intensive app. If you think about it, every commit's changes have to be analyzed, and the results stored, to keep track of code quality and emerging tech debt.&lt;/p&gt;

&lt;p&gt;Since it's based on &lt;a href="https://bullettrain.co/" rel="noopener noreferrer"&gt;Bullet Train&lt;/a&gt;, it makes sense to handle most of these workloads in &lt;strong&gt;incoming webhooks&lt;/strong&gt;. Broadly speaking, there are three groups of incoming webhooks at work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook handlers for &lt;em&gt;processing code analytics data&lt;/em&gt; being reported back from sandbox containers,&lt;/li&gt;
&lt;li&gt;webhook handlers for &lt;em&gt;webhooks sent from GitHub&lt;/em&gt; (e.g. when a new pull request is created),&lt;/li&gt;
&lt;li&gt;everything else, e.g. &lt;em&gt;webhooks coming from &lt;a href="https://www.paddle.com/" rel="noopener noreferrer"&gt;Paddle&lt;/a&gt;&lt;/em&gt;, my payment provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Webhook Processing
&lt;/h2&gt;

&lt;p&gt;Each incoming webhook in Bullet Train is processed by a controller that is created by &lt;a href="https://bullettrain.co/docs/super-scaffolding" rel="noopener noreferrer"&gt;super scaffolding&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Webhooks::Incoming::SandboxWebhooksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebhooksController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate_token!&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SandboxWebhook&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;data: &lt;/span&gt;&lt;span class="no"&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="n"&gt;request&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;read&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_async&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&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;The created model file contains the processing logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Webhooks::Incoming::SandboxWebhook&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Webhook&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;
    &lt;span class="c1"&gt;# logic goes here&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;The actual processing is done by a generic &lt;code&gt;Webhooks::Incoming::WebhookProcessingJob&lt;/code&gt; that's part of Bullet Train and calls back to this &lt;code&gt;process&lt;/code&gt; method. In other words, in a vanilla Bullet Train app, Sidekiq is responsible for working off your incoming webhooks.&lt;/p&gt;

&lt;p&gt;So, let's look at the respective &lt;code&gt;sidekiq.yml&lt;/code&gt; file:&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;:concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;:concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;:concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;:queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mailers&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;low&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;action_mailbox_routing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 5 Sidekiq queues configured. A pretty nasty issue is hidden here in plain sight, though: All incoming webhooks are processed in the &lt;code&gt;default&lt;/code&gt; queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamically Configuring the Webhook Queue
&lt;/h2&gt;

&lt;p&gt;Why is this a problem? Let's walk through a scenario where a new customer signs up.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;she authenticates the Attractor GitHub app, and connects two pretty large repositories,&lt;/li&gt;
&lt;li&gt;she enters her credit card information and wants to conclude the sign-up,&lt;/li&gt;
&lt;li&gt;nothing happens, she is stuck.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's happening here? It took me a while to find out.&lt;/p&gt;

&lt;p&gt;As pointed out above, all incoming webhooks are processed in the &lt;code&gt;default&lt;/code&gt; queue, which in this case means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;all analysis jobs (which can be hundreds or even thousands) are queued up there,&lt;/li&gt;
&lt;li&gt;the one incoming webhook responsible for processing the payment, too - right at the bottom.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words, the payment would be processed only after all analysis jobs (and not only hers, maybe a couple of other customers did sign up simultaneously) have concluded. An unbearable situation, to have a customer hanging in a state of uncertainty whether her payment did successfully activate her subscription of &lt;a href="https://useattr.actor" rel="noopener noreferrer"&gt;Attractor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To remedy this, I could have changed the sign up flow to first require potential customers to check out, then allow them to connect the GitHub app. That would have been a pretty large effort, though, so I opted to try something else.&lt;/p&gt;

&lt;p&gt;We can reopen the &lt;code&gt;Webhooks::Incoming::WebhookProcessingJob&lt;/code&gt; and have the &lt;code&gt;queue_as&lt;/code&gt; method decide dynamically where to queue the webhook it's meant to process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebhookProcessingJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_as&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaddleWebhook&lt;/span&gt;
    &lt;span class="ss"&gt;:critical&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SandboxWebhook&lt;/span&gt;
    &lt;span class="ss"&gt;:low&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="ss"&gt;:default&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;The only remaining question was where to put this small monkey patch. Placing it into an initializer didn't work, because your app's constants (such as the &lt;code&gt;Webhooks::Incoming::WebhookProcessingJob&lt;/code&gt; class) haven't loaded at this time.&lt;/p&gt;

&lt;p&gt;So the only option is to run it after the app has finished initializing, i.e. in an &lt;code&gt;after_initialize&lt;/code&gt; block in your &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s2"&gt;"boot"&lt;/span&gt;

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

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MyApp&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="c1"&gt;# more config&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after_initialize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebhookProcessingJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_as&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaddleWebhook&lt;/span&gt;
          &lt;span class="ss"&gt;:critical&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Incoming&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SandboxWebhook&lt;/span&gt;
          &lt;span class="ss"&gt;:low&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="ss"&gt;:default&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;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;With this tweak, all webhooks coming in from the code analysis sandbox are placed in the &lt;em&gt;low&lt;/em&gt; job queue, keeping others in the &lt;em&gt;default&lt;/em&gt; one. Subsequently, when a webhook comes in from GitHub indicating a new pull request, it is processed before any potentially waiting &lt;code&gt;SandboxWebhook&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;Even more importantly, incoming webhooks from Paddle are placed in the &lt;em&gt;critical&lt;/em&gt; queue, making sure subscription changes are always processed first.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://twitter.com/kaspth" rel="noopener noreferrer"&gt;Kasper Timm Hansen&lt;/a&gt; for pointing me in the right direction! 🙏&lt;/p&gt;

</description>
      <category>rails</category>
    </item>
    <item>
      <title>Speed Up Your Ruby on Rails Application with LiteCache</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 31 Jan 2024 15:00:00 +0000</pubDate>
      <link>https://dev.to/appsignal/speed-up-your-ruby-on-rails-application-with-litecache-488l</link>
      <guid>https://dev.to/appsignal/speed-up-your-ruby-on-rails-application-with-litecache-488l</guid>
      <description>&lt;p&gt;In this series, we have looked at the "musts" (databases) and "shoulds" (asynchronous jobs, websockets) of a web application. Now we turn to one of the "coulds" (that is nonetheless recommended for scaling businesses): &lt;em&gt;caching&lt;/em&gt;. In particular, we mean caching &lt;em&gt;HTML fragments&lt;/em&gt; and other snippets of data, as referred to in &lt;a href="https://guides.rubyonrails.org/caching_with_rails.html" rel="noopener noreferrer"&gt;Rails Guides&lt;/a&gt;. We are &lt;em&gt;not&lt;/em&gt; concerned with HTTP or SQL query caching.&lt;/p&gt;

&lt;p&gt;In this part, we'll see how to speed up our Rails app using LiteCache. But first, we'll touch on Russian doll caching and how it comes in handy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Russian Doll Caching in Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;At first glance, it might seem counterintuitive to employ the same technology for the main database as well as the cache. After all, what speed improvements will that engender? This is where a technique called "Russian doll caching" comes in, which we will briefly shine a light on now.&lt;/p&gt;

&lt;p&gt;In one sentence, the gist of this caching approach boils down to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fastest database query is no query.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What do I mean by that? Let's turn to the &lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching" rel="noopener noreferrer"&gt;Rails Guides definition of fragment caching&lt;/a&gt; first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fragment Caching allows a fragment of view logic to be wrapped in a cache block and served out of the cache store when the next request comes in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, when you wrap a piece of view code in a &lt;code&gt;cache&lt;/code&gt; helper, the &lt;em&gt;rendered&lt;/em&gt; HTML fragment is put into the cache store, with a unique, expirable cache key. This key is constructed to expire whenever either the entities of which the cache key is composed, or the underlying view template, change.&lt;/p&gt;

&lt;h3&gt;
  
  
  An Example
&lt;/h3&gt;

&lt;p&gt;That may sound very unwieldy. Let's look at an example in the context of our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prediction/_prediction.html.erb --&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;% cache prediction do %&amp;gt;
&lt;/span&gt;    &amp;lt;div id="&amp;lt;%= dom_id(prediction) %&amp;gt;"&amp;gt;
      &amp;lt;%= turbo_stream_from prediction %&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;      &amp;lt;% if prediction.prediction_image.present? %&amp;gt;
        &amp;lt;%= image_tag prediction.data_url %&amp;gt;
      &amp;lt;% else %&amp;gt;
        &amp;lt;sl-spinner style="font-size: 8rem;"&amp;gt;&amp;lt;/sl-spinner&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;span class="gi"&gt;+ &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we have wrapped our &lt;code&gt;_prediction&lt;/code&gt; partial in a &lt;code&gt;cache&lt;/code&gt; block. This generates a cache key in the fashion of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;view path                     template digest                  identifier
|                             |                                |
views/predictions/_prediction:9695008de61cf58325bbf974443f54bc/predictions/3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, this key comprises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The view path, containing the &lt;code&gt;cache&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;A digest of the template (i.e., if you change the partial, the cache entry will be invalidated).&lt;/li&gt;
&lt;li&gt;A unique identifier — in other words, our &lt;code&gt;prediction&lt;/code&gt; — see the &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-i-cache_key" rel="noopener noreferrer"&gt;ActiveRecord documentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stored with this key is a timestamp (the &lt;code&gt;updated_at&lt;/code&gt; column of our &lt;code&gt;prediction&lt;/code&gt;) and the HTML fragment, in our case also the complete image's data URL. Whenever this partial is rendered again, and a matching cache key is found, rendering is bypassed. Instead, the stored HTML is returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Use of Russian Doll Caching
&lt;/h3&gt;

&lt;p&gt;What's that "Russian doll" piece about, though? To answer this, let's jump a layer higher into a view that renders this &lt;code&gt;_prediction&lt;/code&gt; partial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/_prompt.html.erb --&amp;gt;
&lt;span class="gi"&gt;+ &amp;lt;% cache prompt do %&amp;gt;
&lt;/span&gt;    &amp;lt;sl-card class="card-header card-prompt" id="&amp;lt;%= dom_id prompt %&amp;gt;"&amp;gt;
      &amp;lt;div slot="header"&amp;gt;
        &amp;lt;!-- header content omitted --&amp;gt;
      &amp;lt;/div&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;      &amp;lt;%= turbo_stream_from :predictions %&amp;gt;
      &amp;lt;div class="grid"&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;%= image_tag prompt.data_url %&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div id="&amp;lt;%= dom_id(prompt, :predictions) %&amp;gt;"&amp;gt;
          &amp;lt;%= render prompt.predictions %&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;      &amp;lt;div slot="footer"&amp;gt;
        &amp;lt;%= prompt.description.presence || "No Description" %&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/sl-card&amp;gt;
&lt;span class="gi"&gt;+ &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can apply the same technique to the &lt;code&gt;_prompt&lt;/code&gt; partial, which, in turn, renders a &lt;code&gt;_prediction&lt;/code&gt; partial for every prediction associated with it. This will result in a single HTML fragment comprising all the child fragments. &lt;strong&gt;We just saved one SQL query for each prediction!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a catch, though: When the list of predictions associated with a prompt changes (for example, a new one is added), the top fragment doesn't know about this and will serve &lt;em&gt;stale content&lt;/em&gt; (without the newly added image).&lt;/p&gt;

&lt;p&gt;In other words, we have to &lt;em&gt;expire&lt;/em&gt; its cache key and construct a fresh fragment. This is where the timestamp stored with each cache entry comes in handy. We can invalidate the cache simply by updating this timestamp. In ActiveRecord, luckily, there's a shorthand for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/models/prediction.rb
  class Prediction &amp;lt; ApplicationRecord
    # callbacks omitted
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-   belongs_to :prompt
&lt;/span&gt;&lt;span class="gi"&gt;+   belongs_to :prompt, touch: true
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    # methods omitted
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;touch&lt;/code&gt; flag on the &lt;code&gt;belongs_to&lt;/code&gt; association, every time a child record (a prediction) is updated, deleted, or added, the &lt;code&gt;updated_at&lt;/code&gt; timestamp of the parent (a prompt) is refreshed. This marks the cache fragment as stale, and it is reconstructed upon the next render cycle.&lt;/p&gt;

&lt;p&gt;The term "Russian doll caching" in this context refers to the fact that all the valid child fragments can still be pulled from the cache, thus speeding up the rendering process.&lt;/p&gt;

&lt;p&gt;Now that we have reviewed how fragment caching works and what benefits it yields, let's discuss how to enable and configure LiteCache.&lt;/p&gt;

&lt;h2&gt;
  
  
  LiteCache Configuration in Rails
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;config/environments/development.rb&lt;/code&gt;, add this configuration snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:litecache&lt;/span&gt;&lt;span class="p"&gt;,&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;Litesupport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cache.sqlite3"&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;This will resolve the root database path depending on your environment (in this case, &lt;code&gt;db/development&lt;/code&gt;) and create a &lt;code&gt;cache.sqlite3&lt;/code&gt; file there. There are more configuration options worth considering, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;size&lt;/code&gt;: the total allowed size of the cache database (the default being 128MB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expiry&lt;/code&gt;: cache record expiry in days (the default is one month)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mmap_size&lt;/code&gt;: how large a portion of the database to hold in memory (default: 128MB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;min_size&lt;/code&gt;: the minimum size of the database's journal (default: 32KB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sleep_interval&lt;/code&gt;: duration of sleep between cleanup runs (default: one second)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;metrics&lt;/code&gt;: boolean flag to indicate whether to gather metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ones you will likely want to tweak to your liking are &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;expiry&lt;/code&gt;, and (potentially) &lt;code&gt;sleep_interval&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To enable caching in development, you have to run:&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="nv"&gt;$ &lt;/span&gt;bin/rails dev:cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although LiteCache has many benefits, it comes with some drawbacks too. Let's first look at some optimizations, and then a few of LiteCache's limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizations and Limitations of LiteCache for Your Ruby App
&lt;/h2&gt;

&lt;p&gt;LiteCache connects to the SQLite database in a way that's optimized for use as a cache store. First, it's important to reiterate that LiteCache is configured to use a &lt;em&gt;separate cache database&lt;/em&gt;, so all these optimizations only affect an isolated environment.&lt;/p&gt;

&lt;p&gt;With this disclaimer in place, let's look at how LiteCache optimally utilizes SQLite.&lt;/p&gt;

&lt;p&gt;First, LiteCache sets the &lt;a href="https://www.sqlite.org/pragma.html#pragma_synchronous" rel="noopener noreferrer"&gt;pragma statement&lt;/a&gt; &lt;code&gt;synchronous&lt;/code&gt; to 0, so there is no sync after a commit to the database. This results in a tremendous speedup at the expense of data safety. In very rare cases, such as a power loss or operating system crashes, data loss might occur. However, considering that cache entries are seen as ephemeral in most cases, this is a sensible tradeoff. Needless to say, you can also override this setting in your configuration.&lt;/p&gt;

&lt;p&gt;LiteCache also uses a &lt;em&gt;least recently used (LRU)&lt;/em&gt; eviction policy with a &lt;a href="https://github.com/oldmoe/litestack/blob/1318e52101e874254490b43033580191466ba6a7/lib/litestack/litecache.sql.yml#L13-L14" rel="noopener noreferrer"&gt;special index&lt;/a&gt;, but it delays updating it. Instead, it will buffer the updates in memory, and flush them as a single transaction every few seconds.&lt;/p&gt;

&lt;p&gt;What about limitations? At the time of writing this article, one crucial piece of the &lt;code&gt;ActiveSupport::Cache&lt;/code&gt; interface isn't implemented yet: The ability to do &lt;strong&gt;multiple reads, writes, and deletes&lt;/strong&gt; against the cache database. Why is that so important? Because other cache backends like Redis demonstrate how significant speedups can be achieved by batching these operations. Indeed, the implicit performance gain built into rendering a collection of partials is the most impressive feat of Rails fragment caching.&lt;/p&gt;

&lt;p&gt;Speaking of performance speed, let's now quickly look at some benchmarks before we wrap up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks: LiteCache Vs. Redis
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md" rel="noopener noreferrer"&gt;benchmarks for LiteCache&lt;/a&gt; are impressive, with a small caveat. While LiteCache outperforms a local Redis installation &lt;a href="https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md#read" rel="noopener noreferrer"&gt;for every read operation&lt;/a&gt;, it seems like there's still &lt;a href="https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md#write" rel="noopener noreferrer"&gt;room for improvement&lt;/a&gt;, especially for large write payloads.&lt;/p&gt;

&lt;p&gt;Considering the increased benefits of caching large HTML fragments, this is a worthwhile limitation that will hopefully be tackled in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up Next: Built-In Full-Text Search with LiteSearch
&lt;/h2&gt;

&lt;p&gt;In this post, we illuminated Russian doll caching as a technique to speed up Rails applications by avoiding unnecessary database calls.&lt;/p&gt;

&lt;p&gt;Through practical examples, we’ve seen how nested cache fragments operate in harmony — each layer independent, yet interconnected — thus ensuring efficient rendering.&lt;/p&gt;

&lt;p&gt;We also delved into the practicalities of configuring LiteCache to your liking and looked at important built-in optimizations. With that in mind, the missing support for &lt;em&gt;multiple&lt;/em&gt; read and write operations is bearable, and perhaps is a feature that might soon arrive on the horizon.&lt;/p&gt;

&lt;p&gt;In the next post of this series, we will take a tour through the latest addition to LiteStack: A SQLite-based full-text search engine called &lt;em&gt;LiteSearch&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Until then, happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Divergent Change</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Fri, 19 Jan 2024 21:47:02 +0000</pubDate>
      <link>https://dev.to/julianrubisch/divergent-change-54om</link>
      <guid>https://dev.to/julianrubisch/divergent-change-54om</guid>
      <description>&lt;p&gt;One of the lesser known code smells, &lt;em&gt;Divergent Change&lt;/em&gt; is one of the strongest multipliers of tech debt. Fixing it has high leverage, so we'll explore how to spot and remediate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Divergent Change?
&lt;/h2&gt;

&lt;p&gt;Divergent Change belongs to the code smells class of &lt;em&gt;Change Preventers&lt;/em&gt;. As such, it is a major impediment for your development velocity, which is why it should be treated swiftly when you spot it.&lt;/p&gt;

&lt;p&gt;In one sentence, you are likely dealing with this smell if one class &lt;strong&gt;changes often, but for unrelated reasons&lt;/strong&gt;. Another hint is the necessity to &lt;strong&gt;change a class in multiple places&lt;/strong&gt; when altering it or a dependency. It is the mirror-universe counterpart of &lt;em&gt;Shotgun Surgery&lt;/em&gt;, in which many classes change for the same reason.&lt;/p&gt;

&lt;p&gt;I am going to demonstrate this with an &lt;code&gt;Alert&lt;/code&gt; ViewComponent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AlertComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&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;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
    &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&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;color&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;success: &lt;/span&gt;&lt;span class="s2"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;info: &lt;/span&gt;&lt;span class="s2"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"red"&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;def&lt;/span&gt; &lt;span class="nf"&gt;icon&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;success: &lt;/span&gt;&lt;span class="s2"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;info: &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"exclamation"&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;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:success&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"YIPPEE!"&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:info&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"Listen up:"&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:error&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"Oh Dear."&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Suppose we want to add a new type of status, &lt;code&gt;warning&lt;/code&gt;, we now have to edit three individual methods. This example is trivial, but imagine these methods are actually spread out between several modules. Suddenly a single change turns into a bug chasing contest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Divergent Change So Harmful?
&lt;/h2&gt;

&lt;p&gt;Every &lt;em&gt;"change preventer"&lt;/em&gt; is malign, but Divergent Change is often harder to spot than its colleagues. That's especially the case if a class' behavior is scattered between various modules, for example Rails' ActiveSupport concerns.&lt;/p&gt;

&lt;p&gt;That's why extracting a concern solely to include it in a single class - albeit reducing perceived complexity - is itself considered a smell. It disguises that a class doesn't follow the Single Responsibility Principle, and should be better treated with other techniques, for example composition.&lt;/p&gt;

&lt;p&gt;Often, divergent change starts out pretty innocent: "Oh, I'll just use a hash as a lookup table", or "one &lt;code&gt;case&lt;/code&gt; statement won't hurt". A simple enough choice in the YAGNI spirit - fair enough. The problem is one of setting an example, i.e. culture. The next developer who needs to program a feature in this class sees the pattern and does the next best thing under deadline pressure - add another conditional. Thus, gradually the class becomes a &lt;strong&gt;God Class&lt;/strong&gt; with far too many concerns mixed into its responsibility - another way of circumscribing Divergent Change. What's worse, such a static structure of conditionals and constants makes it hard to refactor and employ a behavioral design pattern like &lt;strong&gt;Strategy&lt;/strong&gt; or &lt;strong&gt;Visitor&lt;/strong&gt;, which allow to dynamically attach behavior at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do I Detect Divergent Change in My Codebase?
&lt;/h2&gt;

&lt;p&gt;In contrast to &lt;em&gt;Shotgun Surgery&lt;/em&gt;, which is easier to spot (you just have to grep a certain method or class in your codebase), Divergent Change is a master of camouflage. Chances are you already have many such offenses in your codebase which are hiding in plain sight. Let's look at a strategy to detect it by first narrowing down the potential offenders, then looking for specific symptoms.&lt;/p&gt;

&lt;p&gt;Conveniently, &lt;a href="https://useattr.actor" rel="noopener noreferrer"&gt;Attractor&lt;/a&gt; already equips you with the relevant code metrics to filter your classes in search of Divergent Change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Churn&lt;/strong&gt; (how often a class has changed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt; (how hard it is to understand what the class is doing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you restrict your search to those files exhibiting both a high churn count and complexity, you are likely to come across a couple of &lt;strong&gt;God Classes&lt;/strong&gt; that display the symptoms outlined 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%2Fy3gzirt8nqcimvjss7fj.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%2Fy3gzirt8nqcimvjss7fj.png" alt="Churn/Complexity Scatterplot" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's of course only narrowing down the search space. We have to do a bit of detective work to tease out the offenses. Here are a few indicators that you should be familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Looking through the version control log, you see many changes that aren't related to each other&lt;/li&gt;
&lt;li&gt;If you skim through these commits, you notice that more than one class or module was modified&lt;/li&gt;
&lt;li&gt;The class has a lot of control flow logic (i.e. conditionals)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Do I Treat Divergent Change?
&lt;/h2&gt;

&lt;p&gt;As a general heuristic, you should strive that each class only serves one purpose. Ideally you can describe what it does in a single sentence. That, of course, is only a very general guideline.&lt;/p&gt;

&lt;p&gt;I will start out with a simple refactoring that introduces &lt;em&gt;composition&lt;/em&gt; by extracting a class (hierarchy). Bear with me, I'm deliberately taking this step by step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Alert&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&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;class&lt;/span&gt; &lt;span class="nc"&gt;SuccessAlert&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alert&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"green"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"check"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YIPPEE! &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InfoAlert&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alert&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blue"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"info"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Listen up: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ErrorAlert&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alert&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"red"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"exclamation"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Oh Dear. &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AlertComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="no"&gt;VALID_ALERTS_BY_STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;success: &lt;/span&gt;&lt;span class="no"&gt;SuccessAlert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;info: &lt;/span&gt;&lt;span class="no"&gt;InfoAlert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="no"&gt;ErrorAlert&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;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VALID_ALERTS_BY_STATE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&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;message: &lt;/span&gt;&lt;span class="n"&gt;message&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;delegate&lt;/span&gt; &lt;span class="ss"&gt;:color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :state&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how this simple refactoring has already reduced the changes that have to be made to add a new type of alert from 3 to 2: Instead of altering 3 methods, I now only have to add one class, and add it to the &lt;code&gt;VALID_ALERTS_BY_STATE&lt;/code&gt; hash.&lt;/p&gt;

&lt;p&gt;We can do better though. We harness Rails' developers dearest paradigm - &lt;em&gt;Convention over Configuration&lt;/em&gt; - to simplify the class lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AlertComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&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;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Alert"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_constantize&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;message: &lt;/span&gt;&lt;span class="n"&gt;message&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;delegate&lt;/span&gt; &lt;span class="ss"&gt;:color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :state&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we've also gotten rid of the lookup hash and simply assume that the class name adheres to a certain naming scheme. Note that both these approaches still require that you know which are valid states to pass into the component. As we will see in a moment, there's a yet better way to go about this.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can I Prevent It?
&lt;/h2&gt;

&lt;p&gt;There are two &lt;a href="https://en.wikipedia.org/wiki/SOLID" rel="noopener noreferrer"&gt;SOLID&lt;/a&gt; principles that should be your yardsticks here: The &lt;strong&gt;Open/Closed Principle&lt;/strong&gt;, and &lt;strong&gt;Dependency Inversion&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open/Closed Principle
&lt;/h3&gt;

&lt;p&gt;Paraphrased a bit, it goes like this: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Software entities should be open for extension, but closed for modification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have seen in the above example that adding a new type of alert would have meant tinkering with the internals of &lt;code&gt;AlertComponent&lt;/code&gt; quite a bit. You should always strive to see classes as &lt;em&gt;"finished"&lt;/em&gt; entities and not interfere with their implementation if possible. Of course, this requires discipline and often quick iteration in the product development lifecycle exacerbates the situation.&lt;/p&gt;

&lt;p&gt;With refactorings like the above we have already improved the situation. Still, there's a bit of concern regarding the alert class lookup: If the naming convention was ever to change, we'll have to touch a lot of classes, again. It would be better if the &lt;code&gt;AlertComponent&lt;/code&gt; didn't have to bother with that at all and were &lt;em&gt;extensible&lt;/em&gt; by design. This is where the next principle comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Inversion
&lt;/h3&gt;

&lt;p&gt;In short, this principle demands that the software inverts control. Lower level modules (our &lt;code&gt;Alert&lt;/code&gt; classes) should be passed into higher level modules (our &lt;code&gt;AlertComponent&lt;/code&gt;) instead of the latter depending on the former. Rather, they should be treated as abstractions. What does this mean? Let's take a look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AlertComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&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;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@alert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :alert&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;code&gt;AlertComponent&lt;/code&gt; class is completely decoupled. We have &lt;em&gt;inverted control&lt;/em&gt;, i.e. the component simply relies upon the passed in &lt;code&gt;alert&lt;/code&gt; to understand &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt;, and &lt;code&gt;message&lt;/code&gt;. It would be called like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;AlertComponent&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;alert: &lt;/span&gt;&lt;span class="no"&gt;SuccessAlert&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;message: &lt;/span&gt;&lt;span class="s2"&gt;"You have successfully inverted control"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notably, the component now isn't concerned with adding a new type of alert at all anymore. All we'd have to do is implement it as a class and inject it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WarningAlert&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alert&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"yellow"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"triangle-exclamation"&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Yikes. &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;AlertComponent&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;alert: &lt;/span&gt;&lt;span class="no"&gt;WarningAlert&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;message: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/ruby-science/divergent-change.html" rel="noopener noreferrer"&gt;Thoughtbot Article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://refactoring.guru/smells/divergent-change" rel="noopener noreferrer"&gt;Refactoring Guru&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Stream Updates to Your Users with LiteCable for Ruby on Rails</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 10 Jan 2024 14:00:00 +0000</pubDate>
      <link>https://dev.to/appsignal/stream-updates-to-your-users-with-litecable-for-ruby-on-rails-4307</link>
      <guid>https://dev.to/appsignal/stream-updates-to-your-users-with-litecable-for-ruby-on-rails-4307</guid>
      <description>&lt;p&gt;So far in this series, we have been exploring the capabilities of SQLite for classic HTTP request/response type usage. In this post, we will push the boundary further by also using SQLite as a Pub/Sub adapter for ActionCable, i.e., WebSockets.&lt;/p&gt;

&lt;p&gt;This is no small feat: WebSocket adapters need to handle thousands of concurrent connections performantly. The emergence of alternatives to ActionCable — read AnyCable — bears witness to the fact that this is a pressing concern for modern web applications. We'll take a look at how SQLite performs under these conditions.&lt;/p&gt;

&lt;p&gt;But first, let's set up our app to broadcast streaming updates to users via Turbo Streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Your Ruby on Rails App to Use LiteCable for Websockets
&lt;/h2&gt;

&lt;p&gt;Configuring your application to use &lt;em&gt;LiteCable&lt;/em&gt; for WebSocket connections is as easy as specifying the adapter in &lt;code&gt;config/cable.yml&lt;/code&gt;:&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;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litecable&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litecable&lt;/span&gt;

&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litecable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preparing Our Rails App for Live Updates
&lt;/h2&gt;

&lt;p&gt;Before we dive deep into how to broadcast model updates using Turbo Rails, we need to rework the mechanics of creating a prediction.&lt;/p&gt;

&lt;p&gt;First, we'll create an empty prediction in &lt;code&gt;GenerateImageJob&lt;/code&gt; to display a placeholder in our &lt;code&gt;_prompt&lt;/code&gt; partial. This has the added benefit of forwarding the actual prediction's SGID to the webhook. Note, though, that we also have to pass the account's SGID, because the incoming webhook doesn't have any session information about the currently active user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/jobs/generate_image_job.rb
&lt;span class="err"&gt;
&lt;/span&gt;  class GenerateImageJob &amp;lt; ApplicationJob
    include Rails.application.routes.url_helpers
&lt;span class="err"&gt;
&lt;/span&gt;    queue_as :default
&lt;span class="err"&gt;
&lt;/span&gt;    def perform(prompt:)
&lt;span class="gi"&gt;+     empty_prediction = prompt.predictions.create
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;      model = Replicate.client.retrieve_model("stability-ai/stable-diffusion-img2img")
      version = model.latest_version
      version.predict({prompt: prompt.title, image: prompt.data_url},
        replicate_rails_url(host: Rails.application.config.action_mailer.default_url_options[:host],
&lt;span class="gd"&gt;-         params: {sgid: prompt.to_sgid.to_s}))
&lt;/span&gt;&lt;span class="gi"&gt;+         params: {prediction: empty_prediction.to_sgid.to_s,
+         account: prompt.account.to_sgid.to_s}))
&lt;/span&gt;    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In parallel, in our &lt;code&gt;ReplicateWebhook&lt;/code&gt;, we can locate and simply update the prediction. Note that we have to set the &lt;code&gt;Current.account&lt;/code&gt; because &lt;code&gt;Prompt&lt;/code&gt; is scoped to an account and would otherwise end up empty (due to the way &lt;code&gt;AccountScoped&lt;/code&gt; is set up).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # config/initializers/replicate.rb
&lt;span class="err"&gt;
&lt;/span&gt;  class ReplicateWebhook
    def call(prediction)
      query = URI(prediction.webhook).query
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-     sgid = CGI.parse(query)["sgid"].first
&lt;/span&gt;&lt;span class="gi"&gt;+     prediction_sgid = CGI.parse(query)["prediction"].first
+     account_sgid = CGI.parse(query)["account"].first
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-     prompt = GlobalID::Locator.locate_signed(sgid)
&lt;/span&gt;&lt;span class="gi"&gt;+     located_prediction = GlobalID::Locator.locate_signed(sgid)
+     Current.account = GlobalID::Locator.locate_signed(account_sgid)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-     prompt.predictions.create(
&lt;/span&gt;&lt;span class="gi"&gt;+     located_prediction.update(
&lt;/span&gt;        prediction_image: URI.parse(prediction.output.first).open.read,
        replicate_id: prediction.id,
        replicate_version: prediction.version,
        logs: prediction.logs
      )
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This change entails that the created prediction is empty, i.e., has no prediction image (obviously). Let's cater for this by adding a conditional to our &lt;code&gt;_prompt.html.erb&lt;/code&gt; partial. When the image is missing, we display a spinner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/_prompt.html.erb --&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;p&amp;gt;
    &amp;lt;strong&amp;gt;Generated images:&amp;lt;/strong&amp;gt;
    &amp;lt;% prompt.predictions.each do |prediction| %&amp;gt;
&lt;span class="gi"&gt;+     &amp;lt;% if prediction.prediction_image.present? %&amp;gt;
&lt;/span&gt;        &amp;lt;%= image_tag prediction.data_url %&amp;gt;
&lt;span class="gi"&gt;+     &amp;lt;% else %&amp;gt;
+       &amp;lt;sl-spinner style="font-size: 8rem;"&amp;gt;&amp;lt;/sl-spinner&amp;gt;
+     &amp;lt;% end %&amp;gt;
&lt;/span&gt;    &amp;lt;% end %&amp;gt;
  &amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, we're done preparing our app to deliver live updates. Let's implement Turbo-Rails model broadcasts to finish this proof of concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering Prediction Updates Live with Turbo-Rails
&lt;/h2&gt;

&lt;p&gt;To test the WebSocket capabilities of LiteStack, we are going to use &lt;code&gt;Turbo::Broadcastable&lt;/code&gt;. We'd like to show the spinner and the generated image once it has been created.&lt;/p&gt;

&lt;p&gt;The way to do that is quite idiomatic: We tie this to &lt;code&gt;after_create_commit&lt;/code&gt; and &lt;code&gt;after_update_commit&lt;/code&gt; model callbacks invoking one of &lt;code&gt;Turbo::Broadcastable&lt;/code&gt;'s &lt;a href="https://www.rubydoc.info/github/hotwired/turbo-rails/Turbo/Broadcastable" rel="noopener noreferrer"&gt;broadcast methods&lt;/a&gt;. Before we can do that, though, let's separate out a model partial for &lt;code&gt;Prediction&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/predictions/_prediction.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prediction_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;image_tag&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_url&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;sl-spinner&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 8rem;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/sl-spinner&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observe that I added a &lt;code&gt;turbo_stream_from&lt;/code&gt; tag to the partial, containing the stream identifier and subscribing to the channel. We can now simply call &lt;code&gt;render&lt;/code&gt; from the prompt partial and add another &lt;code&gt;turbo_stream_from&lt;/code&gt; to listen for changes to the prediction list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/_prompt.html.erb --&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;  &amp;lt;p&amp;gt;
    &amp;lt;strong&amp;gt;Generated images:&amp;lt;/strong&amp;gt;
&lt;span class="gd"&gt;-   &amp;lt;% prompt.predictions.each do |prediction| %&amp;gt;
-     &amp;lt;% if prediction.prediction_image.present? %&amp;gt;
-       &amp;lt;%= image_tag prediction.data_url %&amp;gt;
-     &amp;lt;% else %&amp;gt;
-       &amp;lt;sl-spinner style="font-size: 8rem;"&amp;gt;&amp;lt;/sl-spinner&amp;gt;
-     &amp;lt;% end %&amp;gt;
-   &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   &amp;lt;%= turbo_stream_from :predictions %&amp;gt;
+   &amp;lt;div id="&amp;lt;%= dom_id(prompt, :predictions) %&amp;gt;"&amp;gt;
+     &amp;lt;%= render prompt.predictions %&amp;gt;
+   &amp;lt;/div&amp;gt;
&lt;/span&gt;  &amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we're ready to set up model broadcasts. In the &lt;code&gt;Prediction&lt;/code&gt; class, we add two model callbacks, invoking two Turbo Stream actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/models/prediction.rb
&lt;span class="err"&gt;
&lt;/span&gt;  class Prediction &amp;lt; ApplicationRecord
&lt;span class="gi"&gt;+   include ActionView::RecordIdentifier
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   after_create_commit -&amp;gt; { broadcast_append_later_to :predictions,
+     target: dom_id(prompt, :predictions) }
+   after_update_commit -&amp;gt; { broadcast_replace_later_to self }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    belongs_to :prompt
&lt;span class="err"&gt;
&lt;/span&gt;    def data_url
      encoded_data = Base64.strict_encode64(prediction_image)
&lt;span class="err"&gt;
&lt;/span&gt;      "data:image/png;base64,#{encoded_data}"
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What's Happening Here?
&lt;/h3&gt;

&lt;p&gt;First, when a prediction is created, we &lt;em&gt;append&lt;/em&gt; it to the predictions list. This will show our loading spinner once &lt;code&gt;GenerateImageJob&lt;/code&gt; has run.&lt;/p&gt;

&lt;p&gt;Then, every update to the record will trigger a &lt;em&gt;replace&lt;/em&gt; of the prediction partial. Once the prediction is updated in &lt;code&gt;ReplicateWebhook&lt;/code&gt;, the image returned from Replicate displays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/images/blog/2023-12/image-prediction.mp4" rel="noopener noreferrer"&gt;Here's what this looks like&lt;/a&gt; - note that I'm using &lt;a href="https://shoelace.style/" rel="noopener noreferrer"&gt;Shoelace components&lt;/a&gt; for styling purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks: LiteCable Vs. Redis
&lt;/h2&gt;

&lt;p&gt;So far, this article has shown that it's &lt;em&gt;possible&lt;/em&gt; to run ActionCable with LiteCable as its adapter. This is a nice proof of concept, but we're here to check how LiteStack &lt;em&gt;compares&lt;/em&gt; to other adapters as well.&lt;/p&gt;

&lt;p&gt;Luckily, the &lt;a href="https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md" rel="noopener noreferrer"&gt;official LiteStack benchmarks&lt;/a&gt; include measurements for LiteCable against Redis, which I am going to quote here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's a small but important caveat:&lt;/strong&gt; &lt;em&gt;All these measurements were performed on the same machine. In typical production setups with managed Redis, you'll have to factor in additional network latency.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's look at the &lt;strong&gt;requests per second&lt;/strong&gt; metric first. This captures how many Pub/Sub requests the Redis and SQLite processes are able to serve.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Redis requests/second&lt;/th&gt;
&lt;th&gt;LiteStack requests/second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;2611&lt;/td&gt;
&lt;td&gt;3058&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;3110&lt;/td&gt;
&lt;td&gt;5328&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;3403&lt;/td&gt;
&lt;td&gt;5385&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that LiteStack is able to process more requests per second, but tapers off with higher loads. Though not representative, this might be an issue for loads of 1M requests and beyond — but that's when you'll typically reach for faster solutions like &lt;a href="https://anycable.io/" rel="noopener noreferrer"&gt;AnyCable&lt;/a&gt; over stock ActionCable anyway.&lt;/p&gt;

&lt;p&gt;Furthermore, there are some &lt;strong&gt;latency tests&lt;/strong&gt; included in the benchmarks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Redis p90 Latency&lt;/th&gt;
&lt;th&gt;LiteStack p90 Latency&lt;/th&gt;
&lt;th&gt;Redis p99 Latency&lt;/th&gt;
&lt;th&gt;LiteStack p99 Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;138&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;235&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Allowing for some inaccuracy of measurement, both perform equivalently in this regard, maybe with the exception of the 99 percentile. Here, SQLite's locking model interferes with the amount of concurrent requests.&lt;/p&gt;

&lt;p&gt;Again keep in mind, though, that you'll have to add a couple of milliseconds of latency once Redis runs on a different machine (LiteCable always runs on the same machine by design).&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations of SQLite for Rails
&lt;/h2&gt;

&lt;p&gt;It is fair to assume that once you hit a certain level of Pub/Sub activity, you'll reach the ceiling of what's possible with a single SQLite database. That's the moment when you'll have to think about sharding, and here other technologies like Redis have a head start — though it will be interesting to see what &lt;a href="https://fly.io/docs/litefs/" rel="noopener noreferrer"&gt;LiteFS&lt;/a&gt; will have to offer.&lt;/p&gt;

&lt;p&gt;Continuous monitoring of your app's WebSocket performance metrics using &lt;a href="https://www.appsignal.com/ruby" rel="noopener noreferrer"&gt;tools like AppSignal&lt;/a&gt; is your friend here. Reusing the ActionCable consumer on the client side is also advisable, as it will prevent wasting Pub/Sub connections.&lt;/p&gt;

&lt;p&gt;LiteCable is tailored for vertical scaling by a tight integration of components. If you extract maximum performance from the SQLite engine, the limits of this approach are pushed a lot further. Once you observe that your latencies start to explode, though, I would suggest researching options like &lt;a href="https://anycable.io/" rel="noopener noreferrer"&gt;AnyCable&lt;/a&gt;, which inherently provide better strategies for horizontal scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up Next: Speed Up Rails App Rendering with LiteCache
&lt;/h2&gt;

&lt;p&gt;In this post, we explored using SQLite as a Pub/Sub adapter for ActionCable to enable real-time updates in a Rails application via WebSockets. Configuring LiteCable was straightforward, requiring just a simple adapter specification. Leveraging &lt;code&gt;Turbo::Broadcastable&lt;/code&gt; model callbacks made our implementation clean, tying broadcasts to creation and updates.&lt;/p&gt;

&lt;p&gt;Though powerful, LiteCable is not designed to scale across multiple processes or servers. But for single-machine deployments, it unlocks real-time features in Rails without requiring a separate Redis instance.&lt;/p&gt;

&lt;p&gt;Our next post will look at the next puzzle piece in LiteStack: the ActiveSupport cache store it provides. We'll test out how it can help us to lower server response times, and look at some benchmarks again.&lt;/p&gt;

&lt;p&gt;See you then!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Handle Incoming Webhooks with LiteJob for Ruby on Rails</title>
      <dc:creator>julianrubisch</dc:creator>
      <pubDate>Wed, 22 Nov 2023 15:18:01 +0000</pubDate>
      <link>https://dev.to/appsignal/handle-incoming-webhooks-with-litejob-for-ruby-on-rails-ai6</link>
      <guid>https://dev.to/appsignal/handle-incoming-webhooks-with-litejob-for-ruby-on-rails-ai6</guid>
      <description>&lt;p&gt;In parts one and two of this series, we only dealt with the pure CRUD aspects of using SQLite as a production database.&lt;/p&gt;

&lt;p&gt;In this post, we will explore the world of queue mechanisms, using SQLite as the pub/sub adapter for ActiveJob.&lt;/p&gt;

&lt;p&gt;Let's make use of LiteJob to handle incoming webhooks in our Rails application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Ruby on Rails Use Case
&lt;/h2&gt;

&lt;p&gt;Our use case is the image generation that we are currently doing synchronously in the &lt;code&gt;PromptsController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stability-ai/stable-diffusion-img2img"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest_version&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;prompt: &lt;/span&gt;&lt;span class="n"&gt;prompt_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;image: &lt;/span&gt;&lt;span class="vi"&gt;@prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;replicate_rails_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clearly, there are some long-running processes here that should be deferred to a background job: the &lt;em&gt;loading of the model&lt;/em&gt;, and the &lt;em&gt;prediction&lt;/em&gt; itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;In your environment configuration files, ensure you reference LiteJob as the queue adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/development.rb&lt;/span&gt;
&lt;span class="c1"&gt;# config/environments/production.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&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="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:litejob&lt;/span&gt;

  &lt;span class="c1"&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;Now we're all set to continue our investigation. Don't forget to restart your development server, though!&lt;/p&gt;

&lt;p&gt;If you like, you can add more ActiveJob configuration in &lt;code&gt;config/litejob.yml&lt;/code&gt; — for example, queue priorities:&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;queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;urgent&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;critical&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spawn"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please refer to the &lt;a href="https://github.com/oldmoe/litestack#configuration-file" rel="noopener noreferrer"&gt;README&lt;/a&gt; for further details.&lt;/p&gt;

&lt;p&gt;That's set up, so let's move on to generating our images asynchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Images Asynchronously in Our Ruby on Rails Application
&lt;/h2&gt;

&lt;p&gt;We'll start by scaffolding our job with the usual Rails generator command:&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="nv"&gt;$ &lt;/span&gt;bin/rails g job GenerateImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's move the image generation code from &lt;code&gt;PromptsController&lt;/code&gt; into the &lt;code&gt;perform&lt;/code&gt; method of this job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateImageJob&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="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url_helpers&lt;/span&gt;

  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stability-ai/stable-diffusion-img2img"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest_version&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;prompt: &lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;image: &lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;replicate_rails_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&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;Note that we have to include the URL helpers module here to access &lt;code&gt;replicate_rails_url&lt;/code&gt; (as opposed to controllers, where this happens automatically). Furthermore, we also need to explicitly specify the &lt;code&gt;host&lt;/code&gt; — we grab it off ActionMailer's &lt;code&gt;default_url_options&lt;/code&gt;, in this case. Look at &lt;a href="https://andycroll.com/ruby/url-helpers-outside-views-controllers/" rel="noopener noreferrer"&gt;Andy Croll's excellent 'Use Rails URL helpers outside views and controllers' post&lt;/a&gt; for more sophisticated uses.&lt;/p&gt;

&lt;p&gt;To make this work with the incoming webhook, make sure you also reference your ngrok URL in your app's configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/development.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_NGROK_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port: &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the final step on the requesting side of our call to Replicate, we have to actually call the job from &lt;code&gt;PromptsController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/controllers/prompts_controller.rb
&lt;span class="err"&gt;
&lt;/span&gt;  def create
    # ...
&lt;span class="gd"&gt;-   model = Replicate.client.retrieve_model("stability-ai/stable-diffusion-img2img")
-   version = model.latest_version
-   version.predict({prompt: prompt_params[:title],
-     image: @prompt.data_url}, replicate_rails_url)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    respond_to do |format|
      if @prompt.save
&lt;span class="gi"&gt;+       GenerateImageJob.perform_later(prompt: @prompt)
+
&lt;/span&gt;        format.html { redirect_to prompt_url(@prompt), notice: "Prompt was successfully created." }
        format.json { render :show, status: :created, location: @prompt }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @prompt.errors, status: :unprocessable_entity }
      end
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Persisting Replicate.com Predictions
&lt;/h2&gt;

&lt;p&gt;Now it's time to take a look at the receiving end of our prediction request, i.e., the incoming webhook.&lt;/p&gt;

&lt;p&gt;Right now, generated predictions are handed back to us, but we don't do anything with them. Since images are purged from Replicate's CDN periodically, we want to store them locally.&lt;/p&gt;

&lt;p&gt;Let's start by generating a new child model, &lt;code&gt;Prediction&lt;/code&gt;. We'll prepare it to store the generated image as a binary blob again:&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="nv"&gt;$ &lt;/span&gt;bin/rails g model Prediction prompt:references replicate_id:string replicate_version:string prediction_image:binary logs:text
&lt;span class="nv"&gt;$ &lt;/span&gt;bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, we create columns to store some metadata returned from Replicate.com: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, and &lt;code&gt;logs&lt;/code&gt; — &lt;a href="https://replicate.com/docs/reference/http#predictions.create" rel="noopener noreferrer"&gt;see the API docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we also have to register this new association in the &lt;code&gt;Prompt&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/models/prompt.rb
&lt;span class="err"&gt;
&lt;/span&gt;  class Prompt &amp;lt; ApplicationRecord
    include AccountScoped
&lt;span class="err"&gt;
&lt;/span&gt;    belongs_to :account
&lt;span class="gi"&gt;+   has_many :predictions, dependent: :destroy
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    # ...
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we just have to process the incoming predictions in our webhook, but we face an issue. We have to find a way to identify the specific &lt;code&gt;Prompt&lt;/code&gt; instance the prediction was created for. We can circumvent this problem by adding a query param to the incoming webhook URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  # app/jobs/generate_image_job.rb
&lt;span class="err"&gt;
&lt;/span&gt;  class GenerateImageJob &amp;lt; ApplicationJob
    include Rails.application.routes.url_helpers
&lt;span class="err"&gt;
&lt;/span&gt;    queue_as :default
&lt;span class="err"&gt;
&lt;/span&gt;    def perform(prompt:)
      model = Replicate.client.retrieve_model("stability-ai/stable-diffusion-img2img")
      version = model.latest_version
&lt;span class="gd"&gt;-     version.predict({prompt: prompt.title, image: prompt.data_url}, replicate_rails_url(host: Rails.application.config.action_mailer.default_url_options[:host]))
&lt;/span&gt;&lt;span class="gi"&gt;+     version.predict({prompt: prompt.title, image: prompt.data_url}, replicate_rails_url(host: Rails.application.config.action_mailer.default_url_options[:host], params: {sgid: prompt.to_sgid.to_s}))
&lt;/span&gt;    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will effectively send our incoming webhook to &lt;code&gt;YOUR_NGROK_URL/replicate/webhook?sgid=YOUR_RECORD_SGID&lt;/code&gt;, making it easy to identify the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obtain the &lt;code&gt;sgid&lt;/code&gt; in Ruby
&lt;/h2&gt;

&lt;p&gt;Sadly, the &lt;code&gt;replicate-rails&lt;/code&gt; gem's default controller doesn't pass the query parameters to the webhook handler, but it &lt;em&gt;does&lt;/em&gt; pass the exact webhook URL.&lt;/p&gt;

&lt;p&gt;So we can flex our Ruby standard library muscles to obtain the &lt;code&gt;sgid&lt;/code&gt; from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/replicate.rb&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"open-uri"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReplicateWebhook&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;

    &lt;span class="n"&gt;sgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CGI&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="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"sgid"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GlobalID&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locate_signed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sgid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predictions&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;prediction_image: &lt;/span&gt;&lt;span class="no"&gt;URI&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="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;replicate_id: &lt;/span&gt;&lt;span class="n"&gt;prediction&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;replicate_version: &lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;logs: &lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logs&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;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's dissect this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; First, we pluck the exact webhook URL off the incoming &lt;code&gt;prediction&lt;/code&gt; object and parse it, returning the &lt;code&gt;query&lt;/code&gt; string.&lt;/li&gt;
&lt;li&gt; Next, we invoke &lt;code&gt;CGI.parse&lt;/code&gt; to convert the query string into a hash. Accessing the &lt;code&gt;"sgid"&lt;/code&gt; key returns a one-element array, which is why we have to send &lt;code&gt;first&lt;/code&gt; to it.&lt;/li&gt;
&lt;li&gt; Then, we can use the obtained &lt;code&gt;sgid&lt;/code&gt; to locate the &lt;code&gt;prompt&lt;/code&gt; instance.&lt;/li&gt;
&lt;li&gt; Finally, we append a new prediction to our prompt using the properties returned from Replicate.com. We also download the prediction image and save it in SQLite. Note that we can skip resizing because the generated image will already have the correct dimensions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's one final bit we have to tend to: we have to make sure our webhook handler works in an idempotent way. Webhooks can arrive duplicated or out of order, so we have to prepare for this case.&lt;/p&gt;

&lt;p&gt;Luckily, we can use the ID returned by replicate to ensure idempotency. All we have to do is add a unique index to the &lt;code&gt;replicate_id&lt;/code&gt; column. This will make it impossible to add a duplicate prediction to our database:&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="nv"&gt;$ &lt;/span&gt;bin/rails g migration AddUniqueIndexToPredictions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddUniqueIndexToPredictions&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.0&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;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:replicate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&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 shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that you typically would still want to store your assets and attachments in object storage buckets like Amazon S3 or DigitalOcean spaces. To get a glimpse of what SQLite can do, we have opted to store and render them directly from the database here.&lt;/p&gt;

&lt;p&gt;To complete our picture — pun intended — we again have to find a way to display our stored predictions as children of a prompt. We can do this using a data URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  class Prediction &amp;lt; ApplicationRecord
    belongs_to :prompt
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   def data_url
+     encoded_data = Base64.strict_encode64(prediction_image)
+
+     "data:image/png;base64,#{encoded_data}"
+   end
&lt;/span&gt;  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  &amp;lt;!-- app/views/prompts/_prompt.html.erb --&amp;gt;
  &amp;lt;div id="&amp;lt;%= dom_id prompt %&amp;gt;"&amp;gt;
    &amp;lt;p&amp;gt;
      &amp;lt;strong&amp;gt;Title:&amp;lt;/strong&amp;gt;
      &amp;lt;%= prompt.title %&amp;gt;
    &amp;lt;/p&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;    &amp;lt;p&amp;gt;
      &amp;lt;strong&amp;gt;Description:&amp;lt;/strong&amp;gt;
      &amp;lt;%= prompt.description %&amp;gt;
    &amp;lt;/p&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;    &amp;lt;p&amp;gt;
      &amp;lt;strong&amp;gt;Prompt image:&amp;lt;/strong&amp;gt;
      &amp;lt;%= image_tag prompt.data_url %&amp;gt;
    &amp;lt;/p&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   &amp;lt;p&amp;gt;
+     &amp;lt;strong&amp;gt;Generated images:&amp;lt;/strong&amp;gt;
+     &amp;lt;% prompt.predictions.each do |prediction| %&amp;gt;
+       &amp;lt;%= image_tag prediction.data_url %&amp;gt;
+     &amp;lt;% end %&amp;gt;
+   &amp;lt;/p&amp;gt;
&lt;/span&gt;  &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Look Behind the Scenes of LiteJob for Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;Let's quickly look into how LiteJob uses SQLite to implement a job queueing system. In essence, the class &lt;a href="https://github.com/oldmoe/litestack/blob/master/lib/litestack/litequeue.rb" rel="noopener noreferrer"&gt;&lt;code&gt;Litequeue&lt;/code&gt;&lt;/a&gt; interfaces with the SQLite &lt;code&gt;queue&lt;/code&gt; table. This table's columns, like &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;fire_at&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, and &lt;code&gt;created_at&lt;/code&gt;, store and manage job details.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;repush&lt;/code&gt; methods of the &lt;code&gt;Litequeue&lt;/code&gt; class add jobs to the queue, interfacing with their respective SQL statements. When it's time for a job's execution, the &lt;code&gt;pop&lt;/code&gt; method in the same class retrieves and removes a job based on its scheduled time. The &lt;code&gt;delete&lt;/code&gt; method allows for specific job removal.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Litejobqueue&lt;/code&gt; class is a subclass of &lt;code&gt;Litequeue&lt;/code&gt;, which, upon initialization, is tasked with &lt;a href="https://github.com/oldmoe/litestack/blob/master/lib/litestack/litejobqueue.rb#L186" rel="noopener noreferrer"&gt;creating the worker/s&lt;/a&gt;. These workers contain the main run loop that oversees the actual job execution, continuously fetching and running jobs from the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations of LiteJob
&lt;/h2&gt;

&lt;p&gt;Now that we've broken down how to use LiteJob for asynchronous job processing, let's look at some of the potential drawbacks ensuing from this approach.&lt;/p&gt;

&lt;p&gt;While an SQLite-based architecture provides simplicity and reduces the need for external dependencies, it also introduces several limitations.&lt;/p&gt;

&lt;p&gt;Firstly, LiteJob's reliance on SQLite inherently restricts its horizontal scaling capabilities. Unlike other databases, SQLite is designed for single-machine use, making it challenging to distribute workload across multiple servers. This can certainly be done using novel technologies like &lt;a href="https://github.com/superfly/litefs" rel="noopener noreferrer"&gt;LiteFS&lt;/a&gt;, but it is far from intuitive.&lt;/p&gt;

&lt;p&gt;Additionally, even on the same machine, concurrency contends with your web server (e.g., Puma) because LiteJob spawns threads or fibers that compete with those handling the web requests.&lt;/p&gt;

&lt;p&gt;On the other hand (and crucially, for the majority of smaller apps), you might never need such elaborate scaling concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  LiteJob's Performance in Benchmarks
&lt;/h2&gt;

&lt;p&gt;On a more positive note, LiteJob seems to allow for a lot of overhead before you have to scale horizontally: at least the &lt;a href="https://github.com/oldmoe/litestack/blob/master/BENCHMARKS.md" rel="noopener noreferrer"&gt;benchmarks&lt;/a&gt; put me in a cautiously euphoric mood. Granted, the benchmarks don't carry a realistic payload, but they demonstrate that LiteJob makes very efficient use of threads and fibers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Up Next: Streaming Updates with LiteCable
&lt;/h2&gt;

&lt;p&gt;In this installment of our series, we delved deeper into the intricacies of using SQLite with LiteJob to handle asynchronous image generation and incoming webhooks in Rails applications.&lt;/p&gt;

&lt;p&gt;To recap, while the SQLite-based architecture of LiteJob offers simplicity and reduced external dependencies, it also presents challenges in horizontal scaling. However, for smaller applications, LiteJob's efficiency with threads and fibers suggests it can handle a significant workload before necessitating more complex scaling solutions.&lt;/p&gt;

&lt;p&gt;In the next post of this series, we'll look at providing reactive updates to our users using Hotwire powered by LiteCable.&lt;/p&gt;

&lt;p&gt;Until then, happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
  </channel>
</rss>
