<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Average Dev's blog]]></title><description><![CDATA[Average Dev's blog]]></description><link>https://blog.ivda.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 18:57:02 GMT</lastBuildDate><atom:link href="https://blog.ivda.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Mastering Timestamping in Rails]]></title><description><![CDATA[💡
All provided info is actual as of the date of writing: 30.03.2024. So it's relative to Rails 7.1, 7.0, 6.1, and some other earlier versions. But this may change in future updates. Check the docs and the source code!


UPD: 31/03/2024 I've opened a...]]></description><link>https://blog.ivda.dev/mastering-timestamping-in-rails</link><guid isPermaLink="true">https://blog.ivda.dev/mastering-timestamping-in-rails</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Dmitrii Ivliev]]></dc:creator><pubDate>Sat, 30 Mar 2024 14:53:14 GMT</pubDate><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">All provided info is actual as of the date of writing: 30.03.2024. So it's relative to Rails 7.1, 7.0, 6.1, and some other earlier versions. But this may change in future updates. Check the docs and the source code!</div>
</div>

<p>UPD: 31/03/2024 I've <a target="_blank" href="https://github.com/rails/rails/pull/51455">opened a PR</a> to add <code>touch</code> option to <code>#update_columns</code> and <code>#update_column</code> method</p>
<hr />
<p>In modern web development, precise and efficient data management is crucial for making informed business decisions.</p>
<p>Data consistency, particularly in how database records are dated, plays a crucial role. Data engineers often rely on these dates not only for record-keeping but also as a method to download only the data that has changed, avoiding the need to process entire tables.</p>
<p>This guide addresses the challenge of mastering dating amid the inconsistencies found in Rails' handling of timestamps. It explores strategies for ensuring that timestamps are both accurate and dependable, offering valuable insights for developers looking to navigate these complexities. Whether you're new to the field or seeking solutions to specific dating challenges, this article provides practical tips and strategies to improve your data management practices.</p>
<h3 id="heading-tldr">TLDR</h3>
<p>Not all ActiveRecord persistence methods affect timestamps or have a <code>touch</code> option. For methods like <code>update_columns</code> that don't automatically update timestamps, you can create a RuboCop custom cop, modify ActiveRecord directly, or use database triggers to keep <code>updated_at</code> always up-to-date.</p>
<p>Each method has its advantages and disadvantages, from how easy they are to manage to potential compatibility issues with future Rails updates or the risk of SQL operations skipping ActiveRecord methods.</p>
<p>If you're interested in this topic, join the Rails Discussion thread <a target="_blank" href="https://discuss.rubyonrails.org/t/proposal-add-touch-option-for-update-columns-update-column/85388?u=moofkit">https://discuss.rubyonrails.org/t/proposal-add-touch-option-for-update-columns-update-column/85388</a></p>
<h3 id="heading-activerecord-timestamps-configuration">ActiveRecord timestamps configuration</h3>
<p><a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html">ActiveRecord automatically timestamps</a> create and update operations if the table has fields named <code>created_at/created_on</code> or <code>updated_at/updated_on</code>.</p>
<p>For turning off timestamping, add:</p>
<pre><code class="lang-ruby">config.active_record.record_timestamps = <span class="hljs-literal">false</span> <span class="hljs-comment"># true is default</span>
</code></pre>
<p>Timestamps are in UTC by default, but you can use the local timezone by setting:</p>
<pre><code class="lang-ruby">config.active_record.default_timezone = <span class="hljs-symbol">:local</span> <span class="hljs-comment"># is :utc by default</span>
</code></pre>
<p>ActiveRecord keeps all the <code>datetime</code> and <code>time</code> columns timezone aware. By default, these values are stored in the database as UTC and converted back to the current <a target="_blank" href="http://Time.zone"><code>Time.zone</code></a> when pulled from the database.</p>
<p>This feature can be turned off completely by setting:</p>
<pre><code class="lang-ruby">config.active_record.time_zone_aware_attributes = <span class="hljs-literal">false</span> <span class="hljs-comment"># is true by default</span>
</code></pre>
<h3 id="heading-activerecord-persistence-methods-and-touching-timestamps">ActiveRecord persistence methods and touching timestamps</h3>
<p>There are a lot of persistence methods in ActiveRecord, but not all of them touch timestamps or have a <code>touch</code> option. You can find most of them in the table below.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Module#method</td><td>updates timestamps if <code>record_timestamps == true</code></td><td>has <code>touch</code> option</td></tr>
</thead>
<tbody>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-save">save</a></td><td>yes</td><td>yes</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-save-21">save</a>!</td><td>yes</td><td>yes</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create">create</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create-21">create!</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update">update</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update-21">update</a>!</td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute">update_attribute</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch">touch</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment">increment!</a></td><td>no</td><td>yes</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column">update_column</a></td><td>no</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns">update_columns</a></td><td>no</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-toggle-21">toggle!</a></td><td>yes</td><td>no</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert">insert</a></td><td>yes</td><td>yes, via <code>record_timestamps</code> keyword argument</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert-21">insert!</a></td><td>yes</td><td>yes, via <code>record_timestamps</code> keyword</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all">insert_all</a></td><td>yes</td><td>yes, via <code>record_timestamps</code> keyword</td></tr>
<tr>
<td>Persistence#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all">upsert_all</a></td><td>yes</td><td>yes, via <code>record_timestamps</code> keyword</td></tr>
<tr>
<td>Relation#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all">update_all</a></td><td>no</td><td>no</td></tr>
<tr>
<td>Relation#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all">touch_all</a></td><td>yes</td><td>yes, via positional arguments</td></tr>
<tr>
<td>Relation#<a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_counters">update_counters</a></td><td>no</td><td>yes</td></tr>
</tbody>
</table>
</div><p>As you can see, three methods don't update timestamps by default nor provide a touch option: <code>update_column</code>, <code>update_columns</code>, and <code>update_all</code>. Sometimes this may be a problem, i.e., there is some <a target="_blank" href="https://aws.amazon.com/what-is/etl/">ETL</a> processing that, instead of copying the whole table, looks into <code>updated_at</code> timestamps. So if someone uses <code>update_columns</code> because of performance reasons, it may lead to lost updates. However, there are a couple of methods to solve this problem.</p>
<p>Let's consider a pretty basic example.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationController</span> &lt; ActionController::Base</span>
  before_action <span class="hljs-symbol">:update_last_user_ip</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_last_user_ip</span></span>
    ip = request.remote_ip
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> current_user.last_ip != ip
    <span class="hljs-comment"># we don't perform any callbacks or validations here</span>
    <span class="hljs-comment"># so use #update_columns</span>
    current_user.update_columns(
        <span class="hljs-symbol">last_ip:</span> request.remote_ip,
        <span class="hljs-symbol">updated_at:</span> Time.current <span class="hljs-comment"># but we still want to keep track of the last changes, so have to provide timestamp explicitly</span>
    )
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Here, we aim to update the user's last seen IP in their record without triggering any validations or callbacks. The simplest method for this is using the <code>#update_columns</code> method. However, to ensure the timestamp remains current, we must explicitly include <code>updated_at</code>. What issues might arise from this approach?</p>
<p>Several, including:</p>
<ul>
<li><p>Remembering that <code>#update_columns</code> does not update timestamps, a behavior that is <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns">documented</a> but might still catch you off guard.</p>
</li>
<li><p>The need to explicitly set <code>updated_at/updated_on</code>.</p>
</li>
<li><p>The absence of a <code>touch</code> option, unlike what you find in methods like <code>#increment!</code>.</p>
</li>
<li><p>The <code>record_timestamps</code> setting does not affect timestamp behavior.</p>
</li>
</ul>
<p>So, what can we do if we want to consistently update timestamps across the application? There are a few solutions.</p>
<h3 id="heading-rubocop-cop">Rubocop Cop</h3>
<p><a target="_blank" href="https://github.com/rubocop/rubocop">RuboCop</a> lets you make your own custom cops. You need to make a new file for your custom cop, which we'll name <code>UpdateColumnsCop</code>. Put this file in a folder where RuboCop looks for custom cops. A usual spot for this is <code>lib/rubocop/cop/</code>.</p>
<p>Here's a simple setup for your custom cop:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># rubocop/cop/rails/update_columns_timestamps.rb</span>
<span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">RuboCop</span></span>
  <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Cop</span></span>
    <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Rails</span></span>
      <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UpdateColumnsCop</span> &lt; RuboCop::Cop::<span class="hljs-title">Base</span></span>
        extend RuboCop::Cop::AutoCorrector

        MSG = <span class="hljs-string">"Ensure `updated_at` or `updated_on` is updated when using `update_columns`"</span>

        def_node_matcher <span class="hljs-symbol">:update_columns?</span>, <span class="hljs-string">&lt;&lt;-PATTERN
           (send _ {:update_columns} ...)
        PATTERN</span>

        <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_send</span><span class="hljs-params">(node)</span></span>
          <span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> update_columns?(node)

          <span class="hljs-comment"># Check if `updated_at` or `updated_on` is being updated</span>
          updated_at_or_updated_on_updated = node.arguments.any? <span class="hljs-keyword">do</span> <span class="hljs-params">|arg|</span>
            arg.hash_type? &amp;&amp; arg.pairs.any? <span class="hljs-keyword">do</span> <span class="hljs-params">|pair|</span>
              pair.key.value == <span class="hljs-symbol">:updated_at</span> <span class="hljs-params">||</span> pair.key.value == <span class="hljs-symbol">:updated_on</span>
            <span class="hljs-keyword">end</span>
          <span class="hljs-keyword">end</span>

          <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> updated_at_or_updated_on_updated

          add_offense(node, <span class="hljs-symbol">message:</span> MSG) <span class="hljs-keyword">do</span> <span class="hljs-params">|corrector|</span>
            corrector.insert_after(node.loc.selector, <span class="hljs-string">", updated_at: Time.current"</span>)
          <span class="hljs-keyword">end</span>
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>To make RuboCop aware of your custom cop, you need to register it. Create a <code>.rubocop.yml</code> file in your project root if you don't already have one, and add the following configuration:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">require:</span>
 <span class="hljs-bullet">-</span> <span class="hljs-string">./rubocop/cop/rails/update_columns_timestamps.rb</span>

<span class="hljs-attr">Rails/UpdateColumnsCop:</span>
 <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>It provides lint error in case of using <code>update_columns</code> without <code>updated_at</code> or <code>update_on</code> attribute:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">app/controllers/application_controller.rb:10:5: C:</span> [<span class="hljs-string">Correctable</span>] <span class="hljs-attr">Rails/UpdateColumnsCop:</span> <span class="hljs-string">Ensure</span> <span class="hljs-string">updated_at</span> <span class="hljs-string">or</span> <span class="hljs-string">updated_on</span> <span class="hljs-string">is</span> <span class="hljs-string">updated</span> <span class="hljs-string">when</span> <span class="hljs-string">using</span> <span class="hljs-string">update_columns</span>
    <span class="hljs-string">current_user.update_columns(</span> <span class="hljs-string">...</span>
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Identifies violations without changing behavior</p>
</li>
<li><p>Can be modified or ignored like a standard RuboCop cop</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>It can't handle <code>update_column</code> because it doesn't offer an option for timestamps. This requires an additional rule that completely discourages the use of <code>update_column</code> in favor of <code>update_columns</code>.</p>
</li>
<li><p>It doesn't address every situation. For instance, using raw SQL might still bypass updating the <code>updated_at</code> field.</p>
</li>
</ul>
<h3 id="heading-monkey-patch-activerecord-updatecolumn-updatecolumns">Monkey patch ActiveRecord <code>update_column</code>, <code>update_columns</code></h3>
<p>This method, inspired by <a target="_blank" href="https://gist.github.com/timm-oh">Tim McCarthy's gist</a> (with <a target="_blank" href="https://github.com/choncou">Unathi Chonco</a> as an original author), includes a few modifications for safer patching and extra features.</p>
<ul>
<li><p>Add an initializer for patches</p>
</li>
<li><pre><code class="lang-ruby">      <span class="hljs-comment"># config/initializers/core_ext_require.rb</span>
      <span class="hljs-comment"># <span class="hljs-doctag">NOTE:</span> Require all patches in lib/core_ext</span>
      Dir[Rails.root.join(<span class="hljs-string">"lib/core_ext/**/*.rb"</span>)].each { <span class="hljs-params">|f|</span> <span class="hljs-keyword">require</span> f }
</code></pre>
</li>
<li><p>Add a patch for the <code>Persistence</code> module</p>
<pre><code class="lang-ruby">  <span class="hljs-comment"># lib/core_ext/active_record/persistence/update_columns_patch.rb</span>
  <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">CoreExt</span></span>
    <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">ActiveRecord</span></span>
      <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Persistence</span></span>
        <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">UpdateColumnsPatch</span></span>
          <span class="hljs-comment"># https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activerecord/lib/active_record/persistence.rb#L931-L954</span>
          <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_columns</span><span class="hljs-params">(attributes)</span></span>
            touch = attributes.delete(<span class="hljs-symbol">:touch</span>) { <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.record_timestamps }
            <span class="hljs-keyword">if</span> touch
              names = touch <span class="hljs-keyword">if</span> touch != <span class="hljs-literal">true</span>
              names = Array.wrap(names)
              options = names.extract_options!
              touch_updates = <span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.touch_attributes_with_time(*names, **options)
              attributes.merge!(touch_updates) <span class="hljs-keyword">unless</span> touch_updates.empty?
            <span class="hljs-keyword">end</span>
            <span class="hljs-keyword">super</span>(attributes)
          <span class="hljs-keyword">end</span>

          <span class="hljs-comment"># https://github.com/rails/rails/blob/36c1591bcb5e0ee3084759c7f42a706fe5bb7ca7/activerecord/lib/active_record/persistence.rb#L910-L913</span>
          <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_column</span><span class="hljs-params">(name, value, <span class="hljs-symbol">touch:</span> <span class="hljs-literal">true</span>)</span></span>
            update_columns(name =&gt; value, <span class="hljs-symbol">:touch</span> =&gt; touch)
          <span class="hljs-keyword">end</span>
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  ActiveRecord::Persistence.prepend(CoreExt::ActiveRecord::Persistence::UpdateColumnsPatch)
</code></pre>
<p>  This patch mimics the behavior of the <code>#save</code> method: it updates timestamps by default and introduces a <code>touch:</code> option to choose whether to skip the update. It also respects the <code>record_timestamps</code> setting, both globally and at the model level. With this change, we can simplify our example as follows:</p>
<pre><code class="lang-ruby">  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationController</span> &lt; ActionController::Base</span>
    before_action <span class="hljs-symbol">:update_last_user_ip</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_last_user_ip</span></span>
      ip = request.remote_ip
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> current_user.last_ip != ip
      <span class="hljs-comment"># we don't perform any callbacks or validations here</span>
      <span class="hljs-comment"># so use #update_columns</span>
      current_user.update_columns(<span class="hljs-symbol">last_ip:</span> request.remote_ip)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
</code></pre>
<p>  <code>#update_columns</code> does automatically update the <code>updated_at</code> field. However, if you need to avoid updating it for some reason, you can explicitly use the <code>touch</code> option:</p>
<pre><code class="lang-ruby">  current_user.update_columns(<span class="hljs-symbol">last_ip:</span> request.remote_ip, <span class="hljs-symbol">touch:</span> <span class="hljs-literal">false</span>)
</code></pre>
<p>  Also, if attribute names are provided, they are updated together with the <code>updated_at</code>/<code>updated_on</code> attributes, similar to how <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-update_counters"><code>#update_counters</code></a> works.</p>
<pre><code class="lang-ruby">  current_user.update_columns(
    <span class="hljs-symbol">last_ip:</span> request.remote_ip,
    <span class="hljs-symbol">touch:</span> <span class="hljs-symbol">:last_ip_updated_at</span>
  )
</code></pre>
</li>
</ul>
<p><strong>Pros:</strong></p>
<ul>
<li><p>An ad-hoc solution that's easy to manage and adjust.</p>
</li>
<li><p>Behaves similarly to what we're used to with most methods in the <code>Persistence</code> module.</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Involves monkey patching, which might break in future Rails updates.</p>
</li>
<li><p>Doesn't address all scenarios, for example, using raw SQL might still bypass updating the <code>updated_at</code> field.</p>
</li>
</ul>
<p>If you think these changes are worth including in Rails, please join the Rails Discussion and leave a comment: <a target="_blank" href="https://discuss.rubyonrails.org/t/proposal-add-touch-option-for-update-columns-update-column/85388?u=moofkit">https://discuss.rubyonrails.org/t/proposal-add-touch-option-for-update-columns-update-column/85388</a></p>
<h3 id="heading-database-triggers">Database triggers</h3>
<p>If you need to update timestamps on each insert or update, even with raw SQL, you should use <a target="_blank" href="https://en.wikipedia.org/wiki/Database_trigger">database triggers</a>. Database triggers are pieces of procedural code that run in response to specific events in a database. For updating timestamps, this could be an <code>UPDATE</code> SQL statement.</p>
<p>First, we'll create the trigger function. This function is triggered whenever an update operation happens on a table. It will automatically update the <code>updated_at</code> column to the current timestamp.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    NEW.updated_at = <span class="hljs-keyword">NOW</span>();
    RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>This function, <code>update_updated_at_column</code>, is a simple PL/pgSQL function that sets the <code>updated_at</code> column of the new row (<code>NEW</code>) to the current timestamp (<code>NOW()</code>).</p>
<p>Next, you need to create a trigger for each table you want to track updates on. Here's how you can create a trigger for a specific table, let's say <code>your_table_name</code>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> update_your_table_name_trigger
<span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">ON</span> your_table_name
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column();
</code></pre>
<p>This trigger, <code>update_your_table_name_trigger</code>, is set to execute before any update operation on <code>your_table_name</code>. It calls the <code>update_updated_at_column</code> function, which updates the <code>updated_at</code> column.</p>
<p>To handle both insert and update events for setting <code>created_at</code> and <code>updated_at</code> timestamps, you'll need to create two separate triggers for each event type. The first trigger will handle the insert event, setting both <code>created_at</code> and <code>updated_at</code> to the current timestamp. The second trigger will handle the update event, setting only the <code>updated_at</code> column to the current timestamp:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> update_created_updated_at_columns()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> TG_OP = <span class="hljs-string">'INSERT'</span> <span class="hljs-keyword">THEN</span>
        NEW.created_at = <span class="hljs-keyword">NOW</span>();
        NEW.updated_at = NOW();
    ELSIF TG_OP = '<span class="hljs-keyword">UPDATE</span><span class="hljs-string">' THEN
        NEW.updated_at = NOW();
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;</span>
</code></pre>
<p>This function checks the operation type (<code>TG_OP</code>) to determine if it's an insert or update operation. For inserts, it sets both <code>created_at</code> and <code>updated_at</code> to the current timestamp. For updates, it only updates the <code>updated_at</code> column.</p>
<p>Now, create the triggers for both insert and update events:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Trigger for INSERT</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> insert_your_table_name_trigger
<span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ON</span> your_table_name
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> update_created_updated_at_columns();

<span class="hljs-comment">-- Trigger for UPDATE</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> update_your_table_name_trigger
<span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">ON</span> your_table_name
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> update_created_updated_at_columns();
</code></pre>
<p>For better triggers management within Rails it's recommended to use tools like <a target="_blank" href="https://github.com/teoljungberg/fx">fx</a> or <a target="_blank" href="https://github.com/jenseng/hair_trigger">hair_trigger</a>.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Always up-to-date <code>created_at/updated_at</code> timestamps</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Triggers are difficult to manage</p>
</li>
<li><p>You need to add triggers for each new table where you want to keep the timestamps current</p>
</li>
<li><p>Triggers make the app behavior less obvious and, sometimes you might not want to update timestamps, and that removes control from the app</p>
</li>
</ul>
<h3 id="heading-sources">Sources:</h3>
<ul>
<li><p><a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html">https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html</a></p>
</li>
<li><p><a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html">https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html</a></p>
</li>
<li><p><a target="_blank" href="https://gist.github.com/timm-oh/9b702a15f61a5dd20d5814b607dc411d">https://gist.github.com/timm-oh/9b702a15f61a5dd20d5814b607dc411d</a></p>
</li>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Database_trigger">https://en.wikipedia.org/wiki/Database_trigger</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/teoljungberg/fx">https://github.com/teoljungberg/fx</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/rubocop/rubocop">https://github.com/rubocop/rubocop</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Mastering Error Handling in Sidekiq: Techniques and Strategies for Rescuing Jobs]]></title><description><![CDATA[TLDR
If you're looking for a quick solution to handle annoying or expected errors with Sidekiq just try the sidekiq-rescue gem
class MyJob
  include Sidekiq::Job
  include Sidekiq::Rescue::Dsl

  sidekiq_rescue Faraday::ConnectionFailed, delay: 60, l...]]></description><link>https://blog.ivda.dev/mastering-error-handling-in-sidekiq-techniques-and-strategies-for-rescuing-jobs</link><guid isPermaLink="true">https://blog.ivda.dev/mastering-error-handling-in-sidekiq-techniques-and-strategies-for-rescuing-jobs</guid><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Dmitrii Ivliev]]></dc:creator><pubDate>Sat, 03 Feb 2024 16:56:49 GMT</pubDate><content:encoded><![CDATA[<h3 id="heading-tldr">TLDR</h3>
<p>If you're looking for a quick solution to handle annoying or expected errors with Sidekiq just try the <a target="_blank" href="https://github.com/moofkit/sidekiq-rescue">sidekiq-rescue gem</a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job
  <span class="hljs-keyword">include</span> Sidekiq::Rescue::Dsl

  sidekiq_rescue Faraday::ConnectionFailed, <span class="hljs-symbol">delay:</span> <span class="hljs-number">60</span>, <span class="hljs-symbol">limit:</span> <span class="hljs-number">5</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(*)</span></span>
    <span class="hljs-comment"># ...</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>If you are interested in learning some hints and tricks about this topic - welcome to the article!</p>
<h2 id="heading-handling-errors-in-sidekiq">Handling errors in Sidekiq</h2>
<p><a target="_blank" href="https://github.com/sidekiq/sidekiq/">Sidekiq</a> is one of the most popular background processors for Ruby. It's very easy to integrate with Rails or use it as a standalone without any frameworks.</p>
<p>One of the major aspects of software development is error observability and error handling. <a target="_blank" href="https://www.mikeperham.com/2013/08/25/please-use-an-error-service/">It's highly recommended</a> to use one of the error services like <a target="_blank" href="https://rollbar.com">Rollbar</a>, <a target="_blank" href="https://sentry.io/">Sentry</a>, <a target="_blank" href="https://www.honeybadger.io/">Honeybadge</a>r, <a target="_blank" href="https://www.bugsnag.com/">Bugsnag</a>, etc.</p>
<p>Most of these services have integration with Sidekiq and it's very easy to set up and have all your errors in one place. Thus, you and your team will always be able to see bugs that have occurred.</p>
<p>But while you are fixing the next bug, Sidekiq does one important thing for you - keep a fallen job in a special queue and <strong>retry it</strong>.</p>
<p>There is a standard way to retry Sidekiq jobs that have been falling with errors - <a target="_blank" href="https://github.com/sidekiq/sidekiq/wiki/Error-Handling#automatic-job-retry">the automatic job retry mechanism</a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RetryableJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job
  sidekiq_options <span class="hljs-symbol">retry:</span> <span class="hljs-number">25</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>It's a <a target="_blank" href="https://github.com/sidekiq/sidekiq/blob/80f5f73f8e74a5775866a016fe42446dfc1b861e/lib/sidekiq/job_retry.rb#L68-L72">default setting</a>, so specify it only if you want to customize the number of retries</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RetryableJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>It uses a very clever strategy with an exponential backoff using the formula <code>(retry_count ** 4) + 15 + (rand(10) * (retry_count + 1))</code> (i.e. 15, 16, 31, 96, 271, ... seconds + a <a target="_blank" href="https://github.com/sidekiq/sidekiq/issues/480">random amount of time</a>). It will perform 25 retries over approximately 20 days.</p>
<p>It works very well when a job encounters a bug and fails because a developer has plenty of time before the job stops retrying. After that time it will go to the "morgue" and will be there for 6 months, and then will be discarded. It is possible to retry the job from the UI or console after a bug has been fixed.</p>
<p>But in real-world applications, not all the exceptions are bugs. It can be also a network issue, 3rd party services downtime. Or your application is using <a target="_blank" href="https://github.com/leandromoreira/redlock-rb">distributed lock heavily</a> and the resource is busy now and needs to be accessed sometime later.</p>
<p>Sidekiq retry mechanism works fine with all of these cases, but there are some downsides though - the error is still reported to the error service. Such behavior decreases the visibility of real bugs and some errors can be quite annoying.</p>
<p>So there are two types of errors that devs usually deal with:</p>
<ul>
<li><p><strong><em>unexpected errors</em></strong> that we want to see as soon as possible in the error tracker because it's probably a bug</p>
</li>
<li><p><strong><em>expected errors</em></strong> that can occur from time to time and should be reported only when get out of control</p>
</li>
</ul>
<p>As with the first Sidekiq default retry mechanism works perfectly the second one requires some effort to handle.</p>
<h2 id="heading-handle-expecting-errors">Handle expecting errors</h2>
<p>Let's consider an example for a clear picture:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In the given example, <code>HardJob</code> fails with approximately a 1% probability. Let's assume that we don't control how often this error occurs. Naturally, we could ignore such errors, but this might lead to a situation where the error spirals out of control and we remain oblivious to it.</p>
<p>However, several techniques can help!</p>
<h3 id="heading-1-technique-ignore-expected-errors">#1 Technique: Ignore expected errors</h3>
<p>The <a target="_blank" href="https://www.mikeperham.com/2017/09/29/retries-and-exceptions/">first technique</a> proposed by <a target="_blank" href="https://ruby.social/@getajobmike">Mike Perham</a>, the author of Sidekiq, is pretty clever and elegant though. The idea is to patch <code>Exception</code> class with ignore flag:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/exceptions.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Exception</span></span>
  <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:ignore_please</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And fully relies on the Sidekiq retry mechanism:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">rescue</span> AnnoyingError =&gt; e
    <span class="hljs-comment"># flag it to be ignored</span>
    e.ignore_please = <span class="hljs-literal">true</span>
    <span class="hljs-comment"># re-raise it so Sidekiq will retry</span>
    raise e
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And ignore errors with a flag in the error service config:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># https://docs.rollbar.com/docs/ruby#section-ignoring-items</span>
<span class="hljs-comment"># config/initializers/rollbar.rb</span>
handler = proc <span class="hljs-keyword">do</span> <span class="hljs-params">|options|</span>
  raise Rollbar::Ignore <span class="hljs-keyword">if</span> options[<span class="hljs-symbol">:exception</span>].ignore_please
<span class="hljs-keyword">end</span>
Rollbar.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.before_process &lt;&lt; handler
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># https://docs.honeybadger.io/lib/ruby/getting-started/ignoring-errors/#ignore-programmatically</span>
<span class="hljs-comment"># config/initializers/honeybadger.rb</span>
Honeybadger.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.before_notify <span class="hljs-keyword">do</span> <span class="hljs-params">|notice|</span>
    notice.halt! <span class="hljs-keyword">if</span> notice.exception.ignore_please
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Easy to implement</p>
</li>
<li><p>Works with almost all error trackers</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>There are no limits on ignore exceptions. Thus, if an error is raised several times in a row within one job, it will not be reported to the error tracker</p>
</li>
<li><p>Not all trackers can filter errors dynamically, i.e. <a target="_blank" href="https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/#error_collector-ignore_classes">NewRelic</a> can only filter by error class names, and implementing such a filter needs deep knowledge of tracker internals or request a feature from maintainers.</p>
</li>
<li><p>requires custom code for a job - if there are a couple of errors it needs to alter each job <code>#perform</code> method</p>
</li>
</ul>
<p>We can improve it a bit by using <code>sidekiq_retries_exhausted</code> within the job</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  sidekiq_retries_exhausted <span class="hljs-keyword">do</span> <span class="hljs-params">|job, ex|</span>
    ex.ignore_please = <span class="hljs-literal">false</span>
    <span class="hljs-comment"># ErrorTracker is a wrapper around you tracker</span>
    ErrorTracker.notify(ex, <span class="hljs-string">"<span class="hljs-subst">#{job[<span class="hljs-string">'class'</span>]}</span> <span class="hljs-subst">#{job[<span class="hljs-string">"jid"</span>]}</span> just died with error <span class="hljs-subst">#{ex.message}</span>."</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">rescue</span> AnnoyingError =&gt; e
    <span class="hljs-comment"># flag it to be ignored</span>
    e.ignore_please = <span class="hljs-literal">true</span>
    <span class="hljs-comment"># re-raise it so Sidekiq will retry</span>
    raise e
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Or globally with <a target="_blank" href="https://www.rubydoc.info/gems/sidekiq/Sidekiq/Config#death_handlers-instance_method"><code>death_handlers</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-comment"># this goes in your initializer</span>
Sidekiq.configure_server <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.death_handlers &lt;&lt; -&gt;(job, ex) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">if</span> ex.ignore_please <span class="hljs-comment"># checks that some error has been ignored before send it to avoid double notify</span>
      ex.ignore_please = <span class="hljs-literal">false</span>
      ErrorTracker.notify(ex, <span class="hljs-string">"<span class="hljs-subst">#{job[<span class="hljs-string">'class'</span>]}</span> <span class="hljs-subst">#{job[<span class="hljs-string">"jid"</span>]}</span> just died with error <span class="hljs-subst">#{ex.message}</span>."</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>With this improvement, we can see that some jobs have been retried too many times, have gone to the dead queue, and there are probably some bugs worth checking.</p>
<h3 id="heading-2-technique-retry-once-before-erroring">#2 Technique: Retry once before erroring</h3>
<p><a target="_blank" href="https://www.mikecoutermarsh.com/silencing-errors-from-noisy-sidekiq-jobs/">The second</a> technique, by <a target="_blank" href="https://twitter.com/mscccc">Mike Coutermarsh</a>, is more complex, but provides exactly one retry and can be illustrated with this code example</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">rescue</span> AnnoyingError =&gt; e
    <span class="hljs-comment"># here we are retrying the job only once without notifying error tracker</span>
    <span class="hljs-comment"># and then if error will appear again it will be reported</span>
    retry_once_before_raising_error(e)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>You can go through Mike's article to see how it's implemented in detail but I will provide here some ideas to have a clear picture of the final solution.</p>
<p>First of all, it needs to introduce an attribute accessor to the job instance:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job
  <span class="hljs-keyword">attr_writer</span> <span class="hljs-symbol">:retry_count</span>

  <span class="hljs-comment"># Job instances doesn't have direct access to the job payload</span>
  <span class="hljs-comment"># so we have to implement accessor</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retry_count</span></span>
    @retry_count <span class="hljs-params">||</span>= <span class="hljs-number">0</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Then introduce <a target="_blank" href="https://github.com/sidekiq/sidekiq/wiki/Middleware">custom server middleware</a> and add it to the sidekiq config. It needs to be done this way because we have to fetch the retry counter and forward this data to the job instance.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">SidekiqMiddleware</span></span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RetryCount</span></span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span><span class="hljs-params">(job_instance, job_payload, _queue, &amp;block)</span></span>
      <span class="hljs-keyword">if</span> job_instance.respond_to?(<span class="hljs-symbol">:retry_count</span>)
        <span class="hljs-comment"># assign retry count to the job instance to have access there</span>
        job_instance.retry_count = job_payload.fetch(<span class="hljs-string">"retry_count"</span>, <span class="hljs-number">0</span>)
      <span class="hljs-keyword">end</span>

      <span class="hljs-keyword">yield</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># config/initializers/sidekiq.rb</span>
Sidekiq.configure_server <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.server_middleware <span class="hljs-keyword">do</span> <span class="hljs-params">|chain|</span>
    chain.add(SidekiqMiddleware::RetryCount)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Implement a <em>#retry_once_before_raising_error</em> method:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationJob</span></span>
  <span class="hljs-comment"># omit previous code for clarity</span>
  RetryError = Class.new(StandardError)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retry_once_before_raising_error</span><span class="hljs-params">(exception)</span></span>
    <span class="hljs-keyword">if</span> retry_count &lt; <span class="hljs-number">1</span>
      raise RetryError, exception.message
    <span class="hljs-keyword">else</span>
      raise exception
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And add <code>RetryError</code> to the tracker's ignore list:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># https://docs.rollbar.com/docs/ruby#section-exception-level-filters</span>
<span class="hljs-comment"># config/initializers/rollbar.rb</span>
Rollbar.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.exception_level_filters.merge!({
    <span class="hljs-string">'ApplicationJob::RetryError'</span> =&gt; <span class="hljs-string">'ignore'</span>,
  })
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># https://docs.honeybadger.io/lib/ruby/getting-started/ignoring-errors/#ignore-by-class</span>
<span class="hljs-comment"># config/honeybadger.yml</span>
 <span class="hljs-symbol">exceptions:</span>
  <span class="hljs-symbol">ignore:</span>
    - <span class="hljs-string">'ApplicationJob::RetryError'</span>
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>provides an ability to retry once some flacky error and report it if it occurs next time. When the situation goes out of control the error will occur in the error tracker and the developer will see it</p>
</li>
<li><p>easier to reuse by adding only one <code>ApplicationJob::RetryError</code> the exception to the ignore list of tracker</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>more complex to implement due to custom server middleware and altered job class</p>
</li>
<li><p>it retries only once. In some cases, it needs to retry one or two times</p>
</li>
<li><p>needs to alter <code>#perform</code> method of each job with such an error</p>
</li>
</ul>
<p>Let's alter the last technique to have an option to retry several times</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">rescue</span> AnnoyingError =&gt; e
    <span class="hljs-comment"># here we are retrying the job limit times without notifying error tracker</span>
    retry_before_raising_error(e, <span class="hljs-symbol">limit:</span> <span class="hljs-number">3</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The only thing we need to change is the <code>#retry_before_raising_error</code> method</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationJob</span></span>
  <span class="hljs-comment"># omit previous code for clarity</span>
  RetryError = Class.new(StandardError)

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retry_before_raising_error</span><span class="hljs-params">(exception, <span class="hljs-symbol">limit:</span> <span class="hljs-number">1</span>)</span></span>
    <span class="hljs-keyword">if</span> retry_count &lt; limit
      raise RetryError, exception.message
    <span class="hljs-keyword">else</span>
      raise exception
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>all pros from technique #2</p>
</li>
<li><p>can retry as many times as it needs to</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>more complex to implement due to custom server middleware and altered job class</p>
</li>
<li><p>needs to alter <code>#perform</code> method of each job with such an error</p>
</li>
</ul>
<p>Generally, it's good enough for most of the cases, but what if we need some customized delay for this specific error? Then we can use <code>sidekiq_retry_in</code> block!</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  sidekiq_retry_in <span class="hljs-keyword">do</span> <span class="hljs-params">|count, exception, _jobhash|</span>
    <span class="hljs-keyword">case</span> exception
    <span class="hljs-keyword">when</span> AnnoyingError
      <span class="hljs-number">10</span> * (count + <span class="hljs-number">1</span>) <span class="hljs-comment"># (10, 20, 30, 40, 50)</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">10</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">rescue</span> AnnoyingError =&gt; e
    <span class="hljs-comment"># here we are retrying the job limit times without notifying error tracker</span>
    retry_before_raising_error(e, <span class="hljs-symbol">limit:</span> <span class="hljs-number">3</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>all pros from technique #2</p>
</li>
<li><p>can retry as many times as it needs to</p>
</li>
<li><p>can have a custom delay</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>the final solution is quite complex - a lot of moving parts</li>
</ul>
<h3 id="heading-4-technique-use-the-sidekiq-rescuehttpsgithubcommoofkitsidekiq-rescue-gem">#4 Technique: Use the <a target="_blank" href="https://github.com/moofkit/sidekiq-rescue">sidekiq-rescue</a> gem</h3>
<p>The easiest, in my opinion, way to solve the problem is to use <a target="_blank" href="https://github.com/moofkit/sidekiq-rescue">sidekiq-rescue</a> gem. It's a tiny plugin, with zero dependency (besides Sidekiq itself!) that provides handy DSL and is very easy to set up.</p>
<p>Install the gem to your project</p>
<pre><code class="lang-bash">bundle add sidekiq-rescue &amp;&amp; bundle install
</code></pre>
<p>Add the middleware to your Sidekiq configuration:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/sidekiq.rb</span>
Sidekiq.configure_server <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.server_middleware <span class="hljs-keyword">do</span> <span class="hljs-params">|chain|</span>
    chain.add Sidekiq::Rescue::ServerMiddleware
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Let's consider our example:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>All we need it's to include <code>Sidekiq::Rescue::DSL</code> module and use <code>sidekiq_rescue</code></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job
  <span class="hljs-keyword">include</span> Sidekiq::Rescue::Dsl

  sidekiq_rescue AnnoyingError

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And that's all! You can configure the number of retries and the delay (in seconds) between retries:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HardJob</span></span>
  <span class="hljs-keyword">include</span> Sidekiq::Job
  <span class="hljs-keyword">include</span> Sidekiq::Rescue::Dsl

  sidekiq_rescue AnnoyingError, <span class="hljs-symbol">delay:</span> <span class="hljs-number">60</span>, <span class="hljs-symbol">limit:</span> <span class="hljs-number">5</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">perform</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># do some important stuff</span>
    raise AnnoyingError <span class="hljs-keyword">if</span> rand(<span class="hljs-number">99</span>) == <span class="hljs-number">0</span> <span class="hljs-comment"># simulate error that occurs time to time</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The <code>delay</code> is not the exact time between retries; it's a minimum delay. The actual delay is calculated based on the retries counter and <code>delay</code> value. The formula is <code>delay + retries * rand(10)</code> seconds. Randomization is used to avoid retry storms.<br />The default values are:</p>
<ul>
<li><p><code>delay</code>: 60 seconds</p>
</li>
<li><p><code>limit</code>: 5 retries</p>
</li>
</ul>
<p>Delay and limit can be configured globally:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/sidekiq.rb</span>
Sidekiq::Rescue.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.delay = <span class="hljs-number">65</span>
  config.limit = <span class="hljs-number">10</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>You can also configure a job to have the delay to be a proc:</p>
<pre><code class="lang-ruby">sidekiq_rescue ExpectedError, <span class="hljs-symbol">delay:</span> -&gt;(counter) { counter * <span class="hljs-number">60</span> }
</code></pre>
<p>Under the hood, this gem uses <a target="_blank" href="https://github.com/moofkit/sidekiq-rescue/blob/6353c0c16eaabebed26989a953c4108f813e7a60/lib/sidekiq/rescue/server_middleware.rb#L39">Sidekiq's perform_at</a> and doesn't rely on the standard retry mechanism. Thus you have independent retry strategies for both types of errors: <strong>unexpected</strong> and <strong>expected.</strong></p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>easy to use</p>
</li>
<li><p>independent retry strategy from the default Sidekiq error handling</p>
</li>
<li><p>can retry as many times as it needs to</p>
</li>
<li><p>can have a custom delay, even with proc</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>one more gem in a Gemfile</li>
</ul>
<p>This gem is still under active development, but it has good coverage and has been tested in production.</p>
<p>As an author, I will be very grateful for any response about issues and PR's</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, error handling in Sidekiq is a critical aspect of software development. While the default retry mechanism works well for unexpected errors, handling expected errors requires additional strategies.</p>
<p>Techniques such as ignoring errors, retrying once before erroring, or using the <a target="_blank" href="https://github.com/moofkit/sidekiq-rescue">sidekiq-rescue gem</a> can be employed to handle these errors more effectively. Each method has its pros and cons, and the choice depends on the specific needs of your application.</p>
<p>By mastering these techniques, developers can significantly improve error observability and handling in their Sidekiq jobs, leading to more robust and reliable applications.</p>
<h2 id="heading-sources">Sources:</h2>
<ul>
<li><p><a target="_blank" href="https://www.mikeperham.com/2013/08/25/please-use-an-error-service/">https://www.mikeperham.com/2013/08/25/please-use-an-error-service/</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/sidekiq/sidekiq/wiki/Error-Handling">https://github.com/sidekiq/sidekiq/wiki/Error-Handling</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/leandromoreira/redlock-rb">https://github.com/leandromoreira/redlock-rb</a></p>
</li>
<li><p><a target="_blank" href="https://www.mikeperham.com/2017/09/29/retries-and-exceptions/">https://www.mikeperham.com/2017/09/29/retries-and-exceptions/</a></p>
</li>
<li><p><a target="_blank" href="https://www.mikecoutermarsh.com/silencing-errors-from-noisy-sidekiq-jobs/">https://www.mikecoutermarsh.com/silencing-errors-from-noisy-sidekiq-jobs/</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/moofkit/sidekiq-rescue">https://github.com/moofkit/sidekiq-rescue</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>