<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
  <title>BigBinary Blog: Solid Queue</title>
  <subtitle>Deep technical writing on Ruby on Rails, performance, scaling, monitoring and engineering.</subtitle>
  <link rel="self" type="application/atom+xml" href="https://www.bigbinary.com/blog/categories/solid-queue/feed.xml"/>
  <link rel="alternate" type="text/html" href="https://www.bigbinary.com/blog/categories/solid-queue"/>
  <id>https://www.bigbinary.com/blog/categories/solid-queue</id>
  <updated>2025-06-03T00:00:00.000Z</updated>
  <author>
    <name>BigBinary</name>
    <uri>https://www.bigbinary.com/</uri>
  </author>
  <icon>https://www.bigbinary.com/favicon.ico</icon>
  <logo>https://www.bigbinary.com/images/bigbinary_square_big_logo.png</logo>
  <rights>Copyright 2025 Big Binary LLC</rights>
  <generator uri="https://nextjs.org">Next.js</generator>
  <entry>
      <title type="text"><![CDATA[Understanding Queueing Theory]]></title>
      <link rel="alternate" type="text/html" href="https://www.bigbinary.com/blog/understanding-queueing-theory"/>
      <link rel="enclosure" type="image/jpeg" href="https://www.bigbinary.com/blog/images/social_preview_images/understanding_queueing_theory.jpg"/>
      <id>https://www.bigbinary.com/blog/understanding-queueing-theory</id>
      <published>2025-06-03T00:00:00.000Z</published>
      <updated>2025-06-03T00:00:00.000Z</updated>
      <author>
        <name>Vishnu M</name>
        <uri>https://www.bigbinary.com/blog/authors/vishnu-m</uri>
      </author>
      <category term="Rails"/>
      <category term="Sidekiq"/>
      <category term="Ruby"/>
      <category term="Solid Queue"/>
      <media:thumbnail url="https://www.bigbinary.com/blog/images/social_preview_images/understanding_queueing_theory.jpg" width="1200" height="627"/>
      <summary type="text"><![CDATA[This is Part 6 of our blog series on scaling Rails applications. --- Queueing Systems In web applications, not every task needs to be processed immediately. When you upload a large video file, send a...]]></summary>
      <content type="html" xml:base="https://www.bigbinary.com/blog/understanding-queueing-theory"><![CDATA[<p><em>This is Part 6 of our blog series on
<a href="https://www.bigbinary.com/blog/scaling-rails-series">scaling Rails applications</a>.</em></p>
<hr>
<h2>Queueing Systems</h2>
<p>In web applications, not every task needs to be processed immediately. When you
upload a large video file, send a bulk email campaign, or generate a complex
report, these time-consuming operations are often handled in the background.
This is where queueing systems like <a href="https://sidekiq.org/">Sidekiq</a> or
<a href="https://github.com/rails/solid_queue">Solid Queue</a> comes into play.</p>
<p>Queueing theory helps us understand how these systems behave under different
conditions - from quiet periods to peak load times.</p>
<p>Let's understand the fundamentals of queueing theory.</p>
<h2>Basic Terminology in Queueing Theory</h2>
<ol>
<li>
<p><strong>Unit of Work</strong>: This is the individual item needing service - a job.</p>
</li>
<li>
<p><strong>Server</strong>: This is one &quot;unit of parallel processing capacity.&quot; In queuing
theory, this doesn't necessarily mean a physical server. It refers to the
ability to process one unit of work at a time. For JRuby or TruffleRuby, each
thread can be considered a separate &quot;server&quot; since they can execute in
parallel. For CRuby/MRI, because of the GVL, the concept of a &quot;server&quot; is
different. We'll discuss it later.</p>
</li>
<li>
<p><strong>Queue Discipline</strong>: This is the rule determining which unit of work is
selected next from the queue. For Sidekiq and Solid Queue, it is FCFS(First
Come First Serve). If there are multiple queues, which job is selected
depends on the priority of the queue.</p>
</li>
<li>
<p><strong>Service Time</strong>: The actual time it takes to process a unit of work (how
long a job takes to execute).</p>
</li>
<li>
<p><strong>Latency/Wait Time</strong>: How long jobs spend waiting in the queue before being
processed.</p>
</li>
<li>
<p><strong>Total Time</strong>: The sum of service time and wait time. It's the complete
duration from when a job is enqueued until it finishes executing.</p>
</li>
</ol>
<h2>Little's law</h2>
<p>Little's law is a theorem in queuing theory that states that the average number
of jobs in a system is equal to the average arrival rate of new jobs multiplied
by the average time a job spends in the system.</p>
<pre><code class="language-ruby">L = λW
</code></pre>
<p>L = Average number of jobs in the system <br>
λ = Average arrival rate of new jobs <br>
W = Average time a job spends in the system</p>
<p>For example, if jobs arrive at a rate of 10 per minute (λ), and each job takes
30 seconds (W) to complete:</p>
<p>Average number of jobs in system = 10 jobs/minute * 0.5 minutes = 5 jobs</p>
<p>This helps us understand the current state of our system and that there are 5
jobs in the system on average at any given moment.</p>
<p><code>L</code> is also called <strong>offered traffic</strong>.</p>
<p>Note: Little's Law assumes the arrival rate is consistent over time.</p>
<h3>Managing Utilization</h3>
<p>Utilization measures how busy our processing capacity is.</p>
<p>Mathematically, it is the ratio of how much processing capacity we're using to
the processing capacity we have.</p>
<pre><code class="language-ruby">utilization = (average number of jobs in the system/capacity to handle jobs) * 100
</code></pre>
<p>In other words, it could be written as follows.</p>
<pre><code class="language-ruby">utilization = (offered_traffic / parallelism) * 100
</code></pre>
<p>For example, if we are using Sidekiq to manage our background jobs then in a
single-threaded case, parallelism is equal to the number of Sidekiq processes.</p>
<p>Let's look at a practical case with numbers:</p>
<ul>
<li>We have 30 jobs arriving every minute</li>
<li>It takes 0.5 minutes to process a job</li>
<li>We have 20 Sidekiq processes</li>
</ul>
<p>In this case, the utilization will be:</p>
<pre><code class="language-ruby">utilization = ((30 jobs/minute * 0.5 minutes) / 20 processes) * 100 = 75%
</code></pre>
<h3>High utilization is bad for performance</h3>
<p>Let's assume we maintain 100% utilization in our system. It means that if, on
average, we get 30 jobs per minute, then we have just enough capacity to handle
30 jobs per minute.</p>
<p>One day, we started getting 45 jobs per minute. Since utilization is at 100%,
there is no extra room to accommodate the additional load. This leads to higher
latency.</p>
<p>Hence, having a high utilization rate may result in low performance, as it can
lead to higher latency for specific jobs.</p>
<h3>The knee curve</h3>
<p>Mathematically, it would seem that only when the utilization rate hits 100%
should the latency spike up. However, in the real world, it has been found that
latency begins to increase dramatically when utilization reaches around 70-75%.</p>
<p>If we draw a graph between utilization and performance then the graph would look
something like this.</p>
<p><img src="https://www.bigbinary.com/blog/images/images_used_in_blog/2025/understanding-queueing-theory/knee-curve.png" alt="The knee curve"></p>
<p>The point at which the curve bends sharply upwards is called &quot;Knee&quot; in the
performance curve. At this point, the exponential effects predicted by queueing
theory become pronounced, causing the queue latency to climb up quickly.</p>
<p>Running any system consistently above 70-75% utilization significantly increases
the risk of spiking the latency, as jobs spend more and more time waiting.</p>
<p>This would directly impact the customer experience, as it could result in delays
in sending emails or making calls to Twilio to send SMS messages, etc.</p>
<p>Tracking this latency will be covered in the upcoming blogs. The tracking of
metrics depends on the queueing backend used (Sidekiq or Solid Queue).</p>
<h2>Concurrency and theoretical parallelism</h2>
<p>In Sidekiq, a process is the primary unit of parallelism. However, concurrency
(threads per process) significantly impacts a process's effective throughput.
Because of GVL we need to take into account how long the job is waiting for I/O.</p>
<p>The more time a job spends waiting on external resources (like databases or
APIs) rather than executing Ruby code, the more other threads within the same
process can run Ruby code while the first thread waits.</p>
<p>We learned about Amdahl's law in
<a href="https://www.bigbinary.com/blog/amdahls-law-the-theoretical-relationship-between-speedup-and-concurrency">Part 3</a>
of this series.</p>
<p><img src="https://www.bigbinary.com/blog/images/images_used_in_blog/2025/understanding-queueing-theory/amdahls-law.png" alt="Amdahl's law"></p>
<p>Where:</p>
<p><code>p</code> is the portion that can be parallelized (the I/O percentage)</p>
<p><code>n</code> is the number of threads (concurrency)</p>
<p>Speedup is equivalent to theoretical parallelism in this context. In queueing
theory, parallelism refers to how many units of work can be processed
simultaneously. When we calculate speedup using Amdahl's Law, we're essentially
determining how much faster a multi-threaded system can handle work compared to
a single-threaded system.</p>
<p>Let's assume that a system has an I/O of 50% and a concurrency of 10. Then
Speedup will be:</p>
<pre><code class="language-ruby">Speedup = 1 / ((1 - 0.5) + 0.5 / 10) = 1 / 0.55 = 1.82 ≈ 2
</code></pre>
<p>This means one Sidekiq process with 10 threads will handle jobs twice as fast as
Sidekiq with a single process with a single thread.</p>
<p>Let's recap what we are saying here. We are assuming that the system has I/O of
50%. System is using a single Sidekiq process with 10 threads(concurrency). Then
because of 10 threads the system has a speed gain of 2x compared to system
having just a single thread. In other words, just because we have 10 threads
running we are not going to gain 10X performance improvement. What those 10
threads is getting us is what is called &quot;theoretical parallelism&quot;.</p>
<p>Similarly, for other values of I/O and Concurrency, we can get the theoretical
parallelism.</p>
<table>
<thead>
<tr>
<th>I/O</th>
<th>Concurrency</th>
<th>Theoretical parallelism</th>
</tr>
</thead>
<tbody>
<tr>
<td>5%</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>25%</td>
<td>5</td>
<td>1.25</td>
</tr>
<tr>
<td>50%</td>
<td>10</td>
<td>2</td>
</tr>
<tr>
<td>75%</td>
<td>16</td>
<td>3</td>
</tr>
<tr>
<td>90%</td>
<td>32</td>
<td>8</td>
</tr>
<tr>
<td>95%</td>
<td>64</td>
<td>16</td>
</tr>
</tbody>
</table>
<p>Let's go over one more time. In the last example, what we are stating is that if
a system has 95% I/O and if the system has 64 threads running, then that will
give 16x performance improvement over the same system running on a single
thread.</p>
<p>Here is the graph for this data.</p>
<p><img src="https://www.bigbinary.com/blog/images/images_used_in_blog/2025/understanding-queueing-theory/concurrency-vs-effective-parallelism.png" alt="Theoretical Parallelism"></p>
<p>As shown in the graph, a Sidekiq process with 16 threads handling jobs that are
75% I/O-bound achieves a theoretical parallelism of approximately 3. In other
words, 3x performance improvement is there over a single-threaded system.</p>
<h2>Calculating the number of processes required</h2>
<p>At the beginning of this article, we discussed &quot;Little's law&quot; and we discussed
that <code>L</code> is also called &quot;offered traffic,&quot; which depicts the &quot;average number of
jobs in the system&quot;.</p>
<p>If &quot;offered traffic&quot; is 5, then it means we have 5 units of work arriving on
average that requires processing simultaneously.</p>
<p>We just learned that if the utilization is greater than 75% then it can cause
problems as there is a risk of latency to spike.</p>
<p>For queues with low latency requirements(eg. <code>urgent</code>), we need to target a
lower utilization rate. Let's say we want utilization to be around 50% to be on
the safe side.</p>
<p>Now we know the utilization rate that we need to target and we know the &quot;offered
traffic&quot;. So now we can calculate the &quot;parallelism&quot;.</p>
<pre><code class="language-ruby">utilization = offered_traffic / parallelism
=&gt; 0.50 = 5 / parallelism
=&gt; parallelism = 5 / 0.50 = 10
</code></pre>
<p>This means we need a theoretical parallelism of 10 to ensure that the
utilization is 50% at max.</p>
<p>Let's assume the jobs in this queue have an average of <code>50%</code> I/O. Based on the
above mentioned graph, we can see that if the concurrency is 10 then we get
parallelism of 2. However, increasing the concurrency doesn't increase the
parallelism. It means if we want 10 parallelism, then we can't just switch to
concurrency of 50. Even the concurrency of 50 (or 50 threads) will only yield a
parallelism of 2.</p>
<p>So we have no choice but to add more processes. Since one process with 10
concurrency is yielding a parallelism of 2 we need to add 5 processes to get 10
parallelism.</p>
<p><em>To get the I/O wait percentage, we can make use of perfm.
<a href="https://github.com/bigbinary/perfm?tab=readme-ov-file#sidekiq-gvl-instrumentation">Here</a>
is the documentation on how it can be done.</em></p>
<pre><code class="language-ruby">Total number of Sidekiq processes required = 10 / 2 = 5
</code></pre>
<p>Here we're talking about Sidekiq free version, where we'll only be able to run a
single process per dyno. If we're using Sidekiq Pro, we can run multiple
processes per dyno via Sidekiq Swarm.</p>
<p>We can provision 5 dynos for the urgent queue. But we should always have a queue
time based autoscaler like Judoscale enabled to handle spikes.</p>
<h2>Sources of Saturation</h2>
<p>We discussed earlier that, in the context of queueing theory, the saturation
point is typically reached at around 70-75% utilization. This is from the point
of view of further gains by adding more threads.</p>
<p>However saturation can occur in other parts of the system.</p>
<h3>CPU</h3>
<p>The servers running your Sidekiq processes have finite CPU and memory. While CPU
usage is a metric we can track for Sidekiq, it's generally not the only one we
need to focus on for scaling decisions.</p>
<p>CPU utilization can be misleading. If our jobs spend most of their time doing
I/O (like making API calls or database queries), in which case CPU usage will be
very low, even when our Sidekiq system is at capacity.</p>
<h3>Memory</h3>
<p>Memory utilization impacts performance very differently from CPU utilization.
Memory utilization generally exhibits minimal changes in latency or throughput
from 0% to 100% utilization. However, after 100% utilization, things will start
to deteriorate significantly. The system will start using the swap memory, which
can be very slow and thereby increase the job service times.</p>
<h3>Redis</h3>
<p>Another place where saturation can occur is in our datastore i.e Redis in case
of Sidekiq. We have to make sure that we provision a separate Redis instance for
Sidekiq and also make sure to set the eviction policy to <code>noeviction</code>. This
ensures that Redis will reject new data when the memory limit is reached,
resulting in an explicit failure rather than silently dropping important jobs.</p>
<p><em>This was Part 6 of our blog series on
<a href="https://www.bigbinary.com/blog/scaling-rails-series">scaling Rails applications</a>. If any part of the
blog is not clear to you then please write to us at
<a href="https://www.linkedin.com/company/bigbinary">LinkedIn</a>,
<a href="https://twitter.com/bigbinary">Twitter</a> or
<a href="https://bigbinary.com/contact">BigBinary website</a>.</em></p>
]]></content>
    </entry>
  <entry>
      <title type="text"><![CDATA[How we migrated from Sidekiq to Solid Queue]]></title>
      <link rel="alternate" type="text/html" href="https://www.bigbinary.com/blog/migrating-to-solid-queue-from-sidekiq"/>
      <link rel="enclosure" type="image/jpeg" href="https://www.bigbinary.com/blog/images/social_preview_images/how_we_migrated_from_sidekiq_to_solid_queue.jpg"/>
      <id>https://www.bigbinary.com/blog/migrating-to-solid-queue-from-sidekiq</id>
      <published>2024-03-05T00:00:00.000Z</published>
      <updated>2024-03-05T00:00:00.000Z</updated>
      <author>
        <name>Chirag Shah</name>
        <uri>https://www.bigbinary.com/blog/authors/chiraggshah</uri>
      </author>
      <category term="Rails"/>
      <category term="Solid Queue"/>
      <media:thumbnail url="https://www.bigbinary.com/blog/images/social_preview_images/how_we_migrated_from_sidekiq_to_solid_queue.jpg" width="1200" height="627"/>
      <summary type="text"><![CDATA[How we migrated to Solid Queue from Sidekiq]]></summary>
      <content type="html" xml:base="https://www.bigbinary.com/blog/migrating-to-solid-queue-from-sidekiq"><![CDATA[<p>BigBinary is building a suite of products under <a href="https://neeto.com">neeto</a>. We
currently have around 22 products under development, and all of the products are
using <a href="https://github.com/sidekiq/sidekiq">Sidekiq</a>. After the
<a href="https://dev.37signals.com/introducing-solid-queue/">launch of Solid Queue</a>, we
decided to migrate <a href="https://neeto.com/neetoform">NeetoForm</a> from Sidekiq to
Solid Queue.</p>
<p>Please note that Solid Queue currently doesn't support cron-style or recurring
jobs. There is a <a href="https://github.com/basecamp/solid_queue/pull/155">PR open</a>
regarding this issue. We have only partially migrated to Solid Queue. For
recurring jobs, we are still using Sidekiq. Once the PR is merged, we will
migrate completely to Solid Queue.</p>
<h2>Migrating to Solid Queue from Sidekiq</h2>
<p>Here is a step-by-step migration guide you can use to migrate your Rails
application from Sidekiq to Solid Queue.</p>
<h2>1. Installation</h2>
<ul>
<li>Add <code>gem &quot;solid_queue&quot;</code> to your Rails application's Gemfile and run
<code>bundle install</code>.</li>
<li>Run <code>bin/rails generate solid_queue:install</code> which copies the config file and
the required migrations.</li>
<li>Run the migrations using <code>bin/rails db:migrate</code>.</li>
</ul>
<h2>2. Configuration</h2>
<p>The installation step should have created a <code>config/solid_queue.yml</code> file.
Uncomment the file and modify it as per your needs. Here is how the file looks
for our application.</p>
<pre><code class="language-yaml">default: &amp;default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: &quot;auth&quot;
      threads: 3
      processes: 1
      polling_interval: 0.1
    - queues: &quot;urgent&quot;
      threads: 3
      processes: 1
      polling_interval: 0.1
    - queues: &quot;low&quot;
      threads: 3
      processes: 1
      polling_interval: 2
    - queues: &quot;*&quot;
      threads: 3
      processes: 1
      polling_interval: 1

development:
  &lt;&lt;: *default

staging:
  &lt;&lt;: *default

heroku:
  &lt;&lt;: *default

test:
  &lt;&lt;: *default

production:
  &lt;&lt;: *default
</code></pre>
<h2>3. Starting Solid Queue</h2>
<p>On your development machine, you can start Solid Queue by running the following
command.</p>
<pre><code>bundle exec rake solid_queue:start
</code></pre>
<p>This will start Solid Queue's supervisor process and will start processing any
enqueued jobs. The supervisor process forks
<a href="https://github.com/basecamp/solid_queue?tab=readme-ov-file#workers-and-dispatchers">workers and dispatchers</a>
according to the configuration provided in the <code>config/solid_queue.yml</code> file.
The supervisor process also controls the heartbeats of workers and dispatchers,
and sends signals to stop and start them when needed.</p>
<p>Since we use <a href="https://github.com/ddollar/foreman">foreman</a>, we added the above
command to our Procfile.</p>
<pre><code class="language-ruby"># Procfile
web:  bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -C config/sidekiq.yml
solidqueueworker: bundle exec rake solid_queue:start
release: bundle exec rake db:migrate
</code></pre>
<h2>4. Setting the Active Job queue adapter</h2>
<p>You can set the Active Job queue adapter to <code>:solid_queue</code> by adding the
following line in your <code>application.rb</code> file.</p>
<pre><code class="language-ruby"># application.rb
config.active_job.queue_adapter = :solid_queue
</code></pre>
<p>The above change sets the queue adapter at the application level for all the
jobs. However, since we wanted to use Solid Queue for our regular jobs and
continue using Sidekiq for cron jobs, we didn't make the above change in
<code>application.rb</code>.</p>
<p>Instead, we created a new base class that inherited from <code>ApplicationJob</code> and
set the queue adapter to <code>:solid_queue</code> inside that.</p>
<pre><code class="language-ruby"># sq_base_job.rb
class SqBaseJob &lt; ApplicationJob
  self.queue_adapter = :solid_queue
end
</code></pre>
<p>Then we made all the classes implementing regular jobs inherit from this new
class <code>SqBaseJob</code> instead of <code>ApplicationJob</code>.</p>
<pre><code class="language-diff"># send_email_job.rb
- class SendEmailJob &lt; ApplicationJob
+ class SendEmailJob &lt; SqBaseJob
  # ...
end
</code></pre>
<p>By making the above change, all our regular jobs got enqueued via Solid Queue
instead of Sidekiq.</p>
<p>But we realized later that emails were still being sent via Sidekiq. On
debugging and looking into Rails internals, we found that <code>ActionMailer</code> uses
<code>ActionMailer::MailDeliveryJob</code> for enqueuing or sending emails.</p>
<p><code>ActionMailer::MailDeliveryJob</code> inherits from <code>ActiveJob::Base</code> rather than the
application's <code>ApplicationJob</code>. So even if we set the queue_adapter in
<code>application_job.rb</code>, it won't work. <code>ActionMailer::MailDeliveryJob</code> fallbacks
to using the adapter defined in <code>application.rb</code> or environment-specific
(production.rb / staging.rb / development.rb) config files. But we can't do that
because we still want to use Sidekiq for cron jobs.</p>
<p>To use Solid Queue for mailers, we needed to override the queue_adapter for
mailers. We can do that in <code>application_mailer.rb</code>.</p>
<pre><code class="language-ruby"># application_mailer.rb
class ApplicationMailer &lt; ActionMailer::Base
  # ...
  ActionMailer::MailDeliveryJob.queue_adapter = :solid_queue
end
</code></pre>
<p>This change is only until we use both Sidekiq and Solid Queue. Once cron style
jobs feature lands in Solid Queue, we can remove this override and set the
queue_adapter directly in <code>application.rb</code>, which will enforce the setting
globally.</p>
<h2>5. Code changes</h2>
<p>For migrating from Sidekiq to Solid Queue, we had to make the following changes
to the syntax for enqueuing a job.</p>
<ul>
<li>Replaced <code>.perform_async</code> with <code>.perform_later</code>.</li>
<li>Replaced <code>.perform_at</code> with <code>.set(...).perform_later(...)</code>.</li>
</ul>
<pre><code class="language-diff">- SendMailJob.perform_async
+ SendMailJob.perform_later

- SendMailJob.perform_at(1.minute.from_now)
+ SendMailJob.set(wait: 1.minute).perform_later
</code></pre>
<p>At some places we were storing the Job ID on a record, for querying the job's
status or for cancelling the job. For such cases, we made the following change.</p>
<pre><code class="language-diff">def disable_form_at_deadline
- job_id = DisableFormJob.perform_at(deadline, self.id)
- self.disable_job_id = job_id
+ job_id = DisableFormJob.set(wait_until: deadline).perform_later(self.id)
+ self.disable_job_id = job.job_id
end

def cancel_form_deadline
- Sidekiq::Status.cancel(self.disable_job_id)
+ SolidQueue::Job.find_by(active_job_id: self.disable_job_id).destroy!
  self.disable_job_id = nil
end
</code></pre>
<h2>6. Error handling and retries</h2>
<p>Initially, we thought the
<a href="https://github.com/basecamp/solid_queue?tab=readme-ov-file#other-configuration-settings"><code>on_thread_error</code> configuration</a>
provided by Solid Queue can be used for error handling. However, during the
development phase, we noticed that it wasn't capturing errors. We raised
<a href="https://github.com/basecamp/solid_queue/issues/120">an issue with Solid Queue</a>
as we thought it was a bug.</p>
<p><a href="https://github.com/rosa">Rosa Gutiérrez</a>
<a href="https://github.com/basecamp/solid_queue/issues/120#issuecomment-1894413948">responded</a>
on the issue and clarified the following.</p>
<blockquote>
<p><code>on_thread_error</code> wasn't intended for errors on the job itself, but rather
errors in the thread that's executing the job, but around the job itself. For
example, if you had an Active Record's thread pool too small for your number
of threads and you got an error when trying to check out a new connection,
on_thread_error would be called with that.</p>
<p>For errors in the job itself, you could try to hook into Active Job's itself.</p>
</blockquote>
<p>Based on the above information, we modified our <code>SqBaseJob</code> base class to handle
the exceptions and report it to <a href="https://www.honeybadger.io/">Honeybadger</a>.</p>
<pre><code class="language-ruby"># sq_base_job.rb
class SqBaseJob &lt; ApplicationJob
  self.queue_adapter = :solid_queue

  rescue_from(Exception) do |exception|
    context = {
      error_class: self.class.name,
      args: self.arguments,
      scheduled_at: self.scheduled_at,
      job_id: self.job_id
    }
    Honeybadger.notify(exception, context:)
    raise exception
  end
end
</code></pre>
<p>Remember we mentioned that <code>ActionMailer</code> doesn't inherit from <code>ApplicationJob</code>.
So similarly, we would have to handle exceptions for Mailers separately.</p>
<pre><code class="language-ruby"># application_mailer.rb
class ApplicationMailer &lt; ActionMailer::Base
  # ...
  ActionMailer::MailDeliveryJob.rescue_from(Exception) do |exception|
    context = {
      error_class: self.class.name,
      args: self.arguments,
      scheduled_at: self.scheduled_at,
      job_id: self.job_id
    }
    Honeybadger.notify(exception, context:)
    raise exception
  end
end
</code></pre>
<p>For retries, unlike Sidekiq, Solid Queue doesn't include any automatic retry
mechanism, it
<a href="https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs">relies on Active Job for this</a>.
We wanted our application to retry sending emails in case of any errors. So we
added the retry logic in the <code>ApplicationMailer</code>.</p>
<pre><code class="language-ruby"># application_mailer.rb
class ApplicationMailer &lt; ActionMailer::Base
  # ...
  ActionMailer::MailDeliveryJob.retry_on StandardError, attempts: 3
end
</code></pre>
<p>Note that, although the queue adapter configuration can be removed from
<code>application_mailer.rb</code> once the entire application migrates to Solid Queue,
error handling and retry override cannot be removed because of the way
<code>ActionMailer::MailDeliveryJob</code> inherits from <code>ActiveJob::Base</code> rather than
application's <code>ApplicationJob</code>.</p>
<h2>7. Testing</h2>
<p>Once all the above changes were done, it was obvious that a lot of tests were
failing. Apart from fixing the usual failures related to the syntax changes,
some of the tests were failing inconsistently. On debugging, we found that the
affected tests were all related to controllers, specifically tests inheriting
from <code>ActionDispatch::IntegrationTest</code>.</p>
<p>We tried debugging and searched for solutions when we stumbled upon
<a href="https://github.com/bensheldon">Ben Sheldon's</a>
<a href="https://github.com/bensheldon/good_job/issues/846#issuecomment-1432375562">comment on one of Good Job's issues</a>.
Ben points out that this is actually
<a href="https://github.com/rails/rails/issues/37270">an issue in Rails</a> where Rails
sometimes inconsistently overrides ActiveJob's queue_adapter setting with
TestAdapter. A <a href="https://github.com/rails/rails/pull/48585">PR is already open</a>
for the fix. Thankfully, Ben, in the same comment, also mentioned a workaround
for it until the fix has been added to Rails.</p>
<p>We added the workaround in our test <code>helper_methods.rb</code> and called the method in
each of our controller tests which were failing.</p>
<pre><code class="language-ruby"># test/support/helper_methods.rb
def ensure_consistent_test_adapter_is_used
  # This is a hack mentioned here: https://github.com/bensheldon/good_job/issues/846#issuecomment-1432375562
  # The actual issue is in Rails for which a PR is pending merge
  # https://github.com/rails/rails/pull/48585
  (ActiveJob::Base.descendants + [ActiveJob::Base]).each(&amp;:disable_test_adapter)
end
</code></pre>
<pre><code class="language-ruby"># test/controllers/exports_controller_test.rb
class ExportsControllerTest &lt; ActionDispatch::IntegrationTest
  def setup
    ensure_consistent_test_adapter_is_used
    # ...
  end

  # ...
end
</code></pre>
<h2>8. Monitoring</h2>
<p>Basecamp has released
<a href="https://github.com/basecamp/mission_control-jobs">mission_control-jobs</a>, which
can be used to monitor background jobs. It is generic, so it can be used with
any compatible ActiveJob adapter.</p>
<p>Add <code>gem &quot;mission_control-jobs&quot;</code> to your Gemfile and run <code>bundle install</code>.</p>
<p>Mount the mission control route in your <code>routes.rb</code> file.</p>
<pre><code class="language-ruby"># routes.rb
Rails.application.routes.draw do
  # ...
  mount MissionControl::Jobs::Engine, at: &quot;/jobs&quot;
</code></pre>
<p>By default, mission control would try to load the adapter specified in your
<code>application.rb</code> or individual environment-specific files. Currently, Sidekiq
isn't compatible with mission control, so you will face an error while loading
the dashboard at <code>/jobs</code>. The fix is to explicitly specify <code>solid_queue</code> to the
list of mission control adapters.</p>
<pre><code class="language-ruby"># application.rb
# ...
config.mission_control.jobs.adapters = [:solid_queue]
</code></pre>
<p>Now, visiting <code>/jobs</code> on your site should load a dashboard where you can monitor
your Solid Queue jobs.</p>
<p>But that isn't enough. There is no authentication. For development environments,
it is fine, but the <code>/jobs</code> route would be exposed on production too. By
default, Mission Control's controllers will extend the host app's
<code>ApplicationController</code>. If no authentication is enforced, <code>/jobs</code> will be
available to everyone.</p>
<p>To implement some kind of authentication, we can specify a different controller
as the base class for Mission Control's controllers and add the authentication
there.</p>
<pre><code class="language-ruby"># application.rb
# ...
MissionControl::Jobs.base_controller_class = &quot;MissionControlController&quot;
</code></pre>
<pre><code class="language-ruby"># app/controllers/mission_control_controller.rb
class MissionControlController &lt; ApplicationController
  before_action :authenticate!, if: :restricted_env?

  private

    def authenticate!
      authenticate_or_request_with_http_basic do |username, password|
        username == &quot;solidqueue&quot; &amp;&amp; password == Rails.application.secrets.mission_control_password
      end
    end

    def restricted_env?
      Rails.env.staging? || Rails.env.production?
    end
end
</code></pre>
<p>Here, we have specified that <code>MissionControlController</code> would be our base
controller for mission control related controllers. Then in
<code>MissionControlController</code> we implemented basic authentication for staging and
production environments.</p>
<h2>Observations</h2>
<p>We haven't had any complaints so far. Solid Queue offers simplicity, requires no
additional infrastructure and provides visibility for managing jobs since they
are stored in the database.</p>
<p>In the coming days, we will migrate all of our 22 Neeto products to Solid Queue.
And once cron-style job support lands in Solid Queue, we will completely migrate
from Sidekiq.</p>
]]></content>
    </entry>
  <entry>
      <title type="text"><![CDATA[Solid Queue & understanding UPDATE SKIP LOCKED]]></title>
      <link rel="alternate" type="text/html" href="https://www.bigbinary.com/blog/solid-queue"/>
      <link rel="enclosure" type="image/jpeg" href="https://www.bigbinary.com/blog/images/social_preview_images/solid_queue.jpg"/>
      <id>https://www.bigbinary.com/blog/solid-queue</id>
      <published>2024-01-23T00:00:00.000Z</published>
      <updated>2024-01-23T00:00:00.000Z</updated>
      <author>
        <name>Chirag Shah</name>
        <uri>https://www.bigbinary.com/blog/authors/chiraggshah</uri>
      </author>
      <category term="Rails"/>
      <category term="Solid Queue"/>
      <media:thumbnail url="https://www.bigbinary.com/blog/images/social_preview_images/solid_queue.jpg" width="1200" height="627"/>
      <summary type="text"><![CDATA[Introduction to solid queue]]></summary>
      <content type="html" xml:base="https://www.bigbinary.com/blog/solid-queue"><![CDATA[<h2>What is solid queue?</h2>
<p>Recently, <a href="https://37signals.com/">37signals</a> open sourced
<a href="https://dev.37signals.com/introducing-solid-queue">Solid Queue</a>.</p>
<p>Solid Queue is database based queuing backend for Active Job. In contrast
Sidekiq and Resque are Redis-based queuing backends.</p>
<p>In her blog <a href="https://github.com/rosa">Rosa Gutiérrez</a> mentioned following lines
which captured our attention.</p>
<blockquote>
<p>In our case, one feature that PostgreSQL has had for quite some time and that
was finally introduced in MySQL 8, has been crucial to our implementation:</p>
<p>SELECT ... FOR UPDATE SKIP LOCKED</p>
<p>This allows Solid Queue’s workers to fetch and lock jobs without locking other
workers.</p>
</blockquote>
<p>As per her, this feature had been
<a href="https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE">in PostgreSQL</a>
for a while, and now this feature has landed
<a href="https://dev.mysql.com/blog-archive/mysql-8-0-1-using-skip-locked-and-nowait-to-handle-hot-rows">in MySQL</a>
making it possible to build Solid Queue.</p>
<p>We had never heard of <code>UPDATE SKIP LOCKED</code> feature either in PostgreSQL or in
MySQL. We were wondering what is this <code>UPDATE SKIP LOCKED</code> without which it was
not possible to build Solid Queue. So we decided to look into it.</p>
<h2>Processing jobs from a queue</h2>
<p>Consider a case where we need to build a system where a bunch of jobs need to be
processed in the background.</p>
<p>There are a bunch of workers waiting to grab a job and start processing the
moment a job becomes available. The challenge is when multiple workers attempt
to claim the same job simultaneously how do we ensure that only one of the
workers claims the job for processing. At any point of time, a worker should
claim only the &quot;unclaimed&quot; job, and an &quot;unclaimed&quot; job should be claimed by one
and only one worker.</p>
<p>Here is how one might go about it implementing it.</p>
<pre><code class="language-sql">START TRANSACTION
SELECT * FROM JOBS WHERE processed='no' LIMIT 1;
-- Process the job
COMMIT;
</code></pre>
<p>With the above code, it's possible that two workers might claim the same job.</p>
<p>One way to resolve this issue is by marking a particular row as locked for
update.</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT * FROM JOBS WHERE processed='no' FOR UPDATE LIMIT 1;
-- Process the job
COMMIT
</code></pre>
<p><code>SELECT ... FOR UPDATE</code> locks a particular row, and hence no one else can lock
that record.</p>
<p>As soon as a new job comes in, multiple workers will execute the above query and
will try to take a lock on that record. The database will ensure that only one
of the workers gets the lock.</p>
<p>The first worker will take the lock on the record using <code>FOR UPDATE</code>. When other
workers come to that record and they see that there is a lock <code>FOR UPDATE</code>, they
will wait for the lock to be lifted. Yes, these workers will wait until the lock
is released.</p>
<p>The lock will only be released when the transaction is committed. When the
transaction is committed and the lock is released, then other workers will get
hold of the record only to find that the job has already been processed. As you
can see, this is a highly inefficient process.</p>
<p>That is where <code>FOR UPDATE SKIP LOCKED</code> comes in.</p>
<h2>SKIP LOCKED skips locked rows</h2>
<pre><code class="language-sql">START TRANSACTION;
SELECT * FROM jobs_table FOR UPDATE SKIP LOCKED LIMIT 1;
-- Process the job
COMMIT;
</code></pre>
<p>Imagine the same scenario here. A job comes in. Multiple workers compete to
claim the job. The database ensures that only one worker gets the lock. However
in this case the other workers will move on to the next record. They will not
wait. That's what <code>SKIP LOCKED</code> does.</p>
<p>MySQL has detailed documentation on
<a href="https://dev.mysql.com/blog-archive/mysql-8-0-1-using-skip-locked-and-nowait-to-handle-hot-rows/">how SKIP LOCKED works</a>
If you want to read in more detail.</p>
<p>Solid Queue uses <code>FOR UPDATE SKIP LOCKED</code> feature to ensure that a job is
claimed by only one worker.</p>
<h2>How GoodJob manages job processing without SKIP LOCKED</h2>
<p><a href="https://github.com/bensheldon/good_job">GoodJob</a> burst into the scene
<a href="https://island94.org/2020/07/introducing-goodjob-1-0">around July, 2020</a>.
GoodJobs supports only PostgreSQL database because it uses advisory locks to
guarantee that no two workers claim the same job.</p>
<p>PostgreSQL folks understand that the lock mechanism provided by the database
would not satisfy all the variety of cases that might arise in an application.
Advisory locks are a mechanism that allows applications to establish a
communication channel to coordinate actions between different sessions or
transactions. Unlike regular row-level locks enforced by the database system,
advisory locks are implemented as a set of low-level functions that applications
can use to acquire and release locks based on their requirements. We can read
more about it
<a href="https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS">here</a>.</p>
<p><a href="https://www.postgresql.org/docs/9.1/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS">pg_advisory_lock function</a>
will lock the given resource. However, if another session already holds a lock on
the same resourc,e then this function will wait. This is similar to the
<code>FOR UPDATE</code> case we saw above.</p>
<p>However <code>pg_try_advisory_lock function</code> will either obtain the lock immediately
and return true, or return false if the lock cannot be acquired immediately. As
you can see, the name has the word <code>try</code>. This function attempts to acquire a lock.
If it can't get the lock, then it won't wait. Now this function can be utilized
to build a queuing system.</p>
<p>Any usage of an advisory lock means the application needs to coordinate action. It
gives more power to the application but it also means more work by the
application. In contrast, <code>FOR UPDATE SKIP RECORD</code> is natively supported by
PostgreSQL.</p>
<p>Based on the discussions
<a href="https://github.com/bensheldon/good_job/issues/896">here</a> and
<a href="https://github.com/bensheldon/good_job/discussions/831#discussioncomment-6780579">here</a>,
it seems GoodJob is evaluating the possibility of migrating from advisory locks
to using <code>FOR UPDATE SKIP LOCKED</code> for better performance. Going through these
issues was quite revealing and I got to learn a lot about things I was unaware
of.</p>
<h2>Delayed job implementation</h2>
<p><a href="https://github.com/collectiveidea/delayed_job">DelayedJob</a> has been there since
2009, long before Sidekiq. It doesn't use <code>SKIP LOCK</code>. Instead it uses a row
level locking system by
<a href="https://github.com/tobi/delayed_job/blob/719b628bdd54566f80ae3a99c4a02dd39d386c07/lib/delayed/job.rb#L164-L181">updating a field in the job record</a>
to indicate that the job is being processed. In short DelayedJob ensures that no
two workers take the same job at the application level without taking any help
in this direction from the database.</p>
<h2>What about SQLite</h2>
<p>So far, we discussed PostgreSQL and MySQL. What about SQLite? Does it support
<code>SKIP LOCK</code>. No, it doesn't support it, but it's ok. As per the
<a href="https://www.sqlite.org/whentouse.html#dbcklst">documentation</a>, it supports
<code>only writer at any instant in time</code>.</p>
<blockquote>
<p>High Concurrency</p>
<p>SQLite supports an unlimited number of simultaneous readers, but it will only
allow one writer at any instant in time. For many situations, this is not a
problem. Writers queue up. Each application does its database work quickly and
moves on, and no lock lasts for more than a few dozen milliseconds. But there
are some applications that require more concurrency, and those applications
may need to seek a different solution.</p>
</blockquote>
<h2>NOWAIT</h2>
<p>For completeness, let's discuss the <code>NOWAIT</code> feature. We saw earlier that
if we take a lock on a row using <code>FOR UPDATE</code>, then other workers will wait until
the lock is released.</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT * FROM JOBS WHERE processed='no' FOR UPDATE NOWAIT LIMIT 1;
-- Process the job
COMMIT
</code></pre>
<p><code>NOWAIT</code> feature allows other transactions to not wait for the lock to be
released. In this case if a transaction is not able to get a lock on the given
row then it will raise an error and the application needs to handle the error.</p>
<p>In contrast, <code>SKIP LOCKED</code> will allow the transaction to move on to the next row
if a lock is already taken.</p>
<h2>Redis backed queue vs database backed queue</h2>
<p>Now that we looked at how <code>FOR UPDATE SKIP LOCK</code> helps build queuing system
using database itself, let's see some pros and cons of each type of queuing
system.</p>
<h4>Simplicity and familiarity</h4>
<p>Database-backed queues are often simpler to set up and manage, especially if
your application is already using a relational database. There's no need for an
additional dependency like Redis.</p>
<h4>No Additional Infrastructure</h4>
<p>Since the job information is stored in the same database as your application
data, you don't need to set up and maintain a separate infrastructure like a
Redis server.</p>
<h4>Transactionality</h4>
<p>Database-backed queues can leverage database transactions, ensuring that both
the job creation and any related database operations are committed or rolled
back together. This can be important in scenarios where data consistency is
critical.</p>
<h4>Modifiability</h4>
<p>It is easier to modify the jobs stored in the database than Redis, but doing so
requires caution, and it's generally not recommended. In Redis, jobs are often
stored as serialized data, and modifying them directly is not as straightforward
or common. Redis provides commands to interact with data, but modifying job data
directly is not a standard practice and could result in data corruption.</p>
]]></content>
    </entry>
</feed>