<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Karim Hamidou</title>
    <description>The latest articles on DEV Community by Karim Hamidou (@karim_hamidou).</description>
    <link>https://dev.to/karim_hamidou</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F48472%2Fd575ec1c-4e27-44c2-a874-7f45ee0325b9.jpg</url>
      <title>DEV Community: Karim Hamidou</title>
      <link>https://dev.to/karim_hamidou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karim_hamidou"/>
    <language>en</language>
    <item>
      <title>Speeding up our Webhooks System 60x</title>
      <dc:creator>Karim Hamidou</dc:creator>
      <pubDate>Wed, 20 Jun 2018 18:12:18 +0000</pubDate>
      <link>https://dev.to/nylas/speeding-up-our-webhooks-system-60x-2idh</link>
      <guid>https://dev.to/nylas/speeding-up-our-webhooks-system-60x-2idh</guid>
      <description>&lt;p&gt;Last December, we had an interesting problem. Our webhook system, which lets our customers know whenever a change happens to a Nylas account, was struggling to keep up with the traffic. We were seeing some webhooks being delayed as much as 10 minutes, which is a long time for most of our end-users. We decided to take the plunge and rearchitect our webhook system to be faster. In the end, we made it 60x faster and made sure it’ll stay that way for the foreseeable future. In this post you’ll see how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detour: A brief introduction to the Nylas transaction log
&lt;/h2&gt;

&lt;p&gt;The Nylas API is built around the idea of a transaction log. The transaction log is an append-only log of all the changes that happened to our API objects. If you send a message through the Nylas API, we will create a transaction log entry noting that a “message” object was created. The same thing happens if you update an event, calendar or any other API object.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2FDesign-01.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2FDesign-01.svg" alt="Design-01"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We use the transaction log to power all our change notification APIs. For example, the transaction log is how you can ask our delta stream API all the changes for a specific account in the last 24 hours.&lt;/p&gt;

&lt;p&gt;Behind the scenes, the transaction log is implemented as a regular MySQL table. We’ve instrumented our ORM (SQLAlchemy) to write to this table whenever there is a change to an API object. In practice, this works relatively well, even though this part of the codebase relies a lot on SQLAlchemy internals, which makes it very brittle (if you’re curious about how it works, feel free to take a lot at the &lt;a href="https://github.com/nylas/sync-engine/blob/master/inbox/models/transaction.py#L56" rel="noopener noreferrer"&gt;sync-engine source code&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  How our legacy webhook system worked
&lt;/h2&gt;

&lt;p&gt;Our original webhook system was pretty simple and really reliable. It would spawn one reader thread per webhook. Each reader thread would sequentially read from each of our MySQL shards and send the changes it found. This let us offload most of the reliability work to MySQL – for example, if one of the webhooks machines crashed, we would just have to reboot it and it would pick up things where it left off. That also meant that if a customer webhook went down then came back up, we’d be able to send them all the changes that happened in the interval, which made outage recovery for our customers a lot easier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-02.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-02.svg" alt="Design-02"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, as we grew from a couple MySQL shards to several hundred, and from a half-dozen customers to hundreds, this architecture started making less and less sense. Having one thread per customer meant that as our number of customer grew, our system would get slower and slower.&lt;/p&gt;

&lt;p&gt;This might sound like a glaring limitation of our legacy system, but this was the right call when it was written three years ago. At the time, it was more important to have a simple and reliable system than to have something that was fast and scaled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding webhooks
&lt;/h2&gt;

&lt;p&gt;Once we decided to rebuild the system, we had to figure out what kind of architecture would work  best for our workload. To do that, we started by looking at the &lt;a href="https://www.nylas.com/blog/performance/" rel="noopener noreferrer"&gt;flamegraphs&lt;/a&gt; for our legacy system. Here’s a typical one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd2mxuefqeaa7sj.cloudfront.net%2Fs_234B0D79AADA5B4F166BEF2EF90D08BF9C8666FC8D0E26EAFFEA819987372305_1524854788822_Capture%2Bdecran%2B2018-04-27%2Ba%2B11.45.42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fd2mxuefqeaa7sj.cloudfront.net%2Fs_234B0D79AADA5B4F166BEF2EF90D08BF9C8666FC8D0E26EAFFEA819987372305_1524854788822_Capture%2Bdecran%2B2018-04-27%2Ba%2B11.45.42.png" alt="null"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One things jumps out immediately: we’re spending a lot of time executing SQLAlchemy code, and waiting for our MySQL shards. Seeing this confirmed a hunch we’ve had for a long time – we had too many readers. &lt;/p&gt;

&lt;p&gt;To figure out if this was right, we decided to built a prototype that uses a single-reader architecture to send webhooks. Here’s the architecture we came up with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-03.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-03.svg" alt="Design-03"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Basically, we’d be moving from several readers per shards to one reader per-shard. We decided to try this out and see if it would solve our load problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phantom reads problem
&lt;/h2&gt;

&lt;p&gt;After three weeks of work, we felt that we had a system that was reliable enough to run production workloads, so we decided to ship a test version of the system that wouldn’t send actual webhooks. This way, we’d be able to iron out performance issues and make sure that there were no consistency issues between the legacy and new systems.&lt;/p&gt;

&lt;p&gt;After doing a lot of testing, one issue kept happening – sometimes the new system wouldn’t send a transaction that should have been sent. This happened unpredictably and at any time of the day.&lt;/p&gt;

&lt;p&gt;We spent a lot of time trying to figure out the issue – was it a problem in the way we were creating &lt;code&gt;transaction&lt;/code&gt; objects? Was there a subtle bug in the way we were consuming the &lt;code&gt;transaction&lt;/code&gt; table?&lt;/p&gt;

&lt;p&gt;The problem was both more and less complicated. Every time we’d send a transaction to a customer we’d save its &lt;code&gt;id&lt;/code&gt; to know where to resume in case of an interruption. However, it turns out that this isn’t safe at all, because of a misunderstanding we had about MySQL autoincrements:  we assumed that they were generated at &lt;code&gt;COMMIT&lt;/code&gt; time – which means that they’d be always incrementing. &lt;/p&gt;

&lt;p&gt;However, this isn’t the case all the time, for example if two transactions are executing concurrently, one of these may not be  – here’s an example of why: &lt;/p&gt;

&lt;p&gt;&lt;a href="/hubfs/blog%20images/Webhooks%20images/Design-05%20(2).svg?t=1529514994570"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-05%2520%282%29.svg" alt="Design-05 (2)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This issue meant we couldn’t rely on MySQL to get transactions in order. From our point of view, there were three different ways we could go:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We could read from the &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/binary-log.html" rel="noopener noreferrer"&gt;MySQL binlog&lt;/a&gt;, which is the mechanism MySQL uses for its replication&lt;/li&gt;
&lt;li&gt; We could use Apache Kafka&lt;/li&gt;
&lt;li&gt; We could use Amazon Web Services’ Kafka clone, AWS Kinesis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the end, we ended up going with AWS Kinesis mainly because it’s the easiest one to operate – both the MySQL binlog and Kafka have a significant operational cost that we couldn’t pay at the time (by the way, &lt;a href="https://dev.to/jobs/"&gt;we’re hiring ops people&lt;/a&gt; 😅). &lt;/p&gt;

&lt;h2&gt;
  
  
  Kinesis to the rescue
&lt;/h2&gt;

&lt;p&gt;Kinesis is an interesting system – it’s really reliable and easy to operate, as long as you fit inside of its (seemingly arbitrary) constraints. For example,  Kinesis supports sharding, and you have to decide of the shard assignment yourself. This is fine but there’s a catch – every Kinesis is limited to 1MB of writes per second and 2MB of reads. On top of that, you can only have 5 transactions per seconds for reads, so having several processes reading from the same shard was out of question.&lt;/p&gt;

&lt;p&gt;Obviously, this constraints aren’t the end of the world be we had to build our system around them. Here’s the system we ended up with – like the previous system we end up having a single reader per shard, except this time it’s reading from Kinesis instead of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-04%2520%281%29%2520%281%29.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn2.hubspot.net%2Fhubfs%2F3314308%2Fblog%2520images%2FWebhooks%2520images%2FDesign-04%2520%281%29%2520%281%29.svg" alt="Design-04 (1) (1)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One interesting property of the new system is that it only uses Kinesis for getting an ordered log of changes – for the rest (like for example, catching up a customer webhook after a period of downtime), the new system will read the data from our database, which is a plus for durability, and helps us avoid Kinesis’ five transactions per second limitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out
&lt;/h2&gt;

&lt;p&gt;Webhooks are really really important for our customers. Breaking the system would mean breaking their apps for an undefined amount of time. To avoid doing this, we had to roll out this new service a little differently than the way we usually do it.&lt;/p&gt;

&lt;p&gt;The way we usually roll out a new service is to do a staged rollout – we start by shipping the changes to 10% of our userbase, and then gradually increase the numbers throughout the day. We could have done this with the new webhooks service but we wanted to avoid any unexpected issues, were they dropping webhooks, or performance regressions, etc. &lt;/p&gt;

&lt;p&gt;To do this, we decided at the start of the project to work on a version of the service that would simulate sending webhooks instead of actually sending them. This would give us confidence that we wouldn’t run into any issues at the launch. By the time we rolled out the service to our customers, it had been running in “simulated mode” for two months. That gave us a lot of confidence in the reliability of the new system. &lt;/p&gt;

&lt;p&gt;We didn’t stop there though – given that our new system shouldn’t be dropping webhooks, ever, we spent several weeks instrumenting it to make sure that wasn’t the case. There weren’t a lot of good solutions for this, so we ended up instrumenting both the legacy webhooks systems and the new system to make sure that they were sending the same transactions. There’s no good way to do this, so we had to build a custom Flask app that got pinged whenever we sent a webhook.&lt;/p&gt;

&lt;p&gt;Eventually, we became confident enough in the new system to roll it out to all our customers. That meant making a copy of each existing webhook, pointing that copy to the new system, turning off the old webhooks then turning on the new one and making sure no transactions were dropped.&lt;/p&gt;

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

&lt;p&gt;Once the rollout was complete, we were able to measure the latency improvement for all our customers. The results were drastic – the old system would very often take more than a minute just to process a transaction and send it. Our P90 latency was over 90 seconds. With the new system, our P90 latency sits at around 1.5 secs – that’s a 60x improvement!&lt;/p&gt;

&lt;p&gt;Finally, thanks to &lt;a href="https://rcoh.me/" rel="noopener noreferrer"&gt;Russell Cohen&lt;/a&gt; who’s contracted with Nylas on this project and was instrumental in getting it out the door!&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>database</category>
      <category>aws</category>
    </item>
    <item>
      <title>Why Consistency Matters When Building Reliable Systems</title>
      <dc:creator>Karim Hamidou</dc:creator>
      <pubDate>Wed, 06 Dec 2017 21:42:00 +0000</pubDate>
      <link>https://dev.to/nylas/why-consistency-matters-when-building-reliable-systems-a4a</link>
      <guid>https://dev.to/nylas/why-consistency-matters-when-building-reliable-systems-a4a</guid>
      <description>&lt;p&gt;&lt;a href="https://www.nylas.com/blog/why-consistency-matters-when-building-reliable-systems"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--WTyH5t5---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.nylas.com/hubfs/karim-12617.png%3Ft%3D1512586558795" alt="Why Consistency Matters When Building Reliable Systems"&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We recently faced what could have been a serious user-facing outage: we rolled out a bad patch that caused the deletion and recreation of several hundred thousand folders from user’s inboxes. As a Nylas customer, you may have seen dozens of folders getting deleted then recreated.&lt;/p&gt;

&lt;p&gt;As we do after every outage, we conducted an internal postmortem to learn what went wrong and prevent issues like this from happening in the future.&lt;br&gt;
This time, we’ve decided to share this postmortem publicly because we think the bug was interesting!&lt;/p&gt;
&lt;h2&gt;
  
  
  What caused it?
&lt;/h2&gt;

&lt;p&gt;Before digging into the issue, it’s helpful to have context on how the &lt;a href="https://github.com/nylas/sync-engine"&gt;Nylas sync engine&lt;/a&gt; works. The sync engine is the core of our infrastructure and is responsible for &lt;a href="https://www.nylas.com/blog/billions-of-emails-synced-with-python"&gt;syncing billions of emails&lt;/a&gt;, calendar data, and contacts with our customer’s platforms — which are often customer relationship management platforms (CRMs), applicant tracking systems, and such.&lt;/p&gt;

&lt;p&gt;Keeping email data in a two-way sync between the end-user’s inbox and our customer’s platform is a pretty interesting challenge because protocols like IMAP and Exchange weren’t really built for bi-directional sync (in fact, they were built long before most people even know what a CRM was).&lt;/p&gt;

&lt;p&gt;For example, when you rename an IMAP folder, you’re not just re-writing the folder name. Instead, your email app has to re-download all the emails in this folder and reconcile them with the emails it’s previously synced. This means that we can not delete IMAP folders immediately. Instead, we mark them as deleted and move on. Later on, we’ll detect whether deletion was actually a rename by looking at the messages inside it. Folders that weren’t renamed are then garbage-collected by a separate process to make sure we’re not storing them forever.&lt;/p&gt;

&lt;p&gt;This garbage collection process was the source of the incident. Let’s dive into the part of the source code that caused the issue (if you’re curious, you can find the full source code &lt;a href="https://github.com/nylas/sync-engine/blob/master/inbox/mailsync/gc.py"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here and below, &lt;code&gt;Categories&lt;/code&gt; is the model we use to track IMAP folders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gc_deleted_categories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Delete categories which have been deleted on the backend.
&lt;/span&gt;    &lt;span class="c1"&gt;# Go through all the categories and check if there are messages
&lt;/span&gt;    &lt;span class="c1"&gt;# associated with it. If not, delete it.
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EPOCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Check if no message is associated with the category. If yes,
&lt;/span&gt;            &lt;span class="c1"&gt;# delete it.
&lt;/span&gt;            &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;MessageCategory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty straightforward, right? Get all deleted Categories, then check if they have actual messages associated with them. If not, delete them.&lt;/p&gt;

&lt;p&gt;The only strange thing is in the database query we’re making:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EPOCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, why are we filtering on messages more recent than epoch? Let’s look at the category model to figure it out! Here’s what the code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MailSyncBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HasRevisions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HasPublicID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdatedAtMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;DeletedAtMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Need `use_alter` here to avoid circular dependencies
&lt;/span&gt;    &lt;span class="n"&gt;namespace_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'namespace.id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_alter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'category_fk1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="n"&gt;ondelete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'CASCADE'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Namespace'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;load_on_pending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_INDEXABLE_LENGTH&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CategoryNameString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;type_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'folder'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'label'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'folder'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Override the default `deleted_at` column with one that is NOT NULL --
&lt;/span&gt;    &lt;span class="c1"&gt;# Category.deleted_at is needed in a UniqueConstraint.
&lt;/span&gt;    &lt;span class="c1"&gt;# Set the default Category.deleted_at = EPOCH instead.
&lt;/span&gt;    &lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'1970-01-01 00:00:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;__table_args__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'namespace_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'display_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="s"&gt;'deleted_at'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'namespace_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'public_id'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, this is a pretty standard model too. It defines a bunch of fields and sets a &lt;code&gt;UniqueConstraint&lt;/code&gt; to make sure we’re not storing duplicate folders. There is one problem though — because of a &lt;a href="https://bugs.mysql.com/bug.php?id=8173"&gt;12-year-old MySQL bug&lt;/a&gt; we have to set &lt;code&gt;deleted_at = epoch&lt;/code&gt; in order to mark folders as not deleted.&lt;/p&gt;

&lt;p&gt;Fast forward to two weeks ago: we noticed a rare condition where we would delete a folder right before detecting a rename. To work around this, we decided to add a six hour delay before deleting folders. To do that, we changed our previously innocuous query from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EPOCH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;namespace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;CATEGORY_TTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code would be perfectly normal if &lt;code&gt;Category.deleted_at&lt;/code&gt; had a default value of &lt;code&gt;None&lt;/code&gt;. However, in our case it had a default value of &lt;code&gt;EPOCH&lt;/code&gt; to work around this MySQL bug — causing us to delete a whopping 300k (empty) folders in the 45 minutes it took us to notice the issue and roll back this patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;p&gt;(All times in PST)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;13:15 We started rolling out this patch as part of our daily code push to production.&lt;/li&gt;
&lt;li&gt;13:25 The oncall engineer got paged because of an elevated number of accounts failing. They immediately created a status page incident and started looking into the issue.&lt;/li&gt;
&lt;li&gt;13:35 After escalating the issue, several engineers started digging into our logs to figure out what was happening.&lt;/li&gt;
&lt;li&gt;13:45 We decided to rollback as a preventive measure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later in the afternoon, we dug through the patches that got shipped and found the real cause of the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What measures are we taking to prevent this from happening again?
&lt;/h2&gt;

&lt;p&gt;This is an interesting bug because it highlights the importance of consistency in complex systems: all our models follow the convention where &lt;code&gt;deleted_at&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt; if the object isn’t soft-deleted. Both the person who wrote the code and the two people who reviewed assumed it was innocuous. There is no obvious culprit; just unexpected interactions.&lt;/p&gt;

&lt;p&gt;To make sure that this doesn’t happen again we started working on two things:&lt;br&gt;
1) Auditing our codebase for parts which don’t follow our internal conventions and fixing them.&lt;br&gt;
2) Stable identifiers for all our API objects. This would let us resync an account from scratch without any customer-visible effects.&lt;/p&gt;

&lt;p&gt;Stay tuned for more details about this!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qiHoNOTw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://track.hubspot.com/__ptq.gif%3Fa%3D3314308%26k%3D14%26r%3Dhttps%253A%252F%252Fwww.nylas.com%252Fblog%252Fwhy-consistency-matters-when-building-reliable-systems%26bu%3Dhttps%25253A%25252F%25252Fwww.nylas.com%25252Fblog%26bvt%3Drss" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qiHoNOTw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://track.hubspot.com/__ptq.gif%3Fa%3D3314308%26k%3D14%26r%3Dhttps%253A%252F%252Fwww.nylas.com%252Fblog%252Fwhy-consistency-matters-when-building-reliable-systems%26bu%3Dhttps%25253A%25252F%25252Fwww.nylas.com%25252Fblog%26bvt%3Drss" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post was originally published on the &lt;a href="https://www.nylas.com/blog/why-consistency-matters-when-building-reliable-systems"&gt;Nylas Engineering Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>database</category>
      <category>tips</category>
      <category>consistency</category>
    </item>
  </channel>
</rss>
