<?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[Wonolo Engineering]]></title><description><![CDATA[Wonolo Engineering]]></description><link>https://engineeringblog.wonolo.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1659667898966/ZYMC_YNKd.png</url><title>Wonolo Engineering</title><link>https://engineeringblog.wonolo.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 19:43:42 GMT</lastBuildDate><atom:link href="https://engineeringblog.wonolo.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Importance of Automation in IT Operations]]></title><description><![CDATA[Introduction:
Running an IT team in your organization and without automation part of your operations? Learn why you should consider automating your team’s operational tasks as much as you can.
Automating your organization tasks can offer a number of ...]]></description><link>https://engineeringblog.wonolo.com/importance-of-automation-in-it-operations</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/importance-of-automation-in-it-operations</guid><category><![CDATA[IT]]></category><category><![CDATA[No Code]]></category><category><![CDATA[automation]]></category><category><![CDATA[okta]]></category><dc:creator><![CDATA[Gurarsh Singh]]></dc:creator><pubDate>Mon, 16 Oct 2023 15:11:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697218432134/c69aaab6-a52b-4d32-ade6-00272beaa942.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction"><strong>Introduction:</strong></h3>
<p>Running an IT team in your organization and without automation part of your operations? Learn why you should consider automating your team’s operational tasks as much as you can.</p>
<p>Automating your organization tasks can offer a number of benefits, such as reducing manual work, reducing human errors, increasing task completion speed, and even saving money on operational costs.</p>
<p>Automation in IT is often portrayed as requiring coding, but there are now many tools that require little or no coding knowledge to set up automations, including Okta Workflows and Zapier, which are two of the most popular ones and I personally love.</p>
<p>Furthermore, if you are familiar with coding, you can use free tools like Google AppScript.</p>
<p><strong><em>A few examples, how I have automate tasks at my Wonolo:</em></strong></p>
<h3 id="heading-okta-workflows"><strong>Okta Workflows:</strong></h3>
<p>Okta Workflows is one of my favorite no code automation tool. One of the best things about Okta Workflows is the number of workflows and integrations it offers for the most common SaaS tools you use in your organization right out of the box.</p>
<p><strong><em>Below are some examples of how Okta Workflows are used at my current company:</em></strong></p>
<ol>
<li><strong>Automating SaaS provisioning for new hires:</strong></li>
</ol>
<p>Your organization may already have SSO and SCIM solutions in place, however not all SaaS applications support SSO and SCIM provisioning on their beginning billing tiers, so we are unable to make full use of the SSO and SCIM solutions. However, if there is an API solution for provisioning users, you can always automate manual tasks using APIs.</p>
<p>Using Okta Workflows, you can make API calls with different scenarios:</p>
<p>As an example, automatically calling an API endpoint upon Okta account activation to provision an account in the required SaaS service:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*VqCkmzZlx6Sk2n19aNaDpA.png" alt /></p>
<p><em>Flow Chart: Atlassian User Provisioning with Okta Workflow</em></p>
<p>As can be seen in the diagram above, the entire process of provisioning the required user account in Atlassian Jira/Confluence is automated. When the new hire account is activated, it triggers the workflow that automatically creates the user account with the necessary permissions.</p>
<p><strong>2. Zoom Cloud Recordings transfer to User Google Drive:</strong></p>
<p>Automation can also help you with your data cleanups, for example, as Zoom Cloud Storage is expensive and limited, we require our users at organization to save Zoom Cloud Recordings to G Drive, but from the user experience, it can be very non-friendly to manually download all Zoom recordings one by one and upload to their G Drives, since Zoom does not currently allow users or admins to bulk download recordings.</p>
<p>In order to solve the above mentioned problem, I developed a workflow that reads users’ emails from a Google Sheet and copies users’ Zoom Cloud recordings to their Google Drive.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*cuVDzGbbnX4WhhTDWg5eIg.png" alt /></p>
<p><em>Main Workflow (Zoom Videos tranfer to GDrive)</em></p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*6A6NGwQSXIfpk2qATisvGw.png" alt /></p>
<p><em>Helper Flow for the Main flow to parse the JSON result and find the URL to download Zoom Cloud Recording</em></p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*sjqUpFaqL_CBOELCpEC16w.png" alt /></p>
<p><em>Last Flow to download the recording from the dowload URL and transfer to the GDrive</em></p>
<h3 id="heading-zapier"><strong>Zapier:</strong></h3>
<p>The Zapier is another amazing tool that can be used to automate without writing code. Zapier also boasts a wide range of integrations, similar to Okta Workflows.</p>
<p><strong>Slack Alerts</strong></p>
<p><strong>a.</strong> In a few ways, I utilize Zapier is to set up various Slack alerts that trigger when the RSS feed is updated. Zapier will then send a POST webhook to the custom Slack App, which will provide us with the information we needed in our IT Slack channel. It has helped our IT team to keep up-to-date on service degradations and product updates.</p>
<p><strong>b.</strong> Another way I use Zapier to assist our Sales team is by setting up Salesforce Alerts in Slack. Through Zapier and the Salesforce integrations available, I have created alerts for the Sales team in Slack Channel that notify them about new/updated, expansion, etc opportunities with the required details.</p>
<h3 id="heading-google-appscript"><strong>Google AppScript:</strong></h3>
<p>Last but not the least, One of my favorite free tools for automating tasks and requiring coding skills is Google AppScript, a great tool in particular if your organization uses Google Workspaces.</p>
<p>One of the features of the Google AppScript is that it integrates really well with the Google Workspace applications.</p>
<p>At Wonolo, we use Google Forms for various tasks and processes, so we need to know when the Google Form has been filled out by the user.</p>
<p>In order to configure Slack alerts for when the forms response is submitted by the user and to sort the data automatically in the forms response Google Sheet based on the date and time submitted, I wrote code in Google AppScript that first sorts the data in the Google Sheet and sends a POST API call to the custom Slack App to send the alert in the Slack channel containings the required data</p>
<p><a target="_blank" href="https://github.com/igurarsh/slackAlertsForGoogleSheets/blob/main/SlackAlerts_for_GoogleSheets.js"><em>GitHub code link for the above mentioned example</em></a><em>.</em></p>
<h3 id="heading-conclusion"><strong>Conclusion:</strong></h3>
<p>To conclude, automations are really crucial for your business operations and team. If you and your team are currently relying on manual tasks to run your daily IT tasks, you should seriously consider setting up automations to bring the automation culture in to your organization.</p>
]]></content:encoded></item><item><title><![CDATA[Migrate from attr_encrypted to ActiveRecordEncryption]]></title><description><![CDATA[Rails 7 introduced in-built encryption called Active Record Encryption and attr_encrypted has reached its expiry. And now many projects are on the verge of moving from attr_encrypted to Active Record Encryption. This blog will explain how this migrat...]]></description><link>https://engineeringblog.wonolo.com/migrate-from-attrencrypted-to-activerecordencryption</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/migrate-from-attrencrypted-to-activerecordencryption</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[encryption]]></category><category><![CDATA[activerecord]]></category><dc:creator><![CDATA[Shabini Rajadas]]></dc:creator><pubDate>Thu, 28 Sep 2023 21:23:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695922855821/00c5db54-c60a-4d61-bd0e-6ad9497f1047.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Rails 7 introduced in-built encryption called Active Record Encryption and attr_encrypted has reached its expiry. And now many projects are on the verge of moving from attr_encrypted to Active Record Encryption. This blog will explain how this migration can be made possible with very few steps.</p>
<p><strong>Data in attr_encrypted and Active Record Encryption:</strong></p>
<p>attr_encrypted uses two columns in the DB to save the data, the <code>encrypted_attribute</code> and <code>encrypted_attribute_iv</code>. Active Record Encryption uses a single column same as the name of the attribute and saves the data as a hash. For example, to encrypt <code>address</code> in the <code>User</code> model, with attr_encrypted, there are two columns <code>encrypted_address</code> and <code>encrypted_address_iv</code> in DB. With active record encryption, there is a single column in DB, <code>address</code> where the data is saved as a hash as below</p>
<pre><code class="lang-json">{<span class="hljs-attr">"p"</span>:<span class="hljs-string">"Hashed Value"</span>,<span class="hljs-attr">"h"</span>:{<span class="hljs-attr">"iv"</span>:<span class="hljs-string">"Hashed Value"</span>,<span class="hljs-attr">"at"</span>:<span class="hljs-string">"Hashed Value"</span>}}
</code></pre>
<p>As an example, the article will migrate the <code>address</code> attribute from the <code>User</code> model. The migration includes the following steps,</p>
<ol>
<li><p>Add Active Record Encryption to the Rails 7 app.</p>
</li>
<li><p>Add <code>address</code> and <code>address_tmp</code> columns to DB</p>
</li>
<li><p>Create migration to fill data into columns</p>
</li>
<li><p>Remove the attr_encrypted gem and drop columns</p>
</li>
</ol>
<p><strong>Step 1: Add Active Record Encryption to the Rails 7 app</strong></p>
<p>The new encryption from Rails 7 uses 3 keys, <code>primary_key</code>, <code>deterministic_key</code>, and <code>key_derivation_salt</code>. Generate them using the following command</p>
<pre><code class="lang-ruby">bin/rails <span class="hljs-symbol">db:</span><span class="hljs-symbol">encryption:</span>init
</code></pre>
<p>Now save them in your <code>application.rb</code></p>
<pre><code class="lang-ruby">config.active_record.encryption.primary_key = <span class="hljs-string">"Primary Key"</span>
config.active_record.encryption.deterministic_key = <span class="hljs-string">"Deterministic Key"</span>
config.active_record.encryption.key_derivation_salt = <span class="hljs-string">"Key Derivation Salt"</span>
</code></pre>
<p><strong>Step 2: Add</strong> <code>address</code> <strong>and</strong> <code>address_tmp</code> <strong>columns to DB</strong></p>
<p>As mentioned in the introduction, attr_encrypted uses different columns in DB and uses methods to return the value of the <code>address</code> attribute. So the next step is to introduce the column with the same name as the attribute in the DB. But before that, ignore the column in the model.</p>
<pre><code class="lang-ruby">Class User &lt; ApplicationRecord
  <span class="hljs-keyword">self</span>.ignored_columns = <span class="hljs-string">%w[address]</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Now create a migration to add the column in the User Table. The data can’t be moved directly to the <code>address</code> column since it's still used by attr_encrypted, so create two columns in DB. <code>address</code> and <code>address_tmp</code>. <code>address_tmp</code> will be the temporary column to save the encrypted value. The data is first saved in <code>address_tmp</code> and then the <code>address</code> column is updated using the <code>update_all</code> command. The <code>update_all</code> command does not instantiate the <code>User</code> model or trigger Active Record callbacks and validations, which works perfectly in the current scenario.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddAddressAndTmpColumnToUser</span> &lt; ActiveRecord::Migration[7.0]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    add_column <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">:address</span>, <span class="hljs-symbol">:text</span>
    add_column <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">:address_tmp</span>, <span class="hljs-symbol">:text</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Now add the encrypts command on the <code>User</code> model from Active Record Encryption which does all the magic. The encrypts point to the <code>address_tmp</code> and not <code>address</code>, because the attr_encrypted gem still uses the <code>address</code> method.</p>
<pre><code class="lang-ruby">encrypts <span class="hljs-symbol">:address_tmp</span>
</code></pre>
<p><strong>Step 3: Create migration to fill data into columns</strong></p>
<p>Create a migration and fill in values into the <code>address_tmp</code> column and then to the <code>address</code> column using <code>update_all</code>. The code that goes into the migration,</p>
<pre><code class="lang-ruby">User.find_in_batches <span class="hljs-keyword">do</span> <span class="hljs-params">|users_batch|</span>
  users_batch.map <span class="hljs-keyword">do</span> <span class="hljs-params">|user|</span>
    user.address_tmp = user.address
    user.save
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
User.update_all(<span class="hljs-string">"address=address_tmp"</span>)
</code></pre>
<p>Also, add a temporary code in the <code>User</code> model which will save the value in the <code>address</code> and <code>address_tmp</code> columns during updates made by clients using your application.</p>
<pre><code class="lang-ruby">after_commit <span class="hljs-symbol">:write_address</span>, <span class="hljs-symbol">if:</span> -&gt; { previous_changes[<span class="hljs-symbol">:address</span>] }

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_address</span></span>
  <span class="hljs-keyword">self</span>.address_tmp = address
  save!
  User.where(<span class="hljs-symbol">id:</span> id).update_all(<span class="hljs-string">"address=address_tmp"</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<p>Now the data is migrated to the <code>address</code> and <code>address_tmp</code> columns. Any updates made by clients are saved as well in the new columns.</p>
<p><strong>Step 4: Remove the attr_encrypted gem and drop columns</strong></p>
<p>Remove the attr_encrypted reference from the code before removing the gem completely. The existing code might look something like the one below</p>
<pre><code class="lang-ruby">attr_encrypted <span class="hljs-symbol">:address</span>, <span class="hljs-symbol">key:</span> <span class="hljs-string">"key"</span>
</code></pre>
<p>While removing the above code, also point to the new column <code>address</code> with the encrypted data. In the User model add the following code,</p>
<pre><code class="lang-ruby">encrypts <span class="hljs-symbol">:address</span>
</code></pre>
<p>Remove the references that were temporarily created in the <code>User</code> model,</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># to be removed from User model</span>
<span class="hljs-keyword">self</span>.ignored_columns = <span class="hljs-string">%w[address]</span>
after_commit <span class="hljs-symbol">:write_address</span>, <span class="hljs-symbol">if:</span> -&gt; { previous_changes[<span class="hljs-symbol">:address</span>] }
encrypts <span class="hljs-symbol">:address_tmp</span>
</code></pre>
<p>Also if you are using <code>encrypted_address</code> or <code>encrypted_address_iv</code> directly in your code anywhere, make sure to replace them with the new values from the <code>address</code> attribute. Once all the references are removed, it's safe to remove the gem attr_encrypted from the code.</p>
<p>The final step is to remove the following columns <code>encrypted_address</code>, <code>encrypted_address_iv</code><em>,</em> and <code>address_tmp</code> from <code>User</code> model.</p>
<p>To know more details on Active Record Encryption, here is the official link <a target="_blank" href="https://guides.rubyonrails.org/active_record_encryption.html">https://guides.rubyonrails.org/active_record_encryption.html</a></p>
]]></content:encoded></item><item><title><![CDATA[Common Pitfalls While Using Sidekiq]]></title><description><![CDATA[Sidekiq is one of the most commonly used libraries with Ruby on Rails, allowing for code to be executed independently from requests made to an application. It handles a lot of the complexities of managing defined background jobs, with built in logic ...]]></description><link>https://engineeringblog.wonolo.com/common-pitfalls-while-using-sidekiq</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/common-pitfalls-while-using-sidekiq</guid><category><![CDATA[sidekiqpro]]></category><category><![CDATA[sidekiq]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[enterprise]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Evan Brearley]]></dc:creator><pubDate>Tue, 01 Nov 2022 19:33:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/VO5w2Ida70s/upload/v1667331231657/OKDt8WGkJ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sidekiq is one of the most commonly used libraries with Ruby on Rails, allowing for code to be executed independently from requests made to an application. It handles a lot of the complexities of managing defined background jobs, with built in logic for scheduling provided tasks and retrying them, but it doesn’t handle every possible issue for the user. Most of these are documented
<a target="_blank" href="https://github.com/mperham/sidekiq/wiki/">in the wiki on the Sidekiq Github repository</a>.
The <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Best-Practices">best practices page there</a> makes a number of recommendations, namely using simple parameters for new jobs, as well as making the content of these jobs idempotent &amp; transactional to prevent issues arising from race conditions and retries.</p>
<h1 id="heading-keyword-arguments">Keyword Arguments</h1>
<p>Sidekiq allows users to define the signature for the <code>perform</code> method that Sidekiq will call through its <code>perform_async</code>, <code>perform_in</code> &amp; <code>perform_at</code> methods, however <a target="_blank" href="https://github.com/mperham/sidekiq/issues/2372">these methods do not support keyword &amp; named parameters, symbols &amp; some complex objects</a>. This is because the provided parameters are converted to JSON when the job is pushed to Redis to be run later.</p>
<p>This issue can be missed if testing of a new Sidekiq job is done only against the <code>perform</code> method, and not the asynchronous methods. Sidekiq provides methods for testing new jobs <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Testing">here</a>, along with examples on how to use them. In particular, this example:</p>
<pre><code>HardWorker.perform_async(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)
HardWorker.perform_async(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)
assert_equal <span class="hljs-number">2</span>, HardWorker.jobs.size
HardWorker.drain
assert_equal <span class="hljs-number">0</span>, HardWorker.jobs.size
</code></pre><p>is sufficient to test that HardWorker can be successfully called without issues arising from Sidekiq failing to properly pass the arguments from <code>perform_async</code> (or its equivalents) to the user defined <code>perform</code> method.</p>
<p>Errors from invalid parameters are exacerbated by Sidekiq’s retry mechanisms, which will repeat invalid calls until the retry limit is reached or the affected code is fixed by a new deployment.</p>
<h1 id="heading-retries">Retries</h1>
<p>By default, Sidekiq will retry a job that fails 25 times, with increasing delays. This protects jobs from being lost when they fail due to errors that are resolved before the retry limit is hit. Retries come with a number of tradeoffs, especially if the job is not idempotent.</p>
<p>Ideally, each job would be idempotent and transactional, avoiding cases where only part of the job is completed before failing and cases where the retries lead to duplicate outcomes. In practice some tasks, such as network requests to external services require extra care to avoid complications arising from Sidekiq’s default retry strategy.</p>
<p>Network requests are generally not transactional, as they are reliant on external services which might fail part way through their changes, and requests will eventually timeout. In the event of a read timeout where the external service does not respond quickly enough, it is uncertain if the request succeeded on the other end.</p>
<p>By default, Sidekiq will catch exceptions caused by these timeouts, and retry the job, potentially leading to a duplicate call. Polling the status of the external service before making a call each time can prevent this issue, but will be costly. Additionally, catching the error and managing any retries yourself can prevent Sidekiq's default retries. Some APIs offer idempotency guards, such as <a target="_blank" href="https://stripe.com/docs/api/idempotent_requests">Stripe’s idempotency keys</a>, which allow Stripe to identify duplicate requests and ignore them. If all else fails, setting the retry count for a job to 0 may seem like an option, but there are other cases where Sidekiq will retry jobs, mainly when Sidekiq is shut down, intentionally or due to a crash.</p>
<h2 id="heading-deployment-retries">Deployment Retries</h2>
<p>During a deployment, an old Sidekiq process needs to be shutdown so it can be replaced by a new one. As a result, any jobs still active on the old process will stop running when it is terminated, possibly killing them before they are done their work.</p>
<p>Sidekiq outlines its standard (unpaid) solution to this issue <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Deployment">here</a>. Sidekiq will push jobs to Redis to be retried later, provided they do not finish between Sidekiq being notified about an incoming shutdown and the actual shutdown. If Sidekiq is shutdown while it is pushing jobs back to Redis, theunexpected shutdown could trigger <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Reliability">super_fetch</a> retries after jobs have already been pushed to Redis for deployment retries. This can lead to duplicate jobs, and is another reason to assume that any job can be retried or duplicated and add guards against that in their code.</p>
<p>The Enterprise version of Sidekiq comes with the option to do <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Ent-Rolling-Restarts">rolling restarts</a>, where the old process runs until it clears its current jobs while taking no new ones on. New jobs would go to the new process, and this would prevent deployment retries, at the cost of needing to avoid large database changes while rolling restarts are active, and delays in updating code on long running processes.</p>
<h2 id="heading-superfetch">super_fetch</h2>
<p>Paid versions of Sidekiq come with a feature called <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Reliability">super_fetch</a>. When a job fails due to issues with Sidekiq itself (such as out of memory errors), Sidekiq might permanently lose the job. This can also occur if the Sidekiq process is killed before restoring all of its jobs to the queue, typically during deployments. These issues can be mitigated by using multiple queues, spreading the risk of a crash, however this involves additional resources and Sidekiq’s documentation recommends against using too many queues.</p>
<p>With <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Reliability">super_fetch</a>, when Sidekiq recovers from a crash or unexpected shutdown it will retry the affected jobs. On the current version of Sidekiq Pro, super_fetch will recover a job this way 3 times. On an older version of Sidekiq Pro (<a target="_blank" href="https://github.com/mperham/sidekiq/issues/4633">before 5.2</a>), it will have no limits on how many times a job is retried. </p>
<p>These retries come with the same tradeoffs as normal ones, with one additional tradeoff. If a specific job is the cause of Sidekiq crashing, then retrying it can cause all the jobs currently being run by Sidekiq to crash and be retried themselves. This can happen in a loop until the retry limit is hit on the problematic job or the job is manually cancelled (this is the only option on versions without the retry limit).</p>
<p>Unlike normal retries, these must be cancelled manually, by redeploying a version of the code that will not crash, or, as Sidekiq recommends, having the job check Redis for a flag indicating that the job has been cancelled before running. <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/FAQ#how-do-i-cancel-a-sidekiq-job">In the wiki’s FAQ</a>, Sidekiq provides code that can be added to a job that implements this check, and allows a job to be cancelled from a console on the Sidekiq process using a job’s id. </p>
<p>Sidekiq only performs super_fetch retries if it shuts down unexpectedly but by default these are logged at the same level as normal job start and end logs. As a result, these can be missed, either buried within less important logs or silenced if Sidekiq’s logging level is set high enough. As of Sidekiq Pro 5.2, Sidekiq will fire a callback (<a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Reliability#notification">details here</a>) on a super_fetch retry, or a job being killed after reaching its super_fetch retry limit. These can be used to log, alert, and track super_fetch retries as needed.</p>
<h1 id="heading-race-conditions">Race Conditions</h1>
<p>Sidekiq jobs run concurrently with the rest of the app they are part of and with other instances of the same job. These asynchronous tasks must account for the presence of others in order to avoid race conditions, between jobs doing similar actions, multiple instances of the same job and between the jobs and the rest of the application outside of Sidekiq.</p>
<p>Problematic race conditions often follow <a target="_blank" href="https://en.wikipedia.org/wiki/Murphy%27s_law">Murphy’s Law</a>; if one can happen, it does, especially since Sidekiq’s retries and job scheduling can lead to jobs running at unexpected times. Sidekiq does not guarantee that jobs will be processed sequentially if it is configured to run multiple jobs at the same time. With some concurrency jobs created back to back can be run at the same time, in a non sequential manner. Each job’s code should account for the possibility of duplicate jobs on top of other parts of the application affecting the same database entries and external services.</p>
<h2 id="heading-job-scheduling">Job Scheduling</h2>
<p>Sidekiq allows jobs to be scheduled at specific times (with a few seconds delay based on how often Sidekiq will check Redis for scheduled jobs &amp; retries, which is outlined <a target="_blank" href="https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs">here</a>). These scheduled jobs are not guaranteed to be run immediately once polled, but instead enter the queue at the bottom. If there is a backlog of jobs, the scheduled jobs will be delayed until the jobs in front of them are cleared. </p>
<p>This can cause jobs to run at unexpected times, and can cause race conditions for periodic jobs scheduled close enough together.</p>
<p> For example, if a Sidekiq job is scheduled to run every 10 minutes, and the queue is backed up enough to delay them by 10 minutes, then multiple instances of the job can exist in the queue at the same time. When the issue is resolved and the queue clears, these jobs can be run back to back, or concurrently depending on how many processes are running on a Sidekiq queue. If a job does not account for this possibility, this can lead to unexpected or duplicate results.</p>
<h2 id="heading-locking">Locking</h2>
<p>Locking is a common solution for most potential race conditions. Only one active thread in an application can hold a properly implemented lock (such as <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveRecord/Locking.html">the database row level locking strategies provided by Rails for ActiveRecord objects</a>), removing any potential for race conditions. A common issue with locks is with race conditions occurring between checking the state of an object and updating it, often called a <a target="_blank" href="https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use">Time of Check vs Time of Use (TOCTOU) race condition</a>.</p>
<p>If a developer is unaware of the need to lock before reading important data, it can lead to code like:</p>
<pre><code>def unsafe_change_state_from_not_started_to_started
  <span class="hljs-keyword">if</span> state == <span class="hljs-string">'not_started'</span>
    self.with_lock <span class="hljs-keyword">do</span>
      update(state: <span class="hljs-string">'started'</span>)
    end
  end
end
</code></pre><p>If another, nearly identical method called unsafe_change_state_from_not_started_to_cancelled exists, a race condition can happen if one method is still holding the lock while the other loads the object. This can lead to the object’s state being unintentionally changed to <code>started</code> after it was changed to <code>cancelled</code>, or vice versa.</p>
<p>Checks made after calling Rails’s lock! or with_lock methods are safe from this issue, as they will reload the locked object as part of seizing the lock. Similarly, Rails' optimistic locking accounts for this by rejecting an update if the database object has been updated elsewhere since it was originally loaded from the database. Rails locks only protect locked objects, leaving any unlocked objects within a lock exposed to race conditions without additional locks or protections. Non-Rails locks that do not reload important objects within the protected code will be similarly exposed.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This is not a full account of every possible issue that can arise while using Sidekiq. Many that are not covered here are detailed in <a target="_blank" href="https://github.com/mperham/sidekiq/wiki">the very helpful Sidekiq wiki</a>. On top of that, more recent issues might only be present in <a target="_blank" href="https://github.com/mperham/sidekiq/issues">the Sidekiq issues page</a>, and paid versions of Sidekiq come with the option of emailing Sidekiq’s creator for support.</p>
]]></content:encoded></item><item><title><![CDATA[How Wonolo Does On-Call Today]]></title><description><![CDATA[Introduction
I started at Wonolo in 2019. Back then, there was no rotation, no on-call onboarding, no clear expectations, no playbooks, and no pager system—just alerts on a single slack channel. If you were a backend engineer, it was expected that yo...]]></description><link>https://engineeringblog.wonolo.com/how-wonolo-does-on-call-today</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/how-wonolo-does-on-call-today</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[engineering]]></category><category><![CDATA[on-call]]></category><dc:creator><![CDATA[Dane Anderson]]></dc:creator><pubDate>Tue, 06 Sep 2022 15:53:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1662479322251/WkMMx8HLy.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Introduction</strong></p>
<p>I started at Wonolo in 2019. Back then, there was no rotation, no on-call onboarding, no clear expectations, no playbooks, and no pager system—just alerts on a single slack channel. If you were a backend engineer, it was expected that you fix any alerts and bring the system back into a stable state. Without rotations and well-defined processes, it was much easier for backend engineers to constantly be burdened with potential production issues, risking burnout and fatigue.</p>
<p>Today, we have a much more defined process. We make use of an on-call rotation and onboard all new engineers that contribute code to the system. When an incident does occur, we utilize <a target="_blank" href="https://www.atlassian.com/incident-management/postmortem/blameless">blameless Post Mortems</a> and update any on-call playbooks to make sure we incorporate anything that happened during the incident.</p>
<p><strong>Rotation: Who Goes On Call</strong></p>
<p>Everyone. More specifically, every Engineer, Engineering Manager, Director and VP at Wonolo is expected to join an on-call rotation three months after their start date. The following chart is taken from our on-call policy in Confluence:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661349409240/2UCEX444e.png" alt="image.png" /></p>
<p><strong>How We Do On-Call Onboarding</strong></p>
<p>The main goal for our on-call onboarding is to set up everyone for success and ensure they are as comfortable as possible before starting that first on-call rotation. We achieve this by having well-written documentation that is sent out in advance of an onboarding session. The documentation includes the following documents:</p>
<ul>
<li>On-Call Overview: Policy, rotation, responsibilities, escalation, mitigation, and triage</li>
<li>On-Call Setup: How to properly set up your mobile device</li>
<li>On-Call Incident Response: How to handle suspected and confirmed production issues</li>
<li>On-Call Playbooks: Describes symptoms, impact, action, and helpful links</li>
</ul>
<p>The onboarding session itself outlines clear expectations for being on call and, more importantly, helps you feel like you are not alone. We try to reassure everyone that it's always okay to ask anyone for help or to escalate issues in the on-call rotation.</p>
<p><strong>Clear Expectations: The Responsibilities of Being On Call</strong></p>
<p>The ultimate goal of being on call is to remediate any issues with the platform deemed a P0. </p>
<p>A P0 means that the system is in an unhealthy state, not functioning, and should be fixed or mitigated down to a lower priority level as soon as possible. We have reduced the main responsibility to three steps:</p>
<ol>
<li>Alert: Acknowledge receipt of pager message.</li>
<li>Assess: Determine and communicate impact to stakeholders.</li>
<li>Mitigate &amp; Triage: Resolve or reduce to P1, and triage to the appropriate team.</li>
</ol>
<p><strong>Creating a Useful On-Call Playbook</strong></p>
<p>The On-Call Playbook was created with ease of use in mind. We created a table which has a row for every alert we have created and every symptom we have encountered. Each row contains Symptom, Impact, Action, and Helpful Links. Here is a small sample of the on-call playbook:</p>
<p><strong>Symptom</strong>
Contains the exact text that maps to an alert.</p>
<p><strong>Impact</strong>
The best guess of the impact on the system to help speed up the assessment and prioritization of the situation.</p>
<p><strong>Action</strong>
Suggested action for the on-call person to take.</p>
<p><strong>Helpful Links</strong>
Contain links to the exact logs, supporting metric charts, how-to documentation, etc.</p>
<p><strong>Conclusion</strong></p>
<p>The on-call process has evolved significantly since the beginning of my time here at Wonolo. As we grow and learn as a team, there will always be more for us to iterate and improve on, and I’m grateful we’ve built processes for our team around doing so. Having a shared responsibility across engineering, creating an onboarding program, defining clear expectations, and creating an easy-to-use on-call playbook are all important pieces of setting your team up for success.</p>
]]></content:encoded></item><item><title><![CDATA[Ditching Active Record Callbacks]]></title><description><![CDATA[If you've done development on a Ruby on Rails application before you're probably familiar with Active Record callbacks. For those unfamiliar, they are ORM object life-cycle hooks. They allow the developer to run custom code when an object is created,...]]></description><link>https://engineeringblog.wonolo.com/ditching-active-record-callbacks</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/ditching-active-record-callbacks</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[refactoring]]></category><dc:creator><![CDATA[Jeremy Williams]]></dc:creator><pubDate>Wed, 31 Aug 2022 18:01:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1661968320445/N1MMNCWs2.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've done development on a Ruby on Rails application before you're probably familiar with Active Record callbacks. For those unfamiliar, they are ORM object life-cycle hooks. They allow the developer to run custom code when an object is created, saved, updated, deleted and validated. They can be defined on an Active Record object at the class level like so:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span> &lt; ActiveRecord::Base</span>
  before_save <span class="hljs-symbol">:run_callback</span>, <span class="hljs-symbol">if:</span> -&gt; { some_attr_changed? &amp;&amp; some_other_attr_changed? }
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_callback</span></span>
    <span class="hljs-comment"># do something</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>I'm mostly concerned with these ones that run during <code>save</code>.</p>
<pre><code class="lang-ruby">before_validation
after_validation
before_create
after_create
before_update
after_update
before_save
after_save
​
<span class="hljs-comment"># I include custom active model validations because they operate in much the</span>
<span class="hljs-comment"># same way and cause similar headaches.</span>
validate
</code></pre>
<p>​
Unfortunately, for various reasons, callbacks tend to be overused. Their power can be alluring. They allow you to add code that will run whenever an object is saved to the database without needing to update the save call sites with any new code. There also exists the influence from arguments about Fat Controllers &amp; Thin Models vs. Thin Controllers &amp; Fat Models. While these arguments have died down, echoes remain in the way many developers write Rails code.
​</p>
<p>What also remains are the concrete dependencies that were created when the Thin Controllers &amp; Fat Models methodology was taken to the extreme. Developers were attempting to make controllers completely disappear and codify them into abstractions. Libraries like inherited_resources popped up that completely removed the need to write traditional CRUD controller code. Without any other place for the code to go it would end up being shoehorned into Active Record callbacks. inherited_resources was ultimately abandoned by its original author and deemed a misadventure. However, ActiveAdmin remains a popular library to construct admin panels with and continues to use inherited_resources with a "no controllers" coding style.</p>
<p>At our company these effects lead to a codebase with too many callbacks which has caused us many headaches. As well, a tendency for 'God Objects' to accrue in long lived projects compounded our situation. The combination of these problems left us with a few classes that had over seventy callbacks and custom validations defined on them. This makes reasoning about saving, creating and updating these objects incredibly difficult for a multitude of reasons: Callback code tends to be side effect heavy. Unscrupulously making just in time changes to the model attributes before save or validation. Schlepping data between systems to provide eventual consistency. Triggering updates on another model via the ever-present <code>recalculate!</code> method and of course, last but not least, firing off background jobs to do god knows what.
​</p>
<p>Another aspect of side effects that's important to understand is the order they occur in. A good trick to pull on a co-worker is ask them the order of execution on these callbacks when you create a Foo record without looking at the documentation:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span> &lt; ActiveRecord::Base</span>
  before_create <span class="hljs-symbol">:one</span>
  before_save <span class="hljs-symbol">:two</span>
  after_commit <span class="hljs-symbol">:three</span>
  after_commit <span class="hljs-symbol">:four</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The order will, oddly enough, be: two, one, four, three; see the documentation for further details.
​</p>
<p>Side effects and state changes in the system are the most difficult thing to understand because of their far reaching consequences. Now thread the application of them through a gamut of conditionals that are nigh impossible to understand. Having fun yet? You may as well be staring into the Ark of the Covenant instead of trying to understand the code. "It's not that bad! Just fire up the debugger." you might say. Oh wait, right, since callbacks are declared at the class level you can't actually step through them with a debugger as you would imperative code and the conditionals defined on them often can't be inspected in an easy way at runtime.
​
<img src="https://c.tenor.com/pWZZ9gzx_p0AAAAC/face-melting-indiana-jones.gif" alt="Melting" />
​
Unlike Indy, in our case, we could not just close our eyes. We wanted to regain the debuggability and readability of simple imperative Ruby code while maintaining interoperability with the existing code that uses our objects. This meant that if we had a model <code>Foo</code> we didn't want to go to hundreds of places in our code to modify calls to <code>#save</code>, <code>#update</code>, <code>.create</code>, etc. While that might be an admirable goal at some point it wasn't within the scope of this project. We tried a couple different proof of concepts and ultimately landed on something structured like this:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span> &lt; ActionRecord::Base</span>
  alias_method <span class="hljs-symbol">:active_model_validate</span>, <span class="hljs-symbol">:valid?</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">valid?</span><span class="hljs-params">(...)</span></span>
  <span class="hljs-keyword">end</span>
​
  alias_method <span class="hljs-symbol">:validate</span>, <span class="hljs-symbol">:valid?</span>
​
  alias_method <span class="hljs-symbol">:active_record_save!</span>, <span class="hljs-symbol">:save!</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save!</span><span class="hljs-params">(...)</span></span>
  <span class="hljs-keyword">end</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span><span class="hljs-params">(...)</span></span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>All entry-point methods that invoke callback style code ultimately execute these methods. By aliasing the methods before overriding them we preserve access to the original implementations. This provides a starting point for our refactor. The theory being: all we need to do is delete the callback declarations from the model and write our callback code inline in these methods. Let's look at an example of what that might look like. First the before code with some nasty callbacks.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span> &lt; ActionRecord::Base</span>
  before_validation <span class="hljs-symbol">:before_validation_callback</span>, <span class="hljs-symbol">if:</span> -&gt; { some_attr_changed? }
  after_validation <span class="hljs-symbol">:after_validation_callback</span>, <span class="hljs-symbol">if:</span> -&gt; { some_other_attr_changed? }
  before_create <span class="hljs-symbol">:before_create_callback</span>, <span class="hljs-symbol">if:</span> -&gt; { some_attr_changed? &amp;&amp; some_other_attr_changed? }
  after_create <span class="hljs-symbol">:after_create_callback</span>, <span class="hljs-symbol">if:</span> <span class="hljs-symbol">:some_condition?</span>
  before_update <span class="hljs-symbol">:before_update_callback</span>, <span class="hljs-symbol">if:</span> <span class="hljs-symbol">:some_other_condition?</span>
  after_update <span class="hljs-symbol">:after_update_callback</span>
  before_save <span class="hljs-symbol">:before_save_callback</span>, <span class="hljs-symbol">unless:</span> -&gt; { some_attr_changed? }
  before_save <span class="hljs-symbol">:before_save_callback_on_create</span>, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:create</span>, <span class="hljs-symbol">if:</span> <span class="hljs-symbol">:some_condition?</span>
  before_save <span class="hljs-symbol">:before_save_callback_on_update</span>, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:update</span>
  after_save <span class="hljs-symbol">:after_save_callback</span>, <span class="hljs-symbol">if:</span> -&gt; { some_other_attr_changed? }
  after_save <span class="hljs-symbol">:after_save_callback_on_create</span>, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:create</span>
  after_save <span class="hljs-symbol">:after_save_callback_on_update</span>, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:update</span>, <span class="hljs-symbol">unless:</span> <span class="hljs-symbol">:some_other_condition?</span>
​
  validate <span class="hljs-symbol">:custom_validation</span>
  validate <span class="hljs-symbol">:maybe_custom_validation</span>, <span class="hljs-symbol">if:</span> <span class="hljs-symbol">:some_condition?</span>
  validate <span class="hljs-symbol">:custom_validation_on_create</span>, <span class="hljs-symbol">unless:</span> <span class="hljs-symbol">:some_other_condition?</span>, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:create</span>
  validate <span class="hljs-symbol">:custom_validation_on_update</span>, <span class="hljs-symbol">if:</span> -&gt; { some_attr_changed? }, <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:update</span>
​
  <span class="hljs-comment"># referenced methods would be defined below...</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>That's only 16 callbacks and custom validations on a single model. Imagine seventy. Now let's refactor this into our structure above. With some careful reading of the documentation and some experimentation we can discern the expected execution order of these callbacks. Leading us to code that looks like this.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span> &lt; ActionRecord::Base</span>
  alias_method <span class="hljs-symbol">:active_model_validate</span>, <span class="hljs-symbol">:valid?</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">valid?</span><span class="hljs-params">(...)</span></span>
    <span class="hljs-comment"># Before Validation callbacks</span>
    before_validation_callback <span class="hljs-keyword">if</span> some_attr_changed?
​
    <span class="hljs-comment"># Call the original validations. </span>
    active_model_validate(...)
​
    <span class="hljs-comment"># Custom validations</span>
    custom_validation
    maybe_custom_validation <span class="hljs-keyword">if</span> some_condition?
    custom_validation_on_create <span class="hljs-keyword">if</span> !some_other_condition? &amp;&amp; creating_record
    custom_validation_on_update <span class="hljs-keyword">if</span> some_attr_changed? &amp;&amp; !creating_record
​
    <span class="hljs-comment"># After Validation callbacks</span>
    after_validation_callback <span class="hljs-keyword">if</span> some_other_attr_changed?
​
    errors.blank?
  <span class="hljs-keyword">end</span>
​
  alias_method <span class="hljs-symbol">:validate</span>, <span class="hljs-symbol">:valid?</span>
​
  alias_method <span class="hljs-symbol">:active_record_save!</span>, <span class="hljs-symbol">:save!</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span><span class="hljs-params">(...)</span></span>
    save!(...)
  <span class="hljs-keyword">rescue</span> ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
    <span class="hljs-literal">false</span>
  <span class="hljs-keyword">end</span>
​
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save!</span><span class="hljs-params">(**opts)</span></span>
    creating_record = new_record?
    saved = <span class="hljs-literal">false</span>
​
    <span class="hljs-comment"># All callback code runs inside a single transaction</span>
    Foo.transaction <span class="hljs-keyword">do</span>
      <span class="hljs-comment"># Throw :abort can be used inside callbacks to skip saving</span>
      catch(<span class="hljs-symbol">:abort</span>) <span class="hljs-keyword">do</span>
        <span class="hljs-comment"># Don't validate if validate: false was passed in</span>
        <span class="hljs-keyword">if</span> opts[<span class="hljs-symbol">:validate</span>] != <span class="hljs-literal">false</span>
          raise ActiveRecord::RecordInvalid, <span class="hljs-keyword">self</span> <span class="hljs-keyword">unless</span> valid?
        <span class="hljs-keyword">end</span>
​
        <span class="hljs-comment"># Before save callbacks</span>
        before_save_callback <span class="hljs-keyword">unless</span> some_attr_changed?
        before_save_callback_on_create <span class="hljs-keyword">if</span> creating_record &amp;&amp; some_condition?
        before_save_callback_on_update <span class="hljs-keyword">if</span> creating_record
​
        <span class="hljs-comment"># Before create/update callbacks</span>
        <span class="hljs-keyword">if</span> creating_record
          before_create_callback <span class="hljs-keyword">if</span> some_attr_changed? &amp;&amp; some_other_attr_changed?
        <span class="hljs-keyword">else</span>
          before_update_callback <span class="hljs-keyword">if</span> some_other_condition?
        <span class="hljs-keyword">end</span>
​
        <span class="hljs-comment"># Save</span>
        saved = active_record_save!(**opts.merge(<span class="hljs-symbol">validate:</span> <span class="hljs-literal">false</span>))
      <span class="hljs-keyword">end</span>
​
      <span class="hljs-comment"># Raise error if record wans't saved</span>
      raise ActiveRecord::RecordNotSaved <span class="hljs-keyword">unless</span> saved
​
      <span class="hljs-comment"># After create/update callbacks</span>
      <span class="hljs-keyword">if</span> creating_record
        after_create_callback <span class="hljs-keyword">if</span> some_condition?
      <span class="hljs-keyword">else</span>
        after_update_callback
      <span class="hljs-keyword">end</span>
​
      <span class="hljs-comment"># After save callbacks</span>
      after_save_callback <span class="hljs-keyword">if</span> some_other_attr_changed?
      after_save_callback_on_create <span class="hljs-keyword">if</span> creating_record
      after_save_callback_on_update, <span class="hljs-keyword">if</span> !creating_record &amp;&amp; !some_other_condition?
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">rescue</span> StandardError =&gt; e
    <span class="hljs-comment"># Signal "rollback"</span>
    rolledback!
    raise e
  <span class="hljs-keyword">end</span>
​
  <span class="hljs-comment"># referenced methods would be defined below...</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>After the refactor you end up with some pretty ugly code. Make no mistake though, just because there were less lines before doesn't mean it was better or clearer. Our refactor has brought us numerous advantages. It's now possible to stick a debug statement at the top of <code>save!</code> and step through all custom validations and callbacks. You can even be debugging another piece of code and step into the <code>save</code> or <code>save!</code> call and step through it. All the weird control flow that was buried in the Active Record documentation is now obvious. Also, now that we are dealing with plain old Ruby code we've opened up more avenues to factor this code out of our model.
​</p>
<p>Which is exactly what we wanted to do considering how large our model was. At Wonolo we have chosen the Command pattern to fill out our service layer so we explored factoring out the callback code into a command. We created two new commands, one for validating and one for saving. These commands can be filled with the code from the <code>valid?</code> and <code>save!</code> methods respectively.</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">valid?</span><span class="hljs-params">(*args)</span></span>
  Commands::Foo::Validate.new(<span class="hljs-keyword">self</span>, *args).call
<span class="hljs-keyword">end</span>
​
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save!</span><span class="hljs-params">(**opts)</span></span>
  Commands::Foo::Save.new(<span class="hljs-keyword">self</span>, **opts).call
<span class="hljs-keyword">end</span>
</code></pre>
<p>With these two new command classes we now have a place to put code. Not only can we move the contents of these methods but with some minor tweaks we can start factoring out the callback method definitions themselves.
​</p>
<p>I think there are clear advantages to ditching callbacks for more traditional style code. As shown here it's possible without needing to change call sites all over the codebase. I think there is still more work to be done to get the code to the place we want it. However, this strategy provides a great starting point to get the ball rolling. From here I can easily see this code being broken up further into create and update commands. Even that may not be the correct stopping point. Depending on the complexity of the model, create and update could be subdivided further into finer grained commands.
​</p>
<p>So far I've also only written about doing this on a single model by hand. To do this to all models across a large code base is going to be a lot of effort. Fortunately, the code transformations to do this are quite rote and a good candidate for automation. So that's exactly what we did to perform this refactor across our entire codebase. However, that's a topic for another day, so stay tuned.</p>
]]></content:encoded></item><item><title><![CDATA[Fundamentals of Our Engineering Org]]></title><description><![CDATA[Having led engineering teams for the bulk of my career, there are certain ideas that seem to repeat themselves, and ones that I believe are essential to maintaining a well-functioning organization. When I joined Wonolo as the VP of Engineering, I tho...]]></description><link>https://engineeringblog.wonolo.com/fundamentals-of-our-engineering-org</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/fundamentals-of-our-engineering-org</guid><category><![CDATA[engineering]]></category><category><![CDATA[empathy]]></category><dc:creator><![CDATA[Waynn Lue]]></dc:creator><pubDate>Fri, 19 Aug 2022 17:18:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/wawEfYdpkag/upload/v1660848838717/UnboSbNCg.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Having led engineering teams for the bulk of my career, there are certain ideas that seem to repeat themselves, and ones that I believe are essential to maintaining a well-functioning organization. When I joined Wonolo as the VP of Engineering, I thought it would be important for my team to understand my thoughts around some of these concepts, and I wrote a version of this living document as a way for them to see where I was coming from.</p>
<ol>
<li><strong>Empathy</strong>. At the core of every organization are the relationships and bonds between the people on the team. It's important to give direct feedback, but it has to be delivered with empathy and genuine belief in good faith.</li>
<li><strong>Respect</strong>. Treating others with respect sets the foundation for successful interactions. Respect comes in all forms, from allowing others to voice their opinions even if you disagree, to preparing for and giving your attention in meetings, to understanding how decisions you make will affect cross-team milestones or deadlines. </li>
<li><strong>Conflict</strong>. There will always be conflict within any team, but the process by which conflict is resolved carries far more weight than the conflict itself. Disagreeing with someone shouldn’t be construed as being difficult, as long as it’s done honestly and with good intent towards prioritizing what’s best for the customer and company. </li>
<li><strong>Career advancement</strong>. Every engineer on the team has a path to be successful, whether they’re an individual contributor or a manager. The skills necessary to be a good manager are not the same as the skills needed to be a good software engineer. Becoming a manager is often a lateral move, not a promotion.</li>
<li><strong>Technical ability</strong>. At the end of the day, we are a tech company and it’s our team’s responsibility to drive engineering excellence. And while we value technical expertise in managers, they won’t necessarily be the strongest technical person on a team.</li>
<li><strong>Prioritization</strong>. It’s important for an engineering team to see the trees through the forest. Our goal is to understand where we’re going and build a path to get there. Managers should be removing obstacles from their team, and should be prioritizing overall team productivity. We should make choices to improve engineering efficiency throughout the organization.</li>
<li><strong>Communication</strong>. Keeping lines of communication open allows us to do our jobs better and more efficiently. Not only is communication between engineering teams important, but so is cross-team communication. Whether it be Product and Design, RS&amp;O, or Customer Support, our engineers need to build strong relationships so we can build the best product for our customers.</li>
<li><strong>Quality</strong>. QA is not the first line of defense against bugs. Engineers are ultimately responsible for their code and the bugs they ship. You can have a bug once, you shouldn't have it twice. Writing a test is often the best way to do that.</li>
<li><strong>Hiring</strong>. One of the highest leverage actions an engineer can do is hiring. Growing our team by adding new engineers can elevate both the level of technical expertise and the culture within the organization. We hire for culture add, not just culture fit.</li>
<li><strong>Metrics</strong>. We are a metrics driven organization, it allows us to concretely say whether the features we are shipping are moving the needle and getting us the results we want. We don’t ship features for the sake of shipping features. </li>
</ol>
<p>Thanks for reading, and hope you enjoyed this post! I’m sure these ideas will change as the organization continues to evolve, but hopefully this gives you some insight into how the Wonolo engineering team functions.</p>
<ul>
<li>Waynn, VP of Engineering</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Hello World]]></title><description><![CDATA[Hello World!
Welcome to the Wonolo engineering blog! Wonolo is an on-demand staffing platform that helps people find consistent work. We’re a two-sided marketplace that matches workers (or as we call them, Wonoloers) to jobs all over the US.
We’ve be...]]></description><link>https://engineeringblog.wonolo.com/hello-world</link><guid isPermaLink="true">https://engineeringblog.wonolo.com/hello-world</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Waynn Lue]]></dc:creator><pubDate>Fri, 15 Jul 2022 21:13:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/8q6e5hu3Ilc/upload/v1660694046702/OEMkfhXcl.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello World!</p>
<p>Welcome to the Wonolo engineering blog! Wonolo is an on-demand staffing platform that helps people find consistent work. We’re a two-sided marketplace that matches workers (or as we call them, Wonoloers) to jobs all over the US.</p>
<p>We’ve been a technology company since day one, from the first versions of our apps to the complex ML models that now power our matching algorithms. We're starting this blog to share insights we’ve gathered through building the software that powers everything at Wonolo, and we hope this will be a tool for other engineers to get an inside look at how things work here, both technically and philosophically.</p>
<p>Some topics we’re hoping to cover in the upcoming weeks and months: Ditching Active Record Callbacks, Migrating to SwiftUI, Building a new code ownership tool, and many more. We’re super excited to get this started, stay tuned for more!</p>
<ul>
<li>Waynn</li>
</ul>
]]></content:encoded></item></channel></rss>