<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Zulip on Noetic Nought</title>
    <link>https://punchagan.muse-amuse.in/tags/zulip/</link>
    <description>Recent content in Zulip on Noetic Nought</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <copyright>© 2006-2026 CC-BY-SA-4.0</copyright>
    <lastBuildDate>Thu, 28 May 2020 11:34:00 +0530</lastBuildDate>
    <atom:link href="https://punchagan.muse-amuse.in/tags/zulip/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>An innocent query that caused Zulip downtime</title>
      <link>https://punchagan.muse-amuse.in/blog/zulip-downtime-postmortem/</link>
      <pubDate>Thu, 28 May 2020 11:34:00 +0530</pubDate>
      <guid>https://punchagan.muse-amuse.in/blog/zulip-downtime-postmortem/</guid>
      <description>&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&#xA;&lt;p&gt;Recently, &lt;a href=&#34;https://github.com/zulip/zulip/commit/8f32db81a1a1230b425aef8f7c6d2b7fc4543ee7&#34;&gt;a change&lt;/a&gt; that I contributed caused some downtime on the Zulip cloud&#xA;(zulipchat.com). Here&amp;rsquo;s a user &lt;a href=&#34;https://twitter.com/zulip/status/1255005502599131137&#34;&gt;tweeting at us&lt;/a&gt;, about this.&lt;/p&gt;&#xA;&lt;p&gt;I shared this with some friends (who use Zulip) and they asked me to share more&#xA;about what happened. I began writing an explanation, and it eventually turned&#xA;into a blog post. The post tries to explain what caused the down-time, without&#xA;assuming too much knowledge of the Zulip DB schema, but it would help if you&#xA;used Zulip a little.&lt;/p&gt;&#xA;&lt;p&gt;The goal of my change was to change how Zulip backend calculates the&#xA;&lt;code&gt;furthest_read_time&lt;/code&gt; for a user &amp;ndash; the time stamp indicating when the last&#xA;message was read.&lt;/p&gt;&#xA;&lt;p&gt;Previously, the code used simply kept track of the message ID of the messages&#xA;being read by a user in a field called the &amp;ldquo;pointer&amp;rdquo;. My change was to move away&#xA;from this &lt;code&gt;pointer&lt;/code&gt; approach to using the actual timestamp of when a user last&#xA;read a message.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-is-the-pointer&#34;&gt;What is the pointer?&lt;/h2&gt;&#xA;&lt;p&gt;Put very simply, the pointer is just a message ID&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; &amp;ndash; ideally, the ID of the&#xA;&amp;ldquo;last message&amp;rdquo; a user has read. Message IDs in Zulip are incremental &amp;ndash; a&#xA;message that has arrived after another one, would have a higher ID than the&#xA;older message. So, it would&amp;rsquo;ve been a great way to keep track of the last read&#xA;message, if all the messages were in &amp;ldquo;one continuous feed&amp;rdquo;. But, guess what,&#xA;Zulip has streams and topics which are precisely what make Zulip such a valuable&#xA;communication tool!&lt;/p&gt;&#xA;&lt;p&gt;The pointer can be set to the ID of the message that is currently being read,&#xA;but only when we are in the &amp;ldquo;All messages&amp;rdquo; view. In this view, all messages are&#xA;ordered chronologically, so any message that is after another message would have&#xA;arrived after it, and would have an ID greater than the other message. So&#xA;reading a message in this view means, any message with a lower ID must&amp;rsquo;ve&#xA;already been read.&lt;/p&gt;&#xA;&lt;p&gt;When you are reading messages in a stream/topic (narrowed) view, the &lt;code&gt;pointer&lt;/code&gt;&#xA;is not updated. We can&amp;rsquo;t be sure that all messages with ID lower than the&#xA;current message&amp;rsquo;s ID have already been read. There could be a whole bunch of&#xA;unread messages in other streams/topics than the current one you are reading.&#xA;So, the &lt;code&gt;pointer&lt;/code&gt; doesn&amp;rsquo;t get updated until you go to the &amp;ldquo;All messages&amp;rdquo; view&#xA;and scroll over the messages, even if you have already read them in a narrowed&#xA;view.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-change-this&#34;&gt;Why change this?&lt;/h2&gt;&#xA;&lt;p&gt;Using the &lt;code&gt;pointer&lt;/code&gt; to compute the &lt;code&gt;furthest_read_time&lt;/code&gt; doesn&amp;rsquo;t work very well,&#xA;because it is only updated in the &amp;ldquo;All messages&amp;rdquo; view. In other words,&#xA;&lt;code&gt;furthest_read_time&lt;/code&gt; turned out to actually mean something like&#xA;&lt;code&gt;furthest_read_time_in_all_messages_view&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;furthest_read_time&lt;/code&gt; is used in the UI to display the bankruptcy banner &amp;ndash; a&#xA;prompt shown to a user to mark all messages as read, if they have &amp;ldquo;too many&amp;rdquo;&#xA;unreads, and haven&amp;rsquo;t read new messages in the last 48 hours.&lt;/p&gt;&#xA;&lt;p&gt;But, this is a problem, especially in Zulip instances with a lot of traffic,&#xA;where people leave a lot of messages unread until they have the time to&#xA;catch-up. It is common for people to communicate only in a handful of&#xA;streams/topics, and leave a lot of messages unread in other streams or topics,&#xA;that they are hoping to catch-up on, when they have more time at hand. Such&#xA;users would avoid using the &amp;ldquo;All messages&amp;rdquo; view at any cost.&lt;/p&gt;&#xA;&lt;p&gt;Even in not-so-high-traffic realms, some users just prefer not to use the &amp;ldquo;All&#xA;messages&amp;rdquo; view, and just stick to narrowed views.&lt;/p&gt;&#xA;&lt;p&gt;So, for these users the &lt;code&gt;furthest_read_time&lt;/code&gt; would get updated very rarely, and&#xA;could potentially remain at a date way back in the past. This meant the users&#xA;would see the bankruptcy banner despite actively participating in the Realm,&#xA;even if only in a few topics/streams. This is annoying UX, and what something we&#xA;wanted to fix.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-did-we-change-this-to&#34;&gt;What did we change this to?&lt;/h2&gt;&#xA;&lt;p&gt;We decided to use the last read message time as the message to consider for the&#xA;&lt;code&gt;furthest_read_time&lt;/code&gt;. The following &amp;ldquo;innocent looking&amp;rdquo; query should do that for&#xA;us, we thought&amp;hellip;&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;UserMessage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;objects&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;filter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_profile&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_profile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserMessage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;last&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well, it does, but not without bringing down the whole of &lt;code&gt;zulipchat.com&lt;/code&gt; after&#xA;a deploy! 🙈 But, why?!&lt;/p&gt;&#xA;&lt;p&gt;Some background on how data is stored would be useful to see why.&lt;/p&gt;&#xA;&lt;h2 id=&#34;usermessage-table-is-huge-my-friend&#34;&gt;UserMessage table is huge, my friend&lt;/h2&gt;&#xA;&lt;p&gt;Zulip&amp;rsquo;s data model has a &lt;code&gt;Message&lt;/code&gt; table that stores the content of each&#xA;message, when it was sent, by whom, to which topic, etc. The &lt;code&gt;UserMessage&lt;/code&gt; table&#xA;is then used to keep track of what the status of this message is, for each user&#xA;that has received the message.  A 32 bit flag field is used to keep track of&#xA;things like, whether the user has read a message, starred a message, has been&#xA;mentioned in a message, etc. So, if a messages is sent to a stream with N users,&#xA;it creates 1 row in the &lt;code&gt;Message&lt;/code&gt; table and N rows in the &lt;code&gt;UserMessage&lt;/code&gt; table.&#xA;If M such messages are sent, it would create M*N rows in the &lt;code&gt;UserMessage&lt;/code&gt;&#xA;table. So, pretty soon, the &lt;code&gt;UserMessage&lt;/code&gt; table becomes huge! &lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;p&gt;To make common queries on this table efficient, the DB has indexes for them.&#xA;Some of the indexes on the &lt;code&gt;UserMessage&lt;/code&gt; table are documented &lt;a href=&#34;https://zulip.readthedocs.io/en/latest/production/expensive-migrations.html#running-expensive-migrations-early&#34;&gt;here&lt;/a&gt;, though the&#xA;documentation was created in the migration context for production deployments.&#xA;There are indexes for various things, including &lt;strong&gt;unread&lt;/strong&gt; messages.&lt;/p&gt;&#xA;&lt;p&gt;And here&amp;rsquo;s our innocent looking query, as a reminder.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;UserMessage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;objects&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;filter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_profile&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_profile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserMessage&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;last&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This query searches through the &lt;code&gt;read&lt;/code&gt; messages for a particular user, and tries&#xA;to figure out the last message they read. Note that the DB only has indexes for&#xA;unread messages, not read messages! So, this query isn&amp;rsquo;t using DB indexes to&#xA;search for this needle in the haystack, and ends up being pretty damn slow!&lt;/p&gt;&#xA;&lt;p&gt;Additionally, this query is run for every user when the &amp;ldquo;home page&amp;rdquo; is loaded&#xA;since we want to calculate their &lt;code&gt;furthest_read_time&lt;/code&gt; to decide if should show&#xA;the bankruptcy banner.&lt;/p&gt;&#xA;&lt;p&gt;Also, Zulip&amp;rsquo;s architecture is designed to allow using the same app server and DB&#xA;to host multiple different realms. And zulipchat.com is such a deployment where&#xA;a few servers are used to serve all the realms. (I don&amp;rsquo;t know too much about the&#xA;deployment setup, though).&lt;/p&gt;&#xA;&lt;p&gt;When a new change is deployed to zulipchat.com and the server is restarted, all&#xA;the clients of all the users reload. Having an expensive query in that path,&#xA;would just kill the DB. And this innocent looking query &lt;a href=&#34;https://twitter.com/zulip/status/1255005502599131137&#34;&gt;didn&amp;rsquo;t disappoint&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-did-we-fix-this&#34;&gt;How did we fix this?&lt;/h2&gt;&#xA;&lt;p&gt;Well, this change was &lt;a href=&#34;https://github.com/zulip/zulip/commit/976e554799a03ff9d82d7b75d77d45985cd25df4&#34;&gt;reverted&lt;/a&gt; immediately by &lt;a href=&#34;https://github.com/timabbott&#34;&gt;Tim&lt;/a&gt;, and things were kicked back&#xA;into a usable state.&lt;/p&gt;&#xA;&lt;p&gt;Later, we decided to take a different approach. We decided to look at a&#xA;different table (&lt;code&gt;UserActivity&lt;/code&gt;) that tracks user activity, mainly for the&#xA;purpose of analytics. This table keeps track of the last time a user performed&#xA;an action based on the API calls made.&lt;/p&gt;&#xA;&lt;p&gt;Among other things, changing any of the flags in the 32-bit flag field is one of&#xA;the actions that is tracked. Marking a message as read was being tracked too. We&#xA;use the last time this action was performed to compute the last time a user was&#xA;&amp;ldquo;active&amp;rdquo;, and based on that decide if we should show the bankruptcy banner or&#xA;not.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s the &lt;a href=&#34;https://github.com/zulip/zulip/commit/ded3b0076070365dddccd43d7f05fd627c2637f7&#34;&gt;actual commit&lt;/a&gt; that makes this change. But, it turns out that needed a&#xA;&lt;a href=&#34;https://github.com/zulip/zulip/commit/734d651b45542e9a1bdea10cd290637f73be30d9&#34;&gt;follow-up fix&lt;/a&gt; too. 🙈&lt;/p&gt;&#xA;&lt;div style=&#34;font-size:small;&#34; class=&#34;reviewers&#34;&gt;&#xA;&lt;p&gt;Thanks to &lt;a href=&#34;https://web.mit.edu/tabbott/www/&#34;&gt;Tim Abbott&lt;/a&gt; and &lt;a href=&#34;http://baali.muse-amuse.in&#34;&gt;Shantanu&lt;/a&gt; for reading drafts of this post.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;The pointer was used to do a &lt;a href=&#34;https://zulip.readthedocs.io/en/latest/subsystems/pointer.html&#34;&gt;bunch of useful things&lt;/a&gt;, but we are&#xA;currently in the process of &lt;a href=&#34;https://github.com/zulip/zulip/issues/8994&#34;&gt;removing it&lt;/a&gt;.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;Zulip&amp;rsquo;s code base uses a concept called &lt;a href=&#34;https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#soft-deactivation&#34;&gt;soft-deactivation&lt;/a&gt;&#xA;to mitigate this problem on Realms where there are a lot of users who log in&#xA;very infrequently, or don&amp;rsquo;t login after an initial period of activity. So, the&#xA;number of rows are not exactly &lt;code&gt;M*N&lt;/code&gt; in all the cases, but in the worst case&#xA;they are.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
  </channel>
</rss>
