<?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: Kellen</title>
    <description>The latest articles on DEV Community by Kellen (@goodroot).</description>
    <link>https://dev.to/goodroot</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%2F1139629%2Fd87c7da4-07c7-462f-8bfa-0b46d50ac8ff.jpeg</url>
      <title>DEV Community: Kellen</title>
      <link>https://dev.to/goodroot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goodroot"/>
    <language>en</language>
    <item>
      <title>How to increase Grafana refresh rate frequency</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Wed, 10 Jan 2024 16:33:27 +0000</pubDate>
      <link>https://dev.to/questdb/how-to-increase-grafana-refresh-rate-frequency-468b</link>
      <guid>https://dev.to/questdb/how-to-increase-grafana-refresh-rate-frequency-468b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;QuestDB is a high-performance time series database with SQL analytics that can power through market data ingestion and analysis. It's &lt;a href="https://github.com/questdb/questdb" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and integrates well with the tools and languages you use. Check us out!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A nice thing about Grafana is seeing your dynamic dashboards refresh as data updates over time. However, to limit the load on the Grafana server both on the browser and on the underlying database, the maximum default refresh rate is once every 5 seconds. While this is certainly more than enough for some applications, in more real-time use cases such as financial market data, there is a latent need to always get always closer to realtime.&lt;/p&gt;

&lt;p&gt;Conveniently, Grafana allows you to easily tweak server settings to allow for higher frequencies. But there's a catch. Whatever is sending data to your dashboard needs to accommodate your desired rate. Luckily, Questdb can support hyper-fast refresh rates. The end result is more up to date dashboard with smooth continuous-looking updates and that unparalleled feeling of realtime.&lt;/p&gt;

&lt;p&gt;We recently built a series of &lt;a href="https://questdb.io/dashboards/crypto/" rel="noopener noreferrer"&gt;financial market data dashboards&lt;/a&gt; that refresh at a very fast interval. This guide shows you how to do the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why higher frequency
&lt;/h2&gt;

&lt;p&gt;The first question to ask is whether this feature is useful for your use case. Here are some example reasons and scenarios where this can be useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  It depends on the scale
&lt;/h3&gt;

&lt;p&gt;There is little upside in having high refresh rate on large scale charts which span a few hours to a few days. But when you are looking at small intervals such as the last minute, last 5 minutes and so on, then a higher update frequency makes sense as the short term changes become much more visible.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3p9hppy2kq6bu6w8bve.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3p9hppy2kq6bu6w8bve.png" alt="A graph demonstrating the above, a long term chart and a short term chart."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Most up to date information
&lt;/h3&gt;

&lt;p&gt;Often times you want to get updated information as soon as possible. Generally, if you are looking at prices, it's better to be looking at the latest price possible rather than at a price that's 5 seconds old. Whether it is material or not depends on your use case.&lt;/p&gt;

&lt;p&gt;If you are actively trading using Grafana charts as a tool, for example to highlight arbitrage opportunities, then want the latest price as soon as possible. If you are using a Grafana dashboard to view the value of your portfolio, or some other non-time-sensitive metric then - strictly speaking - it's better to be closer to realtime, but not necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shorter feedback loop
&lt;/h3&gt;

&lt;p&gt;Say you are working on a closed-loop system. For example, you change a parameter on your trading algo and you want to see the effect on derived metrics. Does that generate new trades? Does it change other linked parameters by propagation? And so on.&lt;/p&gt;

&lt;p&gt;If your actions trigger reactions, then a more frequent update frequency reduces the latency in receiving feedback. If you make a mistake changing something, you can see it immediately instead of somewhere in the next seconds. In some cases, it may not make a difference, in others, it can be critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's satisfying
&lt;/h3&gt;

&lt;p&gt;It cannot be just me… There's a big difference between looking at a periodically updating dashboard versus a continuously updating one. Yes, strictly speaking, the dashboard is always updating periodically and we just reduce the update period...&lt;/p&gt;

&lt;p&gt;However, when the period gets small, like one second and below, then it looks smooth and continuous. It's a bit like going from a small low definition 24 hz computer monitor to 4k 120hz. Sure, you can work using both. But one is much more comfortable and satisfying to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring update frequencies for your use case
&lt;/h2&gt;

&lt;p&gt;We'll review:&lt;/p&gt;

&lt;p&gt;how to tweak the default settings to add/remove options&lt;br&gt;
how to tweak the server configuration to allow even higher refresh frequencies than allowed by default&lt;br&gt;
Default frequency choices#&lt;/p&gt;

&lt;p&gt;Grafana comes with a few default frequency options&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 day&lt;/li&gt;
&lt;li&gt;2 hours&lt;/li&gt;
&lt;li&gt;1 hour&lt;/li&gt;
&lt;li&gt;30 minutes&lt;/li&gt;
&lt;li&gt;15 minutes&lt;/li&gt;
&lt;li&gt;5 minutes&lt;/li&gt;
&lt;li&gt;1 minute&lt;/li&gt;
&lt;li&gt;30 seconds&lt;/li&gt;
&lt;li&gt;10 seconds&lt;/li&gt;
&lt;li&gt;5 seconds&lt;/li&gt;
&lt;li&gt;off - you need to click the refresh data button manually to update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Tweaking default settings
&lt;/h2&gt;

&lt;p&gt;Typically, your dashboard may not need all the above options. If you typically refresh once every 2 hours for example, then probably the 5 seconds interval is not useful, and vice versa.&lt;/p&gt;

&lt;p&gt;Tweak these default options in the dashboard settings using the Auto refresh field:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuqkk1la0izemevvs5na8.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuqkk1la0izemevvs5na8.png" alt="Grafana dropdown showing refresh intervals"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To change, add, remove frequencies to the list, simply edit the list of time intervals. For example, you may choose to remove anything over 10 minutes, and add a custom intervals such as every 2 minutes, every 10 minutes and so on. The resulting list would look like the following:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygvw8y0eqwm16e29wu6w.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygvw8y0eqwm16e29wu6w.png" alt="Editing the interval"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After saving your dashboard, the new settings are available in the dropdown:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01hdbveso9zj0z9jx6rl.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01hdbveso9zj0z9jx6rl.png" alt="New settings in the dashboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Unleashing high frequency updates
&lt;/h2&gt;

&lt;p&gt;While this works for frequencies up to 5 seconds, this approach does not work by default for anything below 1 second. If you try to add a frequency such as '1s' or '250ms', then you won't normally see them in the dropdown.&lt;/p&gt;

&lt;p&gt;Here is an attempt with higher refresh rates below the standard maximum of once every 5 seconds:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkodzqowxzlw26wkrcgh2.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkodzqowxzlw26wkrcgh2.png" alt="Dropdown with various times, from 15m to 5s"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once we save the dashboard, the frequencies below 5s are not available in the dropdown:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygbdqtt57kgwm80768nq.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygbdqtt57kgwm80768nq.png" alt="Added things, but they do not show up"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is because the Grafana server config grafana.ini has a setting called &lt;code&gt;_min_refresh_interval&lt;/code&gt; which by default is set to &lt;code&gt;5s&lt;/code&gt;. The default location for this file is is &lt;code&gt;/etc/grafana/grafana.ini&lt;/code&gt;. But it depends on your OS and setup. If you need a locating it, checkout the &lt;a href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/" rel="noopener noreferrer"&gt;Grafana config docs&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;############## Dashboards History #######&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;dashboards]

&lt;span class="c"&gt;# Number dashboard versions to keep (per dashboard).&lt;/span&gt;
&lt;span class="c"&gt;# Default: 20, Minimum: 1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; versions_to_keep &lt;span class="o"&gt;=&lt;/span&gt; 20

&lt;span class="c"&gt;# Minimum dashboard refresh interval. When set, this&lt;/span&gt;
&lt;span class="c"&gt;# will restrict users to set the refresh interval of a&lt;/span&gt;
&lt;span class="c"&gt;# dashboard lower than given interval.&lt;/span&gt;
&lt;span class="c"&gt;# Per default this is 5 seconds.&lt;/span&gt;
&lt;span class="c"&gt;# The interval string is a possibly signed sequence&lt;/span&gt;
&lt;span class="c"&gt;# of decimal numbers, followed by a unit&lt;/span&gt;
&lt;span class="c"&gt;# suffix (ms, s, m, h, d), e.g. 30s or 1m.&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; min_refresh_interval &lt;span class="o"&gt;=&lt;/span&gt; 5s &lt;span class="c"&gt;# !!&lt;/span&gt;

&lt;span class="c"&gt;# Path to the default home dashboard.&lt;/span&gt;
&lt;span class="c"&gt;# If this value is empty, then Grafana&lt;/span&gt;
&lt;span class="c"&gt;# uses StaticRootPath + "dashboards/home. json"&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; default_home_dashboard_path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, you can set it to something else, for example &lt;code&gt;200ms&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;############## Dashboards History #######&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;dashboards]

&lt;span class="c"&gt;# Number dashboard versions to keep (per dashboard).&lt;/span&gt;
&lt;span class="c"&gt;# Default: 20, Minimum: 1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; versions_to_keep &lt;span class="o"&gt;=&lt;/span&gt; 20

&lt;span class="c"&gt;# Minimum dashboard refresh interval. When set, this&lt;/span&gt;
&lt;span class="c"&gt;# will restrict users to set the refresh interval of a&lt;/span&gt;
&lt;span class="c"&gt;# dashboard lower than given interval.&lt;/span&gt;
&lt;span class="c"&gt;# Per default this is 5 seconds.&lt;/span&gt;
&lt;span class="c"&gt;# The interval string is a possibly signed sequence&lt;/span&gt;
&lt;span class="c"&gt;# of decimal numbers, followed by a unit&lt;/span&gt;
&lt;span class="c"&gt;# suffix (ms, s, m, h, d), e.g. 30s or 1m.&lt;/span&gt;

min_refresh_interval &lt;span class="o"&gt;=&lt;/span&gt; 200ms &lt;span class="c"&gt;# Faster!&lt;/span&gt;

&lt;span class="c"&gt;# Path to the default home dashboard.&lt;/span&gt;
&lt;span class="c"&gt;# If this value is empty, then Grafana&lt;/span&gt;
&lt;span class="c"&gt;# uses StaticRootPath + "dashboards/home. json"&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; default_home_dashboard_path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the changes only take place upon restart, restart the Grafana instance. We can then setup our new frequencies in the dashboard settings:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzj0r3xn2h1av93h5pw6b.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzj0r3xn2h1av93h5pw6b.png" alt="Adding sub 5s values"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this time, they are available in the list dropdown:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj03jt78ldkhmrfan1zoy.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj03jt78ldkhmrfan1zoy.png" alt="All values in the dropdown"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we can get our realtime charts going!&lt;/p&gt;

&lt;p&gt;See a gif: &lt;a href="https://questdb.io/img/blog/2024-01-08/grafana-refresh-9.gif" rel="noopener noreferrer"&gt;https://questdb.io/img/blog/2024-01-08/grafana-refresh-9.gif&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Normalizing Grafana charts with window functions</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Wed, 10 Jan 2024 16:33:17 +0000</pubDate>
      <link>https://dev.to/questdb/normalizing-grafana-charts-with-window-functions-4ebd</link>
      <guid>https://dev.to/questdb/normalizing-grafana-charts-with-window-functions-4ebd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;QuestDB is a high-performance time series database with SQL analytics that can power through market data ingestion and analysis. It's &lt;a href="https://github.com/questdb/questdb" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and integrates well with the tools and languages you use. Check us out!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a previous post, we looked at how to &lt;a href="https://questdb.io/blog/manage-large-symbol-lists-questdb-grafana/" rel="noopener noreferrer"&gt;create dynamic lists of symbols and charts in Grafana&lt;/a&gt;. While this is great to watch individual charts for different symbols, sometimes you may want to merge all the charts together to compare changes in a visual manner.&lt;/p&gt;

&lt;p&gt;One of our community members had been struggling with this over time and had tried various approaches. As a result, we created an implementation of the &lt;a href="https://questdb.io/docs/reference/function/window/#first_value" rel="noopener noreferrer"&gt;&lt;code&gt;first_value()&lt;/code&gt;&lt;/a&gt; window function which easily solves the underlying issues with these types of visualizations. This article explains how it's applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simple approach
&lt;/h2&gt;

&lt;p&gt;Unfortunately … does not work. If we were to create a chart with the prices of ETH-USD and BTC-USD, then we would end up with something like this…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;trades&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__timeFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ETH-USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'BTC-USD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgndknzbii2wc6dxrv635.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgndknzbii2wc6dxrv635.png" alt="A price chart, one line very high up, one very low down. It's clear there's some volatile action on a very granular scale, but the lines are virtually flat across the X axis."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chart uses the partition by values Grafana transformation to generate two series. As you can see, the price of 'BTC-USD' is roughly 17x greater than 'ETH-USD'. On the same scale, the time series are hard to compare. If we wanted to look at volatility or another moving metric, we'd need lots of patience and a very strong magnifying glass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overriding the axis
&lt;/h2&gt;

&lt;p&gt;One solution to create comparable data is to assign each series to its own axis. We can achieve this with an override on the price ETH-USD series, setting the axis placement property to the right:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwo8b0omsambjqnyemp79.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwo8b0omsambjqnyemp79.png" alt="An improvement, the two lines are overlapped over each other and through two axis cover two very different time ranges. We can compare them now, but is it accurate?"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it makes it easier to see how each series is moving, we are missing any sense of scale because both axes scales remain independent. As a result, the two series do not start at the same point. It is hard to tell if one is moving relatively more than the other in a given direction. In addition, this can quickly become messy if we need to compare more than two series.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overkill
&lt;/h2&gt;

&lt;p&gt;Before window functions, our community member found a trick to achieve what they wanted. It consisted of using sub-queries to do the following…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get the first value of the series for each symbol first&lt;/li&gt;
&lt;li&gt;Get all the values for each symbol for the time period current&lt;/li&gt;
&lt;li&gt;Cross join both of the above based on symbol&lt;/li&gt;
&lt;li&gt;Calculate the percentage change for all symbols, for example &lt;code&gt;current/first * 100&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While this achieved the desired results, it was a heavy query to write and run due to the heavy joins and multiple sub-queries. Over large time periods and with many symbols, this could result in too many data points for Grafana dashboards and thus necessitate the use of &lt;a href="https://questdb.io/docs/reference/sql/sample-by/" rel="noopener noreferrer"&gt;&lt;code&gt;SAMPLE BY&lt;/code&gt;&lt;/a&gt; to further reduce its time frame. While adequate, it further complicates the query. We can do better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first_value() window function
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;first_value()&lt;/code&gt; window function returns the first value of a metric over a time window. With it, we can do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Easily access the value as of the first timestamp of the series&lt;/li&gt;
&lt;li&gt;Use the resultant value as a normalizing factor to compare the evolution of the series
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
      &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="n"&gt;trades&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__timeFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ETH-USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'BTC-USD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;first_value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;perf&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;series&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbd3nagbbzqyxufx6uo2c.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbd3nagbbzqyxufx6uo2c.png" alt="A price comparison across two appropriately scoped ranges. The data diverge and tell a different story than the former graph."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our new approach revealed that our earlier twin-Y axis chart was misleading. It showed us that ETH and BTC moved up by roughly the same relative amount. However, with the normalized chart above we can see everything in the same scale. ETH significantly outperformed BTC over the time interval!&lt;/p&gt;

&lt;p&gt;This sort of visualization is quite powerful as it can synthesize market activity across many instruments in a more simple chart. But we can do even better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
      &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="n"&gt;trades&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;__timeFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;first_value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;perf&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="k"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's unpack the differences.&lt;/p&gt;

&lt;p&gt;In the prior query, the expression named &lt;code&gt;series&lt;/code&gt; filters the &lt;code&gt;trades&lt;/code&gt; table to include only rows where the symbol is either 'ETH-USD' or 'BTC-USD' and the timestamp satisfies the condition specified by &lt;code&gt;$__timeFilter(timestamp)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In our new query, the expression named data filters the trades table to include only rows where the timestamp satisfies the condition specified by &lt;code&gt;$__timeFilter(timestamp)&lt;/code&gt;. It does not filter based on the symbol.&lt;/p&gt;

&lt;p&gt;The prior query will only calculate and return performance (&lt;code&gt;perf&lt;/code&gt;) for 'ETH-USD' and 'BTC-USD', while our new query will calculate and return performance (&lt;code&gt;perf&lt;/code&gt;) for all symbols in the trades table that satisfy the time filter condition.&lt;/p&gt;

&lt;p&gt;Now, we can configure Grafana as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legend mode = &lt;code&gt;Table&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Legend placement = &lt;code&gt;Right&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Legend values = &lt;code&gt;Last&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We then end up with the following chart summarizing how each crypto pair performed in relative terms in the last few hours:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3borfc0iiz8dfgt1jnc.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3borfc0iiz8dfgt1jnc.png" alt="A dizzying array of price pairs and movements. It is a strong example of how many assets can be correlated across various ranges."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chart makes it pretty apparent that SOL pairs are up, while &lt;code&gt;MATIC&lt;/code&gt; and &lt;code&gt;DOT&lt;/code&gt; are strongly down. While it's easy to get this by computing the ratio of first and last prices over the interval, having a full time series helps understand what's going on with higher fidelity. We can ask: Did the pair jump up quickly, did it trend upwards, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Window functions greatly simplify query complexity and performance. Perhaps most important: It leads to higher quality analysis. While we've made progress in making it easier to compare values across time, our approach still requires a sub-query. QuestDB is considering a separate function such as &lt;code&gt;normalised_value(field, base)&lt;/code&gt; where base is the scale. For example: Does the series start at 100, at 1, or somewhere else.&lt;/p&gt;

&lt;p&gt;If you're interested in that functionality or have any other feedback, please drop by our &lt;a href="https://github.com/questdb/questdb/" rel="noopener noreferrer"&gt;open source repository&lt;/a&gt; or &lt;a href="https://slack.questdb.io/" rel="noopener noreferrer"&gt;community Slack&lt;/a&gt; and let us know.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Solving duplicate data with performant deduplication</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Wed, 22 Nov 2023 17:56:40 +0000</pubDate>
      <link>https://dev.to/questdb/solving-duplicate-data-with-performant-deduplication-4afd</link>
      <guid>https://dev.to/questdb/solving-duplicate-data-with-performant-deduplication-4afd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;QuestDB is a high-performance time series database with SQL analytics that can help you overcome ingestion speed bottle necks. It's &lt;a href="https://github.com/questdb/questdb" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and integrates well with tools and languages you use. Check us out!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;It's a mad, mad, mad, mad world...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your plane lands and the cabin crew announces: "You may now use your electronic devices.” You switch your phone on and a few seconds later a text message welcomes you to the network and informs you of the (outrageous) service rates. Minutes later, you get exactly the same message. Double-y outrageous! What just happened? Well, you've been "at-least-once'd”.&lt;/p&gt;

&lt;p&gt;The same thing can happen tens of thousands of times when you ingest time series, analytic or event data. Duplicate data is a pain. It wastes compute and storage resources, slows down ingestion times and distorts the accuracy of your data sets. Wouldn't it be better if “at-least-once” was “exactly once?”&lt;/p&gt;

&lt;p&gt;In this article, we'll look at data deduplication and compare the performance impact of data deduplication across Timescale, Clickhouse and QuestDB.&lt;/p&gt;

&lt;h1&gt;
  
  
  Performance details for the curious mind
&lt;/h1&gt;

&lt;p&gt;Before we get into more details about deduplication, including the approach we took with QuestDB, let's start with the data. Of course, we expect deduplication to degrade performance somewhat. How much will depend on the number of &lt;code&gt;UPSERT Keys&lt;/code&gt; and on the number of conflicts. But how much?&lt;/p&gt;

&lt;p&gt;To demonstrate, we'll run an experiment.&lt;/p&gt;

&lt;p&gt;Our goal is to evaluate the performance impact when ingesting a dataset twice.&lt;/p&gt;

&lt;p&gt;Our methodology is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ingest a dataset once&lt;/li&gt;
&lt;li&gt;Re-ingest the same dataset again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a pretty ugly scenario, as every row means a conflict. To make things even more interesting, we will ingest the datasets in parallel. This will force the databases to not only deal with duplicates, but also with out-of-order data. The processing of out-of-order data — while valuable for customers ingesting large data volumes — is more challenging to process.&lt;/p&gt;

&lt;p&gt;To make sure performance degradation is within industry expectations, we will run the experiment against two very popular and excellent databases that we usually meet in real-time or time-series use cases: Clickhouse and Timescale.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This experiment is not intended to be a rigorous benchmark. The goal is to check relative performance of QuestDB when handling duplicate data. While we are quite knowledgeable of QuestDB, we're average at operating Timescale and Clickhouse. As such, the experiment applies default, out-of-the-box configurations for each database engine, with no alterations. Fine-tuning would certainly generate different results. We welcome you to perform your own tests and share the benchmarks!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This test will run on an AWS EC2 instance: m6a.4xlarge, 16 CPUs, 64 Gigs of RAM, GP3 EBS volume. We will ingest 15 uncompressed CSV files, each containing 12,614,400 rows, for a total of 189,216,000 rows representing 12 years of hourly data.&lt;/p&gt;

&lt;p&gt;The data represents synthetic e-commerce statistics, with one hourly entry per country (ES, DE, FR, IT, and UK) and category (WOMEN, MEN, KIDS, HOME, KITCHEN). It will be ingested into five tables (one per country) with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="s1"&gt;'ecommerce_sample_test_DE'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="n"&gt;SYMBOL&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="k"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="n"&gt;SYMBOL&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="k"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;unique_visitors&lt;/span&gt; &lt;span class="n"&gt;LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;avg_unit_price&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt; &lt;span class="n"&gt;WAL&lt;/span&gt; &lt;span class="n"&gt;DEDUP&lt;/span&gt; &lt;span class="n"&gt;UPSERT&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The total size of the raw CSVs is about 17GB, and we are reading from a RAM disk to minimize the impact of reading the files. We will thus be reading/parsing/ingesting from up to 8 files in parallel. The experiment's scripts are written in Python, so we could optimize ingestion by reducing CSV parsing time using a different programming language. But remember: this is not a rigorous benchmark.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our data set: &lt;a href="https://mega.nz/folder/A1BjnSYQ#NQe5qhYLVBqiRwhWRmcVtg" rel="noopener noreferrer"&gt;https://mega.nz/folder/A1BjnSYQ#NQe5qhYLVBqiRwhWRmcVtg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Our scripts: &lt;a href="https://github.com/javier/deduplication-stats-questdb" rel="noopener noreferrer"&gt;https://github.com/javier/deduplication-stats-questdb&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How did it turn out?&lt;/p&gt;

&lt;p&gt;As far as raw numbers, the table demonstrates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Storage engine&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Ingest without dedupe&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Ingest with dedupe&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency increase (lower is better)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timescale&lt;/td&gt;
&lt;td&gt;13m23s&lt;/td&gt;
&lt;td&gt;15m23s&lt;/td&gt;
&lt;td&gt;+15.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clickhouse&lt;/td&gt;
&lt;td&gt;3m24s&lt;/td&gt;
&lt;td&gt;4m36&lt;/td&gt;
&lt;td&gt;+17.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QuestDB&lt;/td&gt;
&lt;td&gt;2m12s&lt;/td&gt;
&lt;td&gt;2m23s&lt;/td&gt;
&lt;td&gt;+8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To get these results, the ingestion script was run multiple times for every database. The execution numbers below are those of the best runs. In any case, variability was quite low when re-running several times. Looks good! But the raw numbers don't tell the full story.&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-01%2Fingest-with-dedup-chart.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-01%2Fingest-with-dedup-chart.webp" alt="Comparison graph, bar style"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Timescale deduplication performance
&lt;/h3&gt;

&lt;p&gt;Timescale took 13 minutes and 23 seconds to ingest the whole dataset for the first time. When re-ingesting for deduplication over the same tables with &lt;code&gt;ON CONFLICT … DO UPDATE&lt;/code&gt;, the process took 15 minutes and 28 seconds.&lt;/p&gt;

&lt;p&gt;As was to be expected due to the underlying PostgreSQL engine, uniqueness is guaranteed at every point. This would be suitable for strong exactly-once semantics. But performance-wise, re-playing the events took 15.5% longer than the initial ingestion. And this was significantly slower than analytics optimized storage engines, Clickhouse and QuestDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clickhouse deduplication performance
&lt;/h3&gt;

&lt;p&gt;Clickhouse took 3 minutes and 54 seconds to ingest the whole dataset for the first time. When re-ingesting over the same tables using the &lt;code&gt;ReplacingMergeTree&lt;/code&gt; engine and defining Primary Keys, the process took 4 minutes and 36 seconds. That's quite awesome.&lt;/p&gt;

&lt;p&gt;However, when we ran a &lt;code&gt;SELECT count(*) FROM ecommerce_sample_test_ES&lt;/code&gt;, the number of rows was 60,543,200 which is exactly double the number of rows within the dataset. The explanation is that Clickhouse applies deduplication in the background after a few minutes have elapsed. Fast, but during that time period we have inaccurate data.&lt;/p&gt;

&lt;p&gt;If we want to generate the real number of unique rows, we can add the &lt;code&gt;FINAL&lt;/code&gt; keyword to the query. But in that case the query slows down. For example, a simple count went from 0.002 seconds to 0.337 seconds. Doing so is also not recommended by Clickhouse themselves. This makes it tricky to get to a strong exactly-one semantic. Performance-wise, re-playing the events took 17.9% longer than the initial ingestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  QuestDB deduplication performance
&lt;/h3&gt;

&lt;p&gt;QuestDB took 2 minutes and 12 seconds to ingest the whole dataset for the first time. When re-ingesting over the same tables using &lt;code&gt;DEDUP … USERT KEYS&lt;/code&gt;, the process took 2 minutes and 23 seconds. No duplicates were written, so this would be suitable for strong exactly-once semantics. Performance-wise, re-playing the events took 8.3% longer than the initial ingestion.&lt;/p&gt;

&lt;p&gt;Overall, performance looks good. But let's take a step back and explain what's happening within QuestDB. Briefly, how does it work?&lt;/p&gt;

&lt;p&gt;The process for QuestDB deduplication is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sort the commit data by designated timestamp before &lt;code&gt;INSERT&lt;/code&gt; into the table. This is the same as a normal &lt;code&gt;INSERT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If deduplication uses extra &lt;code&gt;UPSERT Keys&lt;/code&gt;, sort the data by an additional key against the matching timestamp&lt;/li&gt;
&lt;li&gt;Eliminate the duplicates in uncommitted data&lt;/li&gt;
&lt;li&gt;Perform a 2-way sorted merge of the uncommitted data into existing partitions. This is also the same as a normal &lt;code&gt;INSERT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If a particular timestamp value exists in both new and existing data, compare the additional key columns. If it is a full match, take the new row instead of the old one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extra steps are 2, 3, and 5. To perform the comparison, these steps require more CPU processing and disk IO to load the values from the additional columns. If there are no additional &lt;code&gt;UPSERT Keys&lt;/code&gt; or if all the timestamps are unique, then these additional steps add virtually no load.&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-01%2Fdata-deduplication-questdb-tech-diagram.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-01%2Fdata-deduplication-questdb-tech-diagram.webp" alt="QuestDB internals"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Towards exactly once
&lt;/h2&gt;

&lt;p&gt;To solve data deduplication, top databases typically implement one of the following strategies:&lt;/p&gt;

&lt;h3&gt;
  
  
  Unique indexes
&lt;/h3&gt;

&lt;p&gt;Traditional databases like PostgreSQL, PostgreSQL-based systems like Timescale and most relational databases require that you create &lt;a href="https://www.postgresql.org/docs/current/indexes-unique.html" rel="noopener noreferrer"&gt;Unique indexes&lt;/a&gt;. Unique indexes thus provide two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Throw an error when a duplicate is encountered&lt;/li&gt;
&lt;li&gt;Use an &lt;code&gt;UPSERT&lt;/code&gt; strategy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;code&gt;UPSERT&lt;/code&gt;, non-duplicate rows are inserted as usual. But rows where the unique keys already exist will be considered updates that store the most recent values for the non-unique columns. The table still doesn't hold duplicates, but it does allow for corrections or updates.&lt;/p&gt;

&lt;p&gt;Since indexes are involved, deduplication has an impact on performance. For the typical use case of a relational database, when the read/write ratio is heavily biased for reads, this impact might be negligible. But with write-heavy systems such as those seen within analytics, performance can noticeably degrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compacted tables
&lt;/h3&gt;

&lt;p&gt;Analytical databases such as Clickhouse prioritize ingestion performance. As a result, they will accept duplicate values. At a later point in time they will&lt;br&gt;
then compact the tables in the background, keeping only the latest version of a row. This means that for an indeterminate time, duplicates will be present on your tables. This is probably not ideal for data accuracy and efficiency.&lt;/p&gt;

&lt;p&gt;To resolve this, developers can add the &lt;code&gt;FINAL&lt;/code&gt; keyword to their queries as a workaround. The &lt;code&gt;FINAL&lt;/code&gt; keyword will prevent duplicates from appearing within query results. But that makes queries slower. Clickhouse recommends that people avoid this method. That means that the recommended, performant path risks duplicates making it into your data set and thus into your queries and dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Out-of-database deduplication
&lt;/h3&gt;

&lt;p&gt;The third strategy is to move deduplication handling outside of the database itself. In this case, the deduplication logic is applied right before ingestion, as a connector running on &lt;a href="https://questdb.io/docs/third-party-tools/kafka/overview/" rel="noopener noreferrer"&gt;Kafka Connect&lt;/a&gt;, &lt;a href="https://questdb.io/docs/third-party-tools/flink/" rel="noopener noreferrer"&gt;Apache Flink&lt;/a&gt;, &lt;a href="https://questdb.io/docs/third-party-tools/spark/" rel="noopener noreferrer"&gt;Apache Spark&lt;/a&gt;, or another similar stream processing component. This solution can be flimsy. Data ingested outside of the connector or outside a particular time-range might result in duplicates. Until recently, this was the only approach available in QuestDB.&lt;/p&gt;

&lt;h1&gt;
  
  
  Adding native data deduplication to QuestDB
&lt;/h1&gt;

&lt;p&gt;While delegating deduplication to an external component was convenient for the QuestDB team, it was not ideal for customers. Customers had to manage an extra component so that all ingestion occurred within the deduplication pipeline. In many cases, external deduplication was only best-effort and didn't hit our high quality standards.&lt;/p&gt;

&lt;p&gt;To address this, the team implemented native data deduplication with four goals in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Guaranteed with exactly-once-semantics&lt;/li&gt;
&lt;li&gt;Works as &lt;code&gt;UPSERT&lt;/code&gt;. Must load then discard exact duplicates, and update the metric columns when a new version of a record appears; make re-ingestion idempotent&lt;/li&gt;
&lt;li&gt;Impact on performance must be negligible, so deduplication can be always-on&lt;/li&gt;
&lt;li&gt;Simple to activate/deactivate and transparent for developers and tools that select data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After a few months of hard work, the &lt;code&gt;DEDUP&lt;/code&gt; keyword was released as part of &lt;a href="https://github.com/questdb/questdb/releases/tag/7.3" rel="noopener noreferrer"&gt;QuestDB 7.3&lt;/a&gt;. Now a developer can activate/deactivate deduplication either as part of a &lt;code&gt;CREATE TABLE&lt;/code&gt; statement, or any time via &lt;code&gt;ALTER TABLE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;QuestDB is a time-series database. Thus deduplication must include the designated timestamp, plus any number of columns to use as &lt;code&gt;UPSERT Keys&lt;/code&gt;. A table with deduplication enabled is guaranteed to store only one row for each unique combination of designated timestamp + &lt;code&gt;UPSERT Keys&lt;/code&gt;. will then silently update the rest of the columns if a new version of the row is received.&lt;/p&gt;

&lt;p&gt;Deduplication happens at ingestion time, which means you will never see any duplicates when selecting rows from a table. This gives you a strong exactly-once guarantee. The best part is that QuestDB will do all of that with very low impact on ingestion performance.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion, only a mad world
&lt;/h1&gt;

&lt;p&gt;If you need ingestion idempotence and exactly-once semantics for your database, QuestDB gives you deduplication with very low impact on performance. As a result, you can enjoy strong performance with deduplication always enabled. When compared to other excellent database engines, QuestDB strikes a very good balance between performance, strong guarantees and usability.&lt;/p&gt;

&lt;p&gt;To learn more about deduplication and see how you can apply it in QuestDB, checkout our &lt;a href="https://questdb.io/docs/concept/deduplication/" rel="noopener noreferrer"&gt;data deduplication documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To learn more about deduplication and see how you can apply it in QuestDB,&lt;br&gt;
checkout our &lt;a href="https://questdb.io/docs/concept/deduplication/" rel="noopener noreferrer"&gt;data deduplication documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Is all data time-series data?</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Wed, 22 Nov 2023 17:33:12 +0000</pubDate>
      <link>https://dev.to/questdb/is-all-data-time-series-data-ing</link>
      <guid>https://dev.to/questdb/is-all-data-time-series-data-ing</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://questdb.io/" rel="noopener noreferrer"&gt;QuestDB&lt;/a&gt; is an open source, high performance time series database. With its massive ingestion throughput speeds and cost effective operation, QuestDB reduces infrastructure costs and helps you overcome tricky ingestion bottlenecks. Thanks for reading!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Time-series data is everywhere. It's the fastest growing new data type. But where is it all coming from? And isn't &lt;em&gt;all&lt;/em&gt; data essentially series data? When does data ever exist outside of time? This article investigates the source of all this new time bound data, and explains why more and more of it will keep coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it all time series?
&lt;/h2&gt;

&lt;p&gt;The quick and clean answer is no, not all data is time-series data. There are &lt;em&gt;many&lt;/em&gt; data types and most do not relate to time. To demonstrate, we'll pick a common non-time-series data type: Relational data.&lt;/p&gt;

&lt;p&gt;Relational data is organized into tables and linked to other data through common attributes. An example would be a database of dogs, their breed, and whether or not they are well-behaved. This data is relational, categorical and cross-sectional. It's a snapshot of a group of entities with no relationship to "when". In this case, these entities are dogs. Adorable! But not time-series data.&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Frelational-dogs.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Frelational-dogs.webp" alt="Dogs outside of time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, time is not relevant to the name, breed and behavioural tendencies of our dogs. It does not matter when the dog was added to the table, or when any of its values changed. The entities are held in a timeless vacuum.&lt;/p&gt;

&lt;p&gt;By contrast, time-series data is indexed in accordance with time, which is linear and synchronized. It consists of a sequence of data points, each associated with a timestamp. An example would be if our database of dogs included the dog's name, their breed, and their time in the local dog race:&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Ftimed-dogs.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Ftimed-dogs.webp" alt="Dogs in time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This table contains a timestamp and so now contains time-series data. But this data is not &lt;em&gt;&lt;strong&gt;the&lt;/strong&gt;&lt;/em&gt; time-series data that requires specific features or a specialized time-series database. To require a specialized database, data must also match a specific set of demand and usage patterns. For now, it is simply time-series data in a &lt;em&gt;transactional, relational database&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  In each action, a wake of time
&lt;/h2&gt;

&lt;p&gt;When does it cross into that threshold? For this example, we will put our table of dogs into a practical light. Consider that a group of people input dog information into a 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Fmagical-relational-ingest.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Fmagical-relational-ingest.webp" alt="People put data into a database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simple! Now take one more practical step. How is the database accessed? A small team of people each login to a front-end data-entry application. For security purposes, an authentication server sits before the web application. A person authenticates, and then their session is kept alive for 24 hours:&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Finsert-with-auth.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Finsert-with-auth.webp" alt="Securing and accessing a database"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The authentication server needs to know exactly when a person logged in to determine when to invalidate their session. This requires a timestamp column. &lt;/p&gt;

&lt;p&gt;The security provider that handles login may receive tens of thousands of requests every second. Tracking each attempt in chronological order and revoking sessions with precision is an intense demand.&lt;/p&gt;

&lt;p&gt;This is a key point. As above, the presence of time-series data didn't mean much to our small-scale transactional, relational database. But now we've got a flow of time-series data. And with it our requirements change.&lt;/p&gt;

&lt;p&gt;And so deeper we go...&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Finsert-with-more.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Finsert-with-more.webp" alt="More time-series data!"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The database is hosted somewhere, perhaps &lt;em&gt;in the Cloud&lt;/em&gt;. Cloud billing is based on compute time. The matching front-end application collects performance metrics. This is all novel time-series data which contains essential insights from which business logic is written.&lt;/p&gt;

&lt;p&gt;We can go deeper still and consider the entire chain. DNS queries hit DNS resolvers and hold a Time-To-Live value for DNS propagation. A Content Delivery Network before the front-end application gives precise detail on when something was accessed and how long static assets will need to live within the cache. Just one transactional update to the relational database generates a wake of essential time-bound data, for security and analysis.&lt;/p&gt;

&lt;p&gt;Thus we retreat from our dive and head into the next section with the following crystallized takeaway: Not all data is time-series data, but time-series data is generated via virtually any operation, including the creation, curation, and&lt;br&gt;
analysis of “non-time-series data” in “non-time-series databases”.&lt;/p&gt;

&lt;h2&gt;
  
  
  OLAP vs OLTP: Process types
&lt;/h2&gt;

&lt;p&gt;This cascading relationship between transactions and analysis is best explained via a comparison of two common data processing techniques: Online Transaction Processing (OLTP) and Online Analytical Processing (OLAP).&lt;/p&gt;

&lt;h3&gt;
  
  
  Online Transaction Processing (OLTP)
&lt;/h3&gt;

&lt;p&gt;OLTP systems prioritize fast queries and data integrity in multi-access environments, where many people make updates at once. They are designed to handle a large number of short, atomic transactions and are optimized for transactional consistency. OTLP systems tend to perform operational tasks, such as creating, reading, updating, and deleting operations — the basic CRUD operations in a database.&lt;/p&gt;

&lt;p&gt;Most OLTP systems use a relational database, where data is organized into normalized tables. Our dog database is OLTP. Another example would be an ecommerce system where customers purchase from an inventory. Many shoppers purchase many shoes from an accurate, single source of inventory. Each transaction is atomic, and the system handles a large number of concurrent&lt;br&gt;
transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Online Analytical Processing (OLAP)
&lt;/h3&gt;

&lt;p&gt;OLAP systems prioritize the fast computation of complex queries and often aggregate large amounts of data. They optimize for query and ingest speed over transactional consistency. They tend to be fast, powerful and flexible.&lt;/p&gt;

&lt;p&gt;Our dog race database &lt;strong&gt;is not&lt;/strong&gt; an example of an OLAP system. Even though it applies time-series data, the race times are recorded and then entered into the database, making it more transactional and thus better suited for OLTP. In other words: the presence of time-series data does not automatically mean it's an OLAP use case.&lt;/p&gt;

&lt;p&gt;However, the example infrastructure above introduces time-series elements where the performance optimizations found in OLAP systems become essential. On the way to one transactional operation, thousands of time-bound data points may be collected. Given how much time-series data is created, ingest and analysis requires the mightier performance profile of a purpose-built analytical system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better with both
&lt;/h2&gt;

&lt;p&gt;OLAP and OLTP are not at odds. They are complementary. Both OLTP and OLAP systems apply time-series data. But due to their respective strengths, time-series specialized databases excel at OLAP while relational databases with their transactional guarantees are a more natural fit for OLTP. At scale, you will see both systems work together.&lt;/p&gt;

&lt;p&gt;Consider ecommerce once more. The ratio of &lt;em&gt;browsers&lt;/em&gt; to &lt;em&gt;buyers&lt;/em&gt; skews very heavily towards &lt;em&gt;browsers&lt;/em&gt;. We all window shop from time to time, but a much smaller percentage of us complete a purchase. Every completed purchase requires a reliable way to update the inventory, which is a task for OLTP.&lt;/p&gt;

&lt;p&gt;But every &lt;em&gt;browser&lt;/em&gt; generates waves of data with their clicks, navigation and interactions: behavioural, statistical, and so on, which can be leveraged to increase the likelihood a person will complete a purchase. Time-series data is created in abundance in the &lt;em&gt;browsers&lt;/em&gt; case.&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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Fmore-browsers-than-buyers.webp" 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%2Fquestdb.io%2Fimg%2Fblog%2F2023-11-15%2Fmore-browsers-than-buyers.webp" alt="# of Browsers &amp;gt; # Buyers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;QuestDB is a specialized time-series database. It excels at OLAP cases, and supplements OLTP cases. Consider a traditional &lt;a href="https://dev.to/glossary/relational-database/"&gt;relational database&lt;/a&gt; like PostgreSQL. In it we may store stock exchange data, such as the current price of a stock for tens of thousands of companies. With the sheer number of stocks and transactions between them, a time-series database is needed beside it to record and analyze its history.&lt;/p&gt;

&lt;p&gt;For example, whenever a stock price changes, an application will update a queue which then updates a PostgreSQL table. The &lt;code&gt;UPDATE/INSERT&lt;/code&gt; event is then sent to something like &lt;a href="https://dev.to/docs/third-party-tools/kafka/overview/"&gt;Apache Kafka&lt;/a&gt;, which reads the events, and then inserts them into a time-series database.&lt;/p&gt;

&lt;p&gt;The relational store, which provides the transactional guarantee, maintains the stock prices and the present "state". The time-series database then keeps the history of changes, visualizing trends via a dashboard with computed averages and a chart showing the volume of changes. The relational database may process and hold &lt;code&gt;100,000&lt;/code&gt; rows at any time, while the time-series database may process and hold &lt;code&gt;100,000 * seconds&lt;/code&gt; rows for present and future analysis.&lt;/p&gt;

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

&lt;p&gt;Not all data is time-series data. But creating and accessing any online data generates time-series data in waves. To respond to this demand, time-series databases like QuestDB and others have arrived to handle the &lt;em&gt;wake of data&lt;/em&gt; left in the exchange of high-integrity transactions and high-volume operations.&lt;/p&gt;

&lt;p&gt;While time-series data is found in both OLAP and OTLP systems, a specialized performance and feature profile is required to handle the significant historical and temporal data that is generated by even basic functions in modern day applications. Time-series databases excel with these demands, while not being confined strictly to OLAP use cases.&lt;/p&gt;

&lt;p&gt;Interesting in a high performance time-series database? Checkout &lt;a href="https://cloud.questdb.com/signup?utm_source=DevTo&amp;amp;utm_content=Blog&amp;amp;utm_campaign=ReleaseWeek4" rel="noopener noreferrer"&gt;QuestDB Cloud&lt;/a&gt; and get started in minutes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Time-series IoT tracker using QuestDB, Node.js, and Grafana</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Fri, 06 Oct 2023 19:54:49 +0000</pubDate>
      <link>https://dev.to/questdb/time-series-iot-tracker-using-questdb-nodejs-and-grafana-51ii</link>
      <guid>https://dev.to/questdb/time-series-iot-tracker-using-questdb-nodejs-and-grafana-51ii</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;QuestDB is a high performance, open source time-series database that breaks through traditional ingest bottlenecks. This tutorial can help you get started! Like what you see? &lt;a href="https://github/questdb/questdb"&gt;Star us on GitHub&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Time-series data is all around us. We rely on financial tick data to make monetary decisions. Application services track web engagement and deep system metrics. Even data with no obvious relationship to time is often bound to time-based metadata. In almost every aspect of our connected lives, time leads a stream of data, from GPS and geolocation to health monitors and much more.&lt;/p&gt;

&lt;p&gt;Managing time-series data - especially at large volumes - is challenging with traditional tools. Why? Time-series data needs precise chronological order. The key insight within a data set is seldom an individual data point, but rather the patterns seen once data is down-sampled or aggregated. Without chronological order, insights from these patterns are lost.&lt;/p&gt;

&lt;p&gt;Since traditional tools were not designed to handle order at very high ingest volume, we see performance challenges at scale. In recent years, we’ve seen a rise of &lt;a href="https://questdb.io/glossary/time-series-database/"&gt;time-series databases&lt;/a&gt; that are better optimized for these workloads.&lt;/p&gt;

&lt;p&gt;In this tutorial, we’ll simulate a busy, real-time IoT tracker and see how QuestDB, a fast time-series database, can ingest and analyze that data efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-series data and IoT
&lt;/h2&gt;

&lt;p&gt;This project will have three main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js IoT simulator to generate some fake data to QuestDB&lt;/li&gt;
&lt;li&gt;QuestDB to store that data&lt;/li&gt;
&lt;li&gt;Grafana to visualize the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can easily replace the IoT simulator with real-data sources from managed services like AWS IoT Core or Azure IoT. But we want to show off ingestion and query performance, so we’re using simulated data for convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en"&gt;Node.js 18+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/get-docker/"&gt;Docker &amp;amp; Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  IoT simulator setup
&lt;/h2&gt;

&lt;p&gt;First, create a new Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir questdb-iot &amp;amp;&amp;amp; npm init -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the &lt;a href="https://github.com/questdb/nodejs-questdb-client"&gt;QuestDB Node.js client&lt;/a&gt; and &lt;a href="https://chancejs.com/usage/node.html"&gt;&lt;code&gt;chance&lt;/code&gt;&lt;/a&gt; library to generate fake data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @questdb/nodejs-client chance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The QuestDB Node.js client applies the InfluxDB Line Protocol (ILP). QuestDB also supports the PostgreSQL wire protocol if you would rather use the PostgreSQL client libraries. For those not familiar with the InfluxDB Line Protocol, it is an efficient, text-based protocol for sending time-series data&lt;br&gt;
points in a concise manner. It compactly sends timestamp, measurement value, as well as other metadata. For more information, check out the &lt;a href="https://dev.to/docs/reference/api/ilp/overview/"&gt;InfluxDB Line Protocol reference guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The simulator code is a variation of the &lt;a href="https://github.com/questdb/questdb-quickstart/blob/main/ingestion/python/tsbs_send/ilp_ingestion.py"&gt;Python Quickstart example&lt;/a&gt;. In the code, we loop over 10,000 devices, generate a random &lt;code&gt;deviceId&lt;/code&gt;, &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;, and &lt;code&gt;temperature&lt;/code&gt; values along with a random &lt;code&gt;deviceVersion&lt;/code&gt; within &lt;code&gt;v1.0&lt;/code&gt;, &lt;code&gt;v1.1&lt;/code&gt;, and &lt;code&gt;v2.0&lt;/code&gt;. We can imagine this as a sort of sensor, perhaps a bursty one in a satellite orbiting the earth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Sender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@questdb/nodejs-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Chance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// create a sender with a 4k buffer&lt;/span&gt;
  &lt;span class="c1"&gt;// it is important to size the buffer correctly so messages can fit&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Sender&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// connect to QuestDB&lt;/span&gt;
  &lt;span class="c1"&gt;// host and port are required in connect options&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9009&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// initialize random generator lib&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Chance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// device types&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deviceVersions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;// loop over devices&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;deviceId&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="nx"&gt;deviceId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;deviceVersions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;deviceVersions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="nx"&gt;sender&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deviceVersions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;randomVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deviceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floatColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floatColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floatColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;temp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floating&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;atNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// flush the buffer of the sender, sending the data to QuestDB&lt;/span&gt;
    &lt;span class="c1"&gt;// the buffer is cleared after the data is sent and the sender is ready to accept new data&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// close the connection after all rows ingested&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we are using the InfluxDB Line Protocol, there is no need to predefine the schema on the database before sending the data. If a table does not exist, it will be created. This is useful for bootstrapping a quick project and tinkering around.&lt;/p&gt;

&lt;h2&gt;
  
  
  QuestDB and Grafana setup
&lt;/h2&gt;

&lt;p&gt;For convenience, we will use Docker Compose to bootstrap QuestDB and Grafana.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file and paste the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;questdb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;questdb/questdb:7.3.1&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;questdb&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;questdb&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9009:9009"&lt;/span&gt;

    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QDB_PG_READONLY_USER_ENABLED=true&lt;/span&gt;

  &lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/grafana-oss:10.1.0&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts up QuestDB and Grafana in a Docker network and creates a read-only user for Grafana to pull data from QuestDB.&lt;/p&gt;

&lt;p&gt;Next, start up the containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have port &lt;code&gt;9000&lt;/code&gt; (QuestDB UI), &lt;code&gt;9009&lt;/code&gt; (InfluxDB Line Protocol), and &lt;code&gt;3000&lt;/code&gt; (Grafana UI) mapped to &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ingesting data
&lt;/h2&gt;

&lt;p&gt;Now to send data.&lt;/p&gt;

&lt;p&gt;Start the simulator!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a specialized time series database, QuestDB has a better handle on the volume and the &lt;a href="https://dev.to/glossary/high-cardinality/"&gt;cardinality&lt;/a&gt; of the incoming time-series data compared to a traditional database, or to alternative time-series databases. It also provides features like &lt;a href="https://dev.to/docs/concept/deduplication/"&gt;data deduplication&lt;/a&gt; and &lt;a href="https://dev.to/blog/2021/05/10/questdb-release-6-0-tsbs-benchmark/#the-problem-with-out-of-order-data"&gt;out-of-order (O3)&lt;/a&gt; indexing which are essential with high amounts of incoming data.&lt;/p&gt;

&lt;p&gt;Neat, fast. However, 10,000 "devices" reporting is not &lt;em&gt;that many&lt;/em&gt; in the time-series world. Let's crank up our sample data, thus increasing both the overall burst and the cardinality of one of our columns. For this, we will run&lt;br&gt;
the script multiple times and make our loop more flavourful.&lt;/p&gt;

&lt;p&gt;We don't want to cook our computers, but we can get creative. For a more robust test scenario, we will raise the number of "devices" to, say, a million, and add a very highly random column full of values between negative million and a million:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;deviceId&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="nx"&gt;deviceId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floatColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;curio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And maybe we want to run it a couple times, for good measure?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run it, navigate to &lt;code&gt;localhost:9000&lt;/code&gt;, and we should see our data arrive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S2ESPIl1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fu6jr3e1it5ng92lpaav.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S2ESPIl1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fu6jr3e1it5ng92lpaav.png" alt="An image showing ingest speed -- 2,000,000 rows in 17ms!" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A broad &lt;code&gt;SELECT&lt;/code&gt; query returns fast, even when Docker-ized. Just like that, we have nice table view of all of our simulated devices.&lt;/p&gt;

&lt;p&gt;Measuring database performance depends on many factors, like hardware, the schema, overall infrastructure and so on. However, we can still get a sense of how a specialized time-series database handles bursting data, even in a local case.&lt;/p&gt;

&lt;p&gt;But speedy ingest is not all we're after. We also want to query and visualize our data. Let's start with the built-in Chart view and see the temperature of all devices with &lt;code&gt;v2.0&lt;/code&gt; over a period of time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TqWFN78C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/byr04pxjlzbawczstzcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TqWFN78C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/byr04pxjlzbawczstzcr.png" alt="A chart view in the QuestDB web console" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to Grafana
&lt;/h2&gt;

&lt;p&gt;Instead of making charts directly within our database, we will offload visualization to a specialized layer for easier access. For this, we'll use Grafana.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;localhost:3000&lt;/code&gt; and use the default credentials to login: &lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;admin&lt;/code&gt;. Remember, if you are creating something in production, change the default user and password values!&lt;/p&gt;

&lt;p&gt;Click on “Add data source” and choose the PostgreSQL type. Since we’re running this within the same Docker network, we can use the container name for the host and specify QuestDB’s PostgreSQL wire port &lt;code&gt;8812&lt;/code&gt; (i.e., &lt;code&gt;questdb:8812&lt;/code&gt;). The read-only user credentials are &lt;code&gt;user&lt;/code&gt;/&lt;code&gt;quest&lt;/code&gt;, with the default database as &lt;code&gt;qdb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cX40Y4r2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpx4gnfgt9b20obc6anl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cX40Y4r2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpx4gnfgt9b20obc6anl.png" alt="Grafana UI -- entering SQL credentials as above described" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After connecting, click on “Explore data”.&lt;/p&gt;

&lt;p&gt;Use the native SQL query to visualize our dataset:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DOfVAox9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ueo6bncnqxeygne58ti.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DOfVAox9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ueo6bncnqxeygne58ti.png" alt="An early visualization in Grafana, fairly busy" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now build out panels using built-in Grafana types. For example, we can use the time-series visualization:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gg41HSmm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljawj2sp9krzbr5qpvxb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gg41HSmm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljawj2sp9krzbr5qpvxb.png" alt="A more organized view of the data using threshold lines" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This adds threshold lines, and zooms in so we can see the different temperatures. From here, there are many ways we can alter the visual acuity to make the tracked values clear and easy to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we used a random IoT device data generator to show how to ingest bursting data into QuestDB. We then used Grafana to visualize the data. Underneath, we used the &lt;a href="https://github.com/questdb/nodejs-questdb-client"&gt;QuestDB Node.js Client library&lt;/a&gt; to utilize the InfluxDB Line Protocol for concise data transfer. Once the data hit QuestDB, we saw how fast it was to analyze them and also to create simple visualizations in the Web Console. Finally, we connected Grafana to the QuestDB PostgreSQL endpoint to build more complex panels.&lt;/p&gt;

&lt;p&gt;In a real-world scenario, the data source may be coming directly from the devices, over a managed IoT connection service like AWS IoT Core, Azure IoT, or from a third-party. In that case, we can modify our simulator to listen to those events and use the same client library to send data. We may elect to also queue and batch our calls or use multiple threads to spread out the load. Whichever way we choose, using a specialized time series database like QuestDB prevents us from running into issues as burst and scale increases.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>node</category>
      <category>javascript</category>
      <category>database</category>
    </item>
    <item>
      <title>Our Website Source Is Now Private, A Cautionary Tale</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Fri, 01 Sep 2023 21:30:59 +0000</pubDate>
      <link>https://dev.to/questdb/our-website-source-is-now-private-a-cautionary-tale-3bn1</link>
      <guid>https://dev.to/questdb/our-website-source-is-now-private-a-cautionary-tale-3bn1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;QuestDB is a high-performance time-series database built in Java and C++, with no dependencies and zero garbage collection. Check us out if you have time series data and are looking for high throughput ingestion and fast SQL queries. Not to worry: QuestDB remains &lt;a href="https://github.com/questdb/questdb" rel="noopener noreferrer"&gt;open source&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;"Imitation is the most sincere form of flattery" - Oscar Wilde&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As an open source company that strives to be as transparent as possible — both internally and in the community — we want to have all our code public. We want you to see it all, even for our blogs, docs and marketing pages. But as far as our website goes, recent unfortunate events have made us reconsider our position.&lt;/p&gt;

&lt;p&gt;We'll share what we learned so that it doesn't happen to you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Great artists…
&lt;/h2&gt;

&lt;p&gt;For page traffic, we want to know whose reading what, what is shared — if we know what you like and what is helpful, we can make more of it.&lt;/p&gt;

&lt;p&gt;We're growing, and our traffic remains modest. When something succeeds on Reddit or HackerNews, we feel it. When the charts go up, it's a real thrill.&lt;/p&gt;

&lt;p&gt;During one such event, one of our engineers, Maciej, looked into Google Analytics and noticed something of an... anomaly in our traffic. It had exploded. &lt;/p&gt;

&lt;p&gt;And a large portion of it originated from an uncommon region — Brazil:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3a33giituokyia46wus.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3a33giituokyia46wus.png" alt="158 thousand views from Brazil. Usual highest, US, below with 1400."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hey, neat! Something must have resonated within the Brazilian developer community. But which page? One of our deep technical articles? Must be.&lt;/p&gt;

&lt;p&gt;A few clicks later, we determined that the source of traffic was not what we had hoped. Far from coming from one of our fresh new articles, it was generated by an &lt;em&gt;unknown&lt;/em&gt; page. A page with which none of us had any familiarity. This was because it was, in fact, from a different website entirely:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefz6lrirafm9f3wwpqiw.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefz6lrirafm9f3wwpqiw.png" alt="The offending site, but crossed out."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Uh oh. But was this just one page? Nope, it was many of them.&lt;/p&gt;

&lt;p&gt;Looking deeper, traffic spanned many paths:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy8533r4gdevh38jwmfs.jpg" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy8533r4gdevh38jwmfs.jpg" alt="A list of strange paths that imply... maybe games? Table games?!"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apart from the root domain, these were all unexpected. Not good. But with all this data pointing to unfamiliar pages, we knew exactly where to look for answers. The moment we landed on the strange site, it confirmed what we had all expected.&lt;/p&gt;

&lt;p&gt;While the main landing page and its site paths were altered, the rest our site had been copied over and hosted in full: metadata, supporting pages with trademarks, images, copy, logos and all. Little effort was taken to obscure this fact.&lt;/p&gt;

&lt;p&gt;According to Google Analytics, in a short time we had collected well over 150,000 new visitors to “our site”. But in reality, it was from an entirely different one.&lt;/p&gt;

&lt;p&gt;Well, hey. Traffic is traffic, right? All press is good press? And the theme was open source, isn't that what open source is for? Yes and no, it's not so clear.&lt;/p&gt;

&lt;p&gt;There are things to consider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not flattering
&lt;/h2&gt;

&lt;p&gt;Their post-launch results were staggering. Once live, their pages spread like wildfire. And we know, we have the data to prove it. But the intermingling traffic made us nervous. This is all much different from our usual keywords, demographics and traffic volume. On top of that, it came from an industry that is somewhat of a gray area.&lt;/p&gt;

&lt;p&gt;Could we receive punishment as a result of the clientele of this site? Did we just become flagged as a “toxic website” to the many sites that interlink with our content? Are our search rankings about to plummet?&lt;/p&gt;

&lt;p&gt;Any of that would be very bad. Can we resolve this, and fast? Yes, we can. But before we go any further, how did this even happen?&lt;/p&gt;

&lt;p&gt;We can point to two key reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docusaurus showcase
&lt;/h3&gt;

&lt;p&gt;We use a static site generator called &lt;a href="https://docusaurus.io" rel="noopener noreferrer"&gt;Docusaurus&lt;/a&gt; for our docs, blogs and marketing pages. The design is customized, and tailored for your (and thus our) needs. As such, it felt great for the team when Docusaurus featured the QuestDB.io design within their showcase:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dej3tw0q9zq4bx9928z.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6dej3tw0q9zq4bx9928z.png" alt="QuestDB.io shown in the Docusaurus showcase."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;People looking for inspiration for their new Docusaurus site can use the filters to select “open source”. From there, as expected, a click of the source button leads you to, that's right — all of it, the goods. This very blog and all its surrounding pieces are hosted from within that repository, in its entirety…&lt;/p&gt;

&lt;p&gt;Given the complexity and uniqueness of our Docusaurus rendition, it did not seem likely someone would fork it. There has been minimal work to template it as such. One does not simply swap some config options and colours and arrive at a brand new site. We're also on a fairly dated version of Docusaurus, and we'd expect people to want &lt;em&gt;the new stuff&lt;/em&gt;. But it turns out we were wrong.&lt;/p&gt;

&lt;p&gt;And upon reflection, we get it. The source is open and under a permissive license: Apache 2.0. People are free to do as they will, within the parameters of the license. But one consideration of the license is to respect existing trademarks and product names:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;... This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using the styles, code and layouts is one thing. But using the same logos, trademarks, and so on, is another. And as for their traffic appearing in our Google Analytics, the blame for that lies with us.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remember your env vars
&lt;/h3&gt;

&lt;p&gt;Analytics providers, search engines, and other third parties who provide tokens that are browser exposed, usually provide safe, read only keys. They are most often non-destructive, as they are exposed to the client.&lt;/p&gt;

&lt;p&gt;As many of you know, each property in Google Analytics gets its own GA tag which is then placed in Google's JavaScript snippet. These are visible when you inspect a website's source.&lt;/p&gt;

&lt;p&gt;To prove it, visit a tech site that you like, inspect the source, and search for &lt;code&gt;GTM&lt;/code&gt;. You will most likely find a &lt;code&gt;GTM-XXXXX&lt;/code&gt; value. There is nothing stopping you for using it, except that it won't reveal anything unless you have access to the matching Google Analytics dashboard.&lt;/p&gt;

&lt;p&gt;Though it is somewhat uncomfortable to admit, we soon confirmed that our Google Analytics tag was hard-coded directly in the source:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct0ocpuhruu2dmd9r8ah.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct0ocpuhruu2dmd9r8ah.png" alt="Shows us applying an ENV VAR over the hard coded GTM tag."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This meant that the repository could be cloned or forked, and bam — the new website is a part of QuestDB, as far as Google Analytics is concerned. This is both unfortunate, and preventable. In hindsight, the value could have been hidden behind an environment variable in the actual code. It is now!&lt;/p&gt;

&lt;p&gt;The moral of the story: &lt;strong&gt;set env vars for any sensitive - or somewhat sensitive - variable&lt;/strong&gt;. It is basic advice, yes, but sometimes one forgets just how wild and random the broad internet can be. Even if you think no one would use it or that no damage could be done, set it as an env var anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closed source, for now
&lt;/h2&gt;

&lt;p&gt;Despite reaching out to Google, we were unable to get anyone to provide any help. But it's no matter, we cleaned up and applied a new Google Analytics tag. And it appears that the blip is not interfering with our business in any major way.&lt;/p&gt;

&lt;p&gt;As of yet we have seen no punitive impact to our rankings. But as a precaution, we have decided to make a change to the visibility of our website repository. The website source will now be set to "private".&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27ret068qs5pdeiw49oh.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27ret068qs5pdeiw49oh.png" alt="A trolley problem meme. Do we keep the source open, and assume risk?"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't so we can twirl our moustaches like villains and apply shady marketing practices. It's to protect us so that if we do silly things like forget an env var, we won't risk cratering the hard-earned value of our website property.&lt;/p&gt;

&lt;p&gt;That said, we will work to open parts of it in the future. For example, documentation has received helpful contributions from community members. Closing the door on them doesn't feel right. Luckily, there is a way around that.&lt;/p&gt;

&lt;p&gt;Right now, QuestDB.io is a single build of our Docusaurus repository. Any doc, blog or content changes in the repo generates a new Netlify build. In the future, we can extract doc contents — which exist in their own folder as &lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.mdx&lt;/code&gt; files — and host them in their own open repository.&lt;/p&gt;

&lt;p&gt;Using GitHub Actions or other build runner, we can setup a pipeline like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On PR event, create a temporary workspace&lt;/li&gt;
&lt;li&gt;Pull private repo &amp;amp; docs into workspace&lt;/li&gt;
&lt;li&gt;Build workspace as though whole&lt;/li&gt;
&lt;li&gt;Provide PR preview&lt;/li&gt;
&lt;li&gt;On merge, rsync doc contents to private repo&lt;/li&gt;
&lt;li&gt;On commits to main, rebuild production website&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this model, our documentation can remain open even with  the rest closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;QuestDB is an open source company. We want to work in the open: no secrets! But we've decided to make our website source private for the time being.&lt;/p&gt;

&lt;p&gt;Remember: even if the tag or key or whatever seems benign, use an environment variable. It might save you from a real hassle.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>learning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Concurrent Data-structure Design Walk-Through</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Tue, 22 Aug 2023 21:47:58 +0000</pubDate>
      <link>https://dev.to/questdb/concurrent-data-structure-design-walk-through-1hc1</link>
      <guid>https://dev.to/questdb/concurrent-data-structure-design-walk-through-1hc1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://questdb.io" rel="noopener noreferrer"&gt;QuestDB&lt;/a&gt; is a time-series database that offers fast ingest speeds, InfluxDB Line Protocol and PGWire support and SQL query syntax. QuestDB is composed mostly in Java, and we've learned a lot of difficult and interesting lessons. We're happy to share them with you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Investigating data structures
&lt;/h2&gt;

&lt;p&gt;Concurrent data structure design is hard. This blog offers a guided tour on constructing a special-purpose concurrent map that heavily favors readers. The article will not just present yet another ready-to-use data structure. Instead, I will walk you through the design process while solving a real-world problem. I will even present dead ends that I bumped into along the way. It's a detective story for programmers interested in concurrent programming. &lt;/p&gt;

&lt;p&gt;By the end of the article, we will have a concurrent map for storing blobs of data in native memory. The map is lock-free on a reading path and is also very conservative with memory allocations. Let's get started!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article assumes basics in Java or a Java-like programming language.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I need a concurrent map where keys are strings and values are fixed-size blobs (public cryptographic keys). This could sound like a &lt;a href="https://twitter.com/PeterVeentjer/status/1685999603684872193" rel="noopener noreferrer"&gt;job for the plain old ConcurentHashMap from JDK&lt;/a&gt;, but there's a twist: the blobs must be available outside of the Java heap.&lt;/p&gt;

&lt;p&gt;Why? So that callers can get a pointer to a blob and pass it to Rust code via JNI. The Rust code then uses the public key to verify digital signatures.&lt;/p&gt;

&lt;p&gt;Here's a simplified version of the interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentString2KeyMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;set()&lt;/code&gt; method receives a username and a pointer to a key. The map outlives the pointers it receives, so it must copy the memory under the received pointers into its own buffer. In other words: the &lt;code&gt;get()&lt;/code&gt; method must return a pointer to this internal buffer, not the original pointer which was used for &lt;code&gt;set()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I can assume the &lt;code&gt;get()&lt;/code&gt; method will be used frequently and often on the hot path, while the mutation methods will be invoked rarely and never on the hot path.&lt;/p&gt;

&lt;p&gt;This is roughly how readers are going to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;challengePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="n"&gt;challengeLen&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;signaturePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;signatureLen&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AuthCrypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PUBLIC_KEY_SIZE_BYTES&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challengePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challengeLen&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signaturePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signatureLen&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there were no mutations, I could have just implemented a pre-populated immutable lookup directory and called it a day. However, shared mutable state brings two classes of challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pointer lifecycle management&lt;/li&gt;
&lt;li&gt;Consistency of map internals&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first issue boils down to ensuring that when a &lt;code&gt;map.get()&lt;/code&gt; returns a pointer, the pointer must remain valid and the memory behind it must not change for &lt;em&gt;as long as needed&lt;/em&gt;. In our case, it means until the &lt;code&gt;AuthCrypto.verifySignature()&lt;/code&gt; returns. &lt;/p&gt;

&lt;p&gt;The second issue is all about concurrent data-structure design, and we will discuss this in more detail later. Let’s&lt;br&gt;
explore the first issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pointer lifecycle management
&lt;/h2&gt;

&lt;p&gt;If our map's values were just regular objects managed by the JVM, things could be simple: &lt;code&gt;map.get()&lt;/code&gt; would return a reference to an object and then it could forget this &lt;code&gt;get()&lt;/code&gt; call ever happened. The &lt;code&gt;remove()&lt;/code&gt;and &lt;code&gt;set()&lt;/code&gt; methods would just remove the map's reference to the value object, and would never change an already returned object. Easy. But that’s not our case, we are working with off-heap memory and have to manage it on our own.&lt;/p&gt;

&lt;p&gt;Fundamentally, there are two ways to solve it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the &lt;code&gt;get()&lt;/code&gt; contract so it doesn't return a pointer. Instead, it receives a pointer from the outside and copies the value there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get()&lt;/code&gt; still returns a pointer, but the map guarantees the memory behind it stays immutable until the caller notifies the map that it’s done and that it won’t use the pointer anymore.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Option 1: Callers own the destination memory
&lt;/h3&gt;

&lt;p&gt;The first option looks interesting. The new contract could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentString2KeyMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;srcKeyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;dstKeyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller would own the &lt;code&gt;dstKeyPtr&lt;/code&gt; pointer and the map would copy the key from its internals to this pointer and forget this &lt;code&gt;get()&lt;/code&gt; call ever happened.&lt;/p&gt;

&lt;p&gt;This sounds quite nice at first, until we realize it just kicks the can down the road: it forces every calling thread to maintain its own buffer to pass to &lt;code&gt;get()&lt;/code&gt;. If callers are all single-threaded, it’s still easy: each calling object owns a buffer to pass to &lt;code&gt;get()&lt;/code&gt; . &lt;/p&gt;

&lt;p&gt;But if calling functions are themselves concurrent, it becomes more complicated. We have to make sure each calling thread uses a different buffer. &lt;/p&gt;

&lt;p&gt;Ideally, the buffer would be allocated on stack, but this is Java so that’s not possible. We certainly do not want to&lt;br&gt;
allocate/deallocate a new buffer in the process heap for every invocation.&lt;/p&gt;

&lt;p&gt;So what’s left? Pooling? That’s messy. &lt;/p&gt;

&lt;p&gt;ThreadLocal? Even more messy and harder to put a cap on the number of buffers. &lt;/p&gt;

&lt;p&gt;Maybe option 1 is not interesting as it seemed at first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 2: Lifecycle notifications
&lt;/h3&gt;

&lt;p&gt;Let’s explore the 2nd option. The contract remains the same as outlined in the original proposal: &lt;code&gt;long get(String username)&lt;/code&gt;. We have to make sure the memory behind the pointer remains unchanged until we are done. &lt;/p&gt;

&lt;p&gt;The absolute simplest thing would be to use a read-write lock. &lt;/p&gt;

&lt;p&gt;Each map would have a read-write lock associated, and then readers acquire a read lock before calling &lt;code&gt;get()&lt;/code&gt; and release it only after returning from &lt;code&gt;AuthCrypto.verifySignature()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;challengePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="n"&gt;challengeLen&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;signaturePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;signatureLen&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquireReadLock&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AuthCrypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PUBLIC_KEY_SIZE_BYTES&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challengePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;challengeLen&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signaturePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signatureLen&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;releaseReadLock&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutators would only have to acquire a write lock before calling &lt;code&gt;set()&lt;/code&gt; or &lt;code&gt;remove()&lt;/code&gt;. Not only is this design simple to reason about, it is also simple to implement. &lt;/p&gt;

&lt;p&gt;Assuming that only &lt;code&gt;set()&lt;/code&gt; and &lt;code&gt;remove()&lt;/code&gt; change the internal state, we can just take a single-threaded map implementation and it will do it. But there's a catch... It violates our original requirements! &lt;/p&gt;

&lt;p&gt;Readers are often on the hot-path and we want them to remain lock-free. The proposed design blocks readers when the map is being updated, so this is a no-go. &lt;/p&gt;

&lt;p&gt;What can we do? We could change the locking schema to be more fine-grained - instead of locking the whole map we could lock particular entries. While this would improve practical behaviour, it would also complicate the map design and the readers could still be blocked when the same key is being updated. &lt;/p&gt;

&lt;p&gt;What else? We could use an optimistic locking schema, but&lt;br&gt;
this brings its own intricacies.&lt;/p&gt;

&lt;p&gt;It’s becoming clear that pointer lifecycle management will have to work in concert with the internal map implementation. So was this exercise completely fruitless? Not completely.&lt;/p&gt;

&lt;p&gt;There is still one design idea we could reuse: Map users must&lt;br&gt;
explicitly notify that they are no longer using the pointer. &lt;/p&gt;

&lt;p&gt;Let’s explore how to&lt;br&gt;
design map internals!&lt;/p&gt;
&lt;h2&gt;
  
  
  Designing a map for lock-free readers
&lt;/h2&gt;

&lt;p&gt;I consider myself an experienced generalist. I know bits and bobs about concurrent programming, distributed systems and all kinds of other areas, but I’m not really narrowly specialized in any particular topic. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;A jack of all trades and master of none?&lt;/em&gt; Probably. So when I was thinking about a suitable data structure, I did what every generalist would do in 2023: asked ChatGPT!&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7t1rinyq0rdl4roln4id.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7t1rinyq0rdl4roln4id.png" alt="Chat GPT had some tremendous feedback"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was amazed GPT realized that I meant to write “single writer” and not “single reader”, which I took as proof that GPT knows what is it talking about! 🙂 So I read further: I might have heard about RCU before, but I have never used it myself. I found the description a bit too vague to be used as an implementation guide, and it was a lunch time anyway.&lt;/p&gt;
&lt;h3&gt;
  
  
  Copy-On-Write intermezzo
&lt;/h3&gt;

&lt;p&gt;While walking to a lunch place, I was thinking about it more and got an idea. Why not use the &lt;a href="https://en.wikipedia.org/wiki/Copy-on-write" rel="noopener noreferrer"&gt;Copy-On-Write&lt;/a&gt; technique to implement a &lt;a href="https://en.wikipedia.org/wiki/Persistent_data_structure" rel="noopener noreferrer"&gt;persistent map&lt;/a&gt;? &lt;/p&gt;

&lt;p&gt;That way, I could take a regular single-threaded map, and mutators would clone the current map, do their thing, and then atomically set this newly created map as the map for readers. Readers would then use whatever map was published as the latest. Published maps are immutable, thus always safe for concurrent readers, even from multiple threads, &lt;a href="http://concurrencyfreaks.blogspot.com/2013/05/lock-free-and-wait-free-definition-and.html" rel="noopener noreferrer"&gt;lock-free&lt;/a&gt;. In fact, even wait-free. Yay! &lt;/p&gt;

&lt;p&gt;Additionally, we would have to introduce a mechanism for safe deallocation of map internal buffers when a stale (=no longer the latest published map) map has no readers. Otherwise, we would be leaking memory. That’s a complication, but it feels like something easily fixable with enough dedication and atomic reference counters.&lt;/p&gt;

&lt;p&gt;So this all sounds good, but as have come to expect... there is still a catch. We need to allocate a block of memory for the map contents on every single mutation. We said that mutations are rare, so perhaps that’s not a big deal? Maybe it’s not, but one of the QuestDB design principles is to be conservative with memory allocations as they cost CPU cycles, memory bandwidth, cause CPU cache thrashing, and in general tend to introduce unpredictable behaviour.&lt;/p&gt;
&lt;h3&gt;
  
  
  Back to the drawing board: Map recycling
&lt;/h3&gt;

&lt;p&gt;So I couldn’t implement a straightforward Copy-On-Write map, but I felt I was on the right path towards the goal: lock-Free readers. At some point I realized that, instead of allocating a new map whenever there is a change, I could keep reusing only 2 maps: one would be available for readers and the other for writers. &lt;/p&gt;

&lt;p&gt;Once a map is published for readers, it’s guaranteed to be immutable as long as there is at least one reader still accessing it.&lt;/p&gt;

&lt;p&gt;It would look similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getMapForReaders&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea looks neat, but it’s clear the code as outlined above has a number of issues and open questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The code always mutates just a single map, but we clearly need to keep both maps in sync. We cannot lose updates.&lt;/li&gt;
&lt;li&gt;Multiple mutating threads could step on each others toes.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;swapMap()&lt;/code&gt; unconditionally swaps reader and writer maps, the mutating thread performing two consecutive mutations could write into a map which still has some readers. This violates our invariant: we must not have readers and writers concurrently accessing the same internal map.&lt;/li&gt;
&lt;li&gt;How to implement &lt;code&gt;getMapForWriters()&lt;/code&gt; and &lt;code&gt;getMapForReaders()&lt;/code&gt;? 🙂&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Single-writer FTW!
&lt;/h3&gt;

&lt;p&gt;Let’s start with problem #2 - multiple mutating threads. &lt;/p&gt;

&lt;p&gt;We said that mutations are rare and never on the hot path. Hence, we can afford to be brutal and use a simple mutex - to make sure there is always at most a single mutator. The single-writer principle is known to simplify design of concurrent algorithms anyway. &lt;/p&gt;

&lt;p&gt;Thus the map now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;writeMutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getMapForReaders&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was easy. Maybe violent, but easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Racing threads
&lt;/h3&gt;

&lt;p&gt;Let’s explore something more complicated - problem #3 - multiple consecutive write operations. What do I mean by this? &lt;/p&gt;

&lt;p&gt;Consider this scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We have 2 instances of &lt;code&gt;InternalMap&lt;/code&gt;, let’s call them &lt;code&gt;m0&lt;/code&gt; and &lt;code&gt;m1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The field &lt;code&gt;readerMap&lt;/code&gt; references the map &lt;code&gt;m0&lt;/code&gt; and &lt;code&gt;writerMap&lt;/code&gt; references &lt;code&gt;m1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reader thread calls &lt;code&gt;map.get()&lt;/code&gt;. Thus &lt;code&gt;getMapForReaders()&lt;/code&gt; returns &lt;code&gt;m0&lt;/code&gt;. At this point the reader thread is paused by the OS.&lt;/li&gt;
&lt;li&gt;Writer thread calls &lt;code&gt;map.set()&lt;/code&gt;. Thus &lt;code&gt;getMapForWriters()&lt;/code&gt; returns &lt;code&gt;m1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writer modifies &lt;code&gt;m1&lt;/code&gt; and &lt;strong&gt;swaps the maps&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The field &lt;code&gt;readerMap&lt;/code&gt; now references the map &lt;code&gt;m1&lt;/code&gt; and &lt;code&gt;writerMap&lt;/code&gt; references &lt;code&gt;m0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Another writer calls &lt;code&gt;map.set()&lt;/code&gt;. Thus &lt;code&gt;getMapForWriters()&lt;/code&gt; returns &lt;code&gt;m0&lt;/code&gt; and the writer starts mutating it. The write operation takes a while.&lt;/li&gt;
&lt;li&gt;The OS resumes the reader thread from #3, and it starts reading &lt;code&gt;m0&lt;/code&gt; (because that’s the map the reader got before its thread was paused!)&lt;/li&gt;
&lt;li&gt;At this point we have a reader thread concurrently accessing the same internal map instance as a writer thread -&amp;gt; &lt;strong&gt;Boom 💥💥💥!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kofpk8mp8cwngbbnf8a.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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kofpk8mp8cwngbbnf8a.png" alt="The racy scenario"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the scenario looks too long and boring and you skipped it, here is a short summary: a reader obtains &lt;code&gt;mapForReaders&lt;/code&gt; and in the next moment this map becomes &lt;code&gt;writerMap&lt;/code&gt;. So what was once a &lt;code&gt;readerMap&lt;/code&gt; is now a &lt;code&gt;writerMap&lt;/code&gt; and thus the next write operation can mutate it at will. Except the stale reader still thinks the same map is safe for reading. &lt;strong&gt;That’s a bad concurrency bug!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How can we prevent the racy scenario outlined above? We already use a mutex on the write path, that’s almost as nasty as it gets. Almost?! Can we be even nastier? Sure we can! &lt;/p&gt;

&lt;p&gt;Each internal map could have a reader counter and &lt;code&gt;getMapForWriters()&lt;/code&gt; won’t return until the reader counter on the current &lt;code&gt;mapForWriters&lt;/code&gt; reaches 0. In other words: the writer won’t mutate &lt;code&gt;writerMap&lt;/code&gt; until all readers indicate they are no longer using this map. &lt;/p&gt;

&lt;p&gt;How about newly arriving readers? New readers don’t touch &lt;code&gt;writerMap&lt;/code&gt; at all, they always load the current &lt;code&gt;readerMap&lt;/code&gt; so that’s not a problem. &lt;/p&gt;

&lt;p&gt;Enough talking! Let’s see some code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;writeMutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="nf"&gt;concurrentReader&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(;;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerArrived&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="nf"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReaders&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;implement&lt;/span&gt; &lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AtomicInteger&lt;/span&gt; &lt;span class="n"&gt;readerCounter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;readerCounter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrement&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;readerArrived&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;readerCounter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;hasReaders&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;readerCounter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// the rest of a single threaded map impl&lt;/span&gt;

  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above looks way more complex than the previous buggy version. &lt;/p&gt;

&lt;p&gt;Let’s go briefly through the changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The most visible change: &lt;strong&gt;the &lt;code&gt;get()&lt;/code&gt; method is gone!&lt;/strong&gt; Instead, there is a new method &lt;code&gt;concurrentReader()&lt;/code&gt; which returns an interface &lt;code&gt;Reader&lt;/code&gt; with two methods: &lt;code&gt;get()&lt;/code&gt; and &lt;code&gt;readerGone()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;There is a skeleton of &lt;code&gt;InternalMap&lt;/code&gt;. It does not show any map-related logic, because it could be any single-threaded implementation of a map-like structure. It only demonstrates that each internal map has its own reader counter.&lt;/li&gt;
&lt;li&gt;For the first time we see an implementation &lt;code&gt;getMapForWriters()&lt;/code&gt;. It’s really not doing anything else except waiting for all the stale readers from the &lt;code&gt;writerMap&lt;/code&gt; to disappear. The &lt;code&gt;backoff()&lt;/code&gt; method may have various implementations and use primitives such as &lt;code&gt;Thread#yield()&lt;/code&gt; or &lt;code&gt;LockSupport#parkNanos()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Counting readers
&lt;/h3&gt;

&lt;p&gt;Let’s have a closer look at each change. &lt;/p&gt;

&lt;p&gt;Why did we introduce the &lt;code&gt;Reader&lt;/code&gt; interface? Isn’t it just an unnecessary complication, and an example of the over-engineering so prevalent in Java culture? &lt;/p&gt;

&lt;p&gt;Well, maybe, but it simplifies the mechanism for readers to&lt;br&gt;
notify the map that they won’t access the returned pointer anymore. &lt;/p&gt;

&lt;p&gt;How? Each internal map has its own reader counter. When a reader no longer needs a pointer returned by a prior &lt;code&gt;get()&lt;/code&gt;, it must invoke &lt;code&gt;readerGone()&lt;/code&gt; on the correct internal map. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Reader&lt;/code&gt; interface does exactly that - it knows which&lt;br&gt;
instance of &lt;code&gt;InternalMap&lt;/code&gt; is in use. When a thread calls &lt;code&gt;reader.readerGone()&lt;/code&gt;, it decrements the reader counter on that map. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharSequence&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;challengePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="n"&gt;challengeLen&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;signaturePtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;signatureLen&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Reader&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;concurrentReader&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AuthCrypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verifySignature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;[...]);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hopefully made it clearer why we need the &lt;code&gt;Reader&lt;/code&gt; interface. &lt;/p&gt;

&lt;p&gt;A little aside: Do you still remember the design idea with a read-write-lock? I decided not to use it, because it could block readers. But the lock usage pattern was an inspiration for this notification mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding check-then-act bugs
&lt;/h3&gt;

&lt;p&gt;Let’s focus on the implementation of &lt;code&gt;concurrentReader()&lt;/code&gt; method. &lt;/p&gt;

&lt;p&gt;It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="nf"&gt;concurrentReader&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(;;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerArrived&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It loads the current &lt;code&gt;readerMap&lt;/code&gt;, increments its reader counter, and returns it to the caller if - and only if - the &lt;code&gt;readerMap&lt;/code&gt; field still points to the same &lt;code&gt;InternalMap&lt;/code&gt; instance. Otherwise, it decrements the reader counter to undo the increment, and retries everything from start. &lt;/p&gt;

&lt;p&gt;Why this complexity? Why do we need the retry mechanism at all? It’s to protect us against a similar problem with stale readers that we already discussed.&lt;/p&gt;

&lt;p&gt;Consider this simpler implementation of &lt;code&gt;concurrentReader()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="nf"&gt;concurrentReader&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// buggy!&lt;/span&gt;
  &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerArrived&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// increment the reader counter&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// getMapForWriters() shown for reference only&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="nf"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReaders&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broken down, we see that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There is a writer thread calling &lt;code&gt;map.set()&lt;/code&gt; and a reader thread calling &lt;code&gt;map.concurrentReader()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The reader thread loads the current &lt;code&gt;readerMap&lt;/code&gt;, but the OS pauses it before it increments the reader counter.&lt;/li&gt;
&lt;li&gt;The writer thread loads the current &lt;code&gt;writerMap&lt;/code&gt;, does a mutation, and swaps the maps. This means the old &lt;code&gt;readerMap&lt;/code&gt; is now the new &lt;code&gt;writerMap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At this point the reader has an instance of &lt;code&gt;InternalMap&lt;/code&gt; which is also set in the &lt;code&gt;writerMap&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;There is another writer operation. &lt;code&gt;getMapForWriters()&lt;/code&gt; returns current &lt;code&gt;writerMap&lt;/code&gt; immediately, because the reader counter is still zero. The writer thread starts mutating the map.&lt;/li&gt;
&lt;li&gt;The OS resumes the reader thread. The thread has a reference to the same internal map which is currently being mutated by the thread from the previous point.&lt;/li&gt;
&lt;li&gt;The reader thread increments the internal map reader counter, but that’s fruitless as the writer thread is already mutating the map.&lt;/li&gt;
&lt;li&gt;The reader thread &lt;code&gt;concurrentReader()&lt;/code&gt; returns a map which is being concurrently mutated by a writer thread -&amp;gt; &lt;strong&gt;Boom 💥💥💥!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The extra check in &lt;code&gt;concurrentReader()&lt;/code&gt; is meant to prevent the above scenario. It guarantees it incremented the reader counter on the map which is still the current &lt;code&gt;readerMap&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt; &lt;span class="nf"&gt;concurrentReader&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(;;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerArrived&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// increment the reader counter&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readerGone&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is still possible that a reader thread increments the reader counter, returns the &lt;code&gt;Reader&lt;/code&gt; to the caller, and in the next microsecond a writer thread swaps the maps so the map instance returned to the caller is now set as the &lt;code&gt;writerMap&lt;/code&gt;. This is entirely possible, but it won’t do any harm. The&lt;br&gt;
writer won’t get access to the &lt;code&gt;writerMap&lt;/code&gt; before its reader counter has reached zero.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is left?
&lt;/h3&gt;

&lt;p&gt;At this point we solved the hardest part of the concurrent algorithm, but there are still some unresolved issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The map is still losing updates! We have 2 internal maps, but each update mutates just a single map.&lt;/li&gt;
&lt;li&gt;Some smaller bits:

&lt;ol&gt;
&lt;li&gt;There is no &lt;a href="https://en.wikipedia.org/wiki/Happened-before" rel="noopener noreferrer"&gt;happens-before relationship&lt;/a&gt; between writers swapping maps and readers loading them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;close()&lt;/code&gt; method is not implemented so the map might leak native memory, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Dealing with lost updates
&lt;/h3&gt;

&lt;p&gt;We can fix the first problem. After swapping the maps, we could wait until the current &lt;code&gt;writerMap&lt;/code&gt; has no readers and then update it. &lt;/p&gt;

&lt;p&gt;So mutation operations would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a safe implementation as the &lt;code&gt;getMapForWriters()&lt;/code&gt; guarantees that the returned map has no readers and no new reader will arrive until the next swap.&lt;/p&gt;

&lt;p&gt;On the other hand, it's inefficient: when we switch maps after writing, the new &lt;code&gt;writerMap&lt;/code&gt; may have stale readers, causing delays until they're cleared.&lt;/p&gt;

&lt;p&gt;Is there a better option? It turns out that there is! &lt;/p&gt;

&lt;p&gt;We could change the first map, swap them and remember the operation including all parameters. During the next mutation we would replay the operation on the &lt;code&gt;mapForWriters&lt;/code&gt; and, if&lt;br&gt;
mutations are sufficiently rare, by the time we are replaying the operation the &lt;code&gt;writerMap&lt;/code&gt; has no longer any readers. &lt;/p&gt;

&lt;p&gt;Let’s see the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;replayLastOperationOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;rememberSetOperation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyPtr&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;map.remove()&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;synchronized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writeMutex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getMapForWriters&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;replayLastOperationOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;swapMaps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;rememberRemoveOperation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rememberSetOperation()&lt;/code&gt; must copy the memory under the pointer to its own buffer, but we only have to remember a single operation. Given our blobs are fixed-size, it allows us to keep reusing the same replay buffer. Zero allocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Playing by the Java Memory Model rules
&lt;/h3&gt;

&lt;p&gt;Now let’s do the last, important change. &lt;/p&gt;

&lt;p&gt;This is how the &lt;code&gt;ConcurrentMap&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;writeMutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WriterOperation&lt;/span&gt; &lt;span class="n"&gt;lastWriterOperation&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;[...]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole mutable state is encapsulated in these 4 objects. The fields &lt;code&gt;writerMap&lt;/code&gt; and &lt;code&gt;lastWriterOperations&lt;/code&gt; are only accessed by a writer thread while holding a mutex. But the &lt;code&gt;readerMap&lt;/code&gt; field is set by a writer thread and then loaded by readers. &lt;/p&gt;

&lt;p&gt;Readers are lock-free, they do not acquire any mutex&lt;br&gt;
before accessing the reader map. That is a &lt;a href="https://en.wikipedia.org/wiki/Race_condition#Data_race" rel="noopener noreferrer"&gt;data race&lt;/a&gt; and it could cause visibility issues. &lt;/p&gt;

&lt;p&gt;The fix is easy, just mark the &lt;code&gt;readerMap&lt;/code&gt; as &lt;code&gt;volatile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;volatile&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;readerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt; &lt;span class="n"&gt;writerMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InternalMap&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;writeMutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WriterOperation&lt;/span&gt; &lt;span class="n"&gt;lastWriterOperation&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer path now looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Acquire a mutex&lt;/li&gt;
&lt;li&gt;Load the current &lt;code&gt;writerMap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait until all stale readers are gone&lt;/li&gt;
&lt;li&gt;Replay the last operation&lt;/li&gt;
&lt;li&gt;Do a new mutation&lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;readerMap&lt;/code&gt; and &lt;code&gt;writerMap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remember the operation so it can be replayed on the untouched map during the new mutation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;readerMap&lt;/code&gt; is now marked as volatile, giving us &lt;a href="https://jepsen.io/consistency/models/sequential" rel="noopener noreferrer"&gt;sequential consistency&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Colloquially speaking, the readers will see the most recent map swap done by a writer thread. The readers are also guaranteed to see all changes which were performed before the writer thread set the map as &lt;code&gt;mapForReaders&lt;/code&gt;. And that’s it!&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;We walked through a process of designing a concurrent data-structure which is lock-free on the read path. We could generalize some of the design principles we applied into the following rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single Writer Rule&lt;/strong&gt;: Use a mutex for writes, ensuring only one writer at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual Maps&lt;/strong&gt;: Maintain two maps – one for readers and another for the writer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointer Swap Mechanism&lt;/strong&gt;: When a writer updates, it operates on the &lt;code&gt;mapForWriters&lt;/code&gt; and then switches the roles of the two maps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reader Counter&lt;/strong&gt;: Each map has an atomic &lt;code&gt;readerCounter&lt;/code&gt;. As a reader begins, the counter increments and decrements upon completion. This ensures that no one accesses the &lt;code&gt;mapForWriters&lt;/code&gt; until all active readers from the previous swap have completed their reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change Tracking&lt;/strong&gt;: Writers, when updating &lt;code&gt;mapForWriters&lt;/code&gt;, log their modifications. This is crucial since we need to replicate these changes to &lt;code&gt;mapForReaders&lt;/code&gt;, which might still be in use by some readers. Instead of waiting for all readers to shift to the new map, we log the change and apply it in the subsequent update. Given that we switch maps post-update, by the time of the next update, &lt;code&gt;mapForWriters&lt;/code&gt; is likely free of old readers, allowing for immediate change application.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What’s next?
&lt;/h3&gt;

&lt;p&gt;We have a working implementation of a concurrent map, but it’s not yet ready for production. There are still some issues to be solved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;close()&lt;/code&gt; method is not implemented so the map might leak native memory. This is trivial to fix and I leave it as an exercise for the reader.&lt;/li&gt;
&lt;li&gt;There are no tests! There are various ways to test concurrent data structures. You could use a stress test, where you spawn a lot of threads and let them mutate the map in a random way and then check the consistency of the map and some invariants. You could learn &lt;a href="https://lamport.azurewebsites.net/tla/tla.html" rel="noopener noreferrer"&gt;TLA+&lt;/a&gt; and write a formal model of the map and then verify it.&lt;/li&gt;
&lt;li&gt;Performance optimizations. The current implementation uses the same write path on both internal maps. Chances are that's not the best option. For example there is no reason to calculate the hash code twice. There is no reason to locate the bucket twice. The first &lt;code&gt;set()&lt;/code&gt; could remember which bucket was used and the second &lt;code&gt;set()&lt;/code&gt; could reuse it. We could also add batching to the writer path: the current implementation swaps the maps after every mutation. This is inefficient when writers are mutating the map in a tight loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Acknowledgements
&lt;/h3&gt;

&lt;p&gt;After I finished the implementation, I got really excited and I wanted to share it with the world. I naively thought that I was the first one to come up with this idea. I was wrong. &lt;/p&gt;

&lt;p&gt;First, I found the &lt;a href="http://concurrencyfreaks.blogspot.com/2013/11/double-instance-locking.html" rel="noopener noreferrer"&gt;Double Instance Locking&lt;/a&gt; pattern at the amazing&lt;br&gt;
&lt;a href="http://concurrencyfreaks.blogspot.com/" rel="noopener noreferrer"&gt;Concurrency Freaks&lt;/a&gt; blog. This pattern is very similar to the one I described here. It also uses 2 internals structures and readers are alternating between them. It uses a read-write lock to protect the map being mutated. Given there is only a single writer then at any given time there is at least one internal map which is available for reading. This gives readers lock freedom. &lt;/p&gt;

&lt;p&gt;It's fair to say that the Double Instance Locking&lt;br&gt;
pattern is simpler to reason about. It decomposes the problem better. But I'd still argue my contribution is the trick with delayed replay of the last operation - if writers are sufficiently rare, then writers won't be blocked at all.&lt;/p&gt;

&lt;p&gt;The same blog also links to the paper &lt;a href="https://master.dl.sourceforge.net/project/ccfreaks/papers/LeftRight/leftright-extended.pdf?viasf=1" rel="noopener noreferrer"&gt;Left-Right: A Concurrency Control Technique with Wait-Free Population Oblivious Reads&lt;/a&gt; which on the surface also looks similar. It claims not only Lock-Freedom, but also Wait-Freedom. I have yet to deep dive into it.&lt;/p&gt;

&lt;p&gt;I would like to thank to reviewers of this article: &lt;a href="https://twitter.com/AndreyPechkurov" rel="noopener noreferrer"&gt;Andrei Pechkurov&lt;/a&gt; and &lt;a href="https://twitter.com/mtopolnik" rel="noopener noreferrer"&gt;Marko Topolnik&lt;/a&gt;. Thank you so much! Without your encouragement I would not find the courage to publish this article! ❤️ Of course, all the mistakes still left are solely my responsibility.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Originally posted on &lt;a href="https://questdb.io/blog/concurrent-lockfree-datastructure-design-walkthrough/" rel="noopener noreferrer"&gt;QuestDB.io&lt;/a&gt; by &lt;a class="mentioned-user" href="https://dev.to/jerrinot"&gt;@jerrinot&lt;/a&gt; *&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>java</category>
      <category>opensource</category>
      <category>database</category>
    </item>
    <item>
      <title>Fuzz testing: the best thing to happen to our application tests</title>
      <dc:creator>Kellen</dc:creator>
      <pubDate>Thu, 17 Aug 2023 21:42:35 +0000</pubDate>
      <link>https://dev.to/questdb/fuzz-testing-the-best-thing-to-happen-to-our-application-tests-1332</link>
      <guid>https://dev.to/questdb/fuzz-testing-the-best-thing-to-happen-to-our-application-tests-1332</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;QuestDB is a time-series database that offers fast ingest speeds, ILP and PGWire support and SQL query syntax. Databases are placed under constant and demanding workloads, and building one teaches many hard lessons. We're happy to share them with you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost two years ago, we were playing an endless game of whack-a-mole game with segfaults, data corruption, and various concurrency bugs. Our users were reporting them and, for each report, we had to reproduce the bug, analyze and - finally - fix it. Eventually, we decided to take a step back and come up with a deeper solution. This article details our pain, and the journey we took to get out of it. Maybe we can help you out of a similar bind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slaying the many headed hydra
&lt;/h2&gt;

&lt;p&gt;One bug solved, five more appear. We caught bugs and our users caught bugs. Each report lead to an investigation and - in most cases - a resolution. But sometimes users would apply workarounds and proceed forward without reporting the issue, and some bugs would go unresolved. This loop led to frustration for both our users and the QuestDB team.&lt;/p&gt;

&lt;p&gt;We introduced the first fuzz test into the QuestDB project in an attempt to make the database more robust, and since then we have added many more of them. It's hard to quantify the bugs found by fuzzing, but all of the known critical ones are gone and it's a very rare case nowadays to see a critical issue reported by the community. &lt;/p&gt;

&lt;p&gt;On top of that, recently the SQLancer team added QuestDB support to their testing tool and helped us to find a number of issues in our SQL engine. That's why we believe that almost any complex application would gain a lot from this kind of test, so if yours doesn't have them, today we hope to inspire you to start writing fuzz tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is fuzzing?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"What's the fuzz? Tell me what's happening." - Modified lyrics from a well-known &lt;a href="https://en.wikipedia.org/wiki/Jesus_Christ_Superstar"&gt;rock opera&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before sharing our story, let's start with the basics and define a fuzz test. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Fuzzing"&gt;Wikipedia says&lt;/a&gt; that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;... fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. The program is then monitored for exceptions such as crashes, failing built-in code assertions, or potential memory leaks. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So for instance, if you write a compiler, you can use fuzzing to generate source code variations, including invalid ones, and test whether your compiler is able to give a meaningful result for the input program. If you have a web service, you can write a fuzz test that would try to send invalid or semi-valid requests to your service, and then analyze whether any of the requests break the security of your service. Or even crash it. &lt;/p&gt;

&lt;p&gt;Furthermore, if you have a command line utility, like &lt;code&gt;curl&lt;/code&gt;, you may use fuzz testing to find nasty things &lt;a href="https://blog.trailofbits.com/2023/02/14/curl-audit-fuzzing-libcurl-command-line-interface/"&gt;like memory corruption bugs&lt;/a&gt;. But not only that. In the database world, fuzzing is usually applied to APIs that accept a query language, like &lt;a href="https://questdb.io/docs/concept/sql-execution-order/"&gt;SQL&lt;/a&gt; or a specialized protocol like the &lt;a href="https://questdb.io/docs/reference/api/ilp/overview/"&gt;InfluxDB Line Protocol&lt;/a&gt; (ILP). Lastly, it would be silly not to mention that fuzzing can be used to find general vulnerabilities in programs.&lt;/p&gt;

&lt;p&gt;So how do we write them?&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting fuzzy
&lt;/h2&gt;

&lt;p&gt;There are a number of different approaches to writing fuzz tests: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Your fuzzer may be generation-based and generate inputs from scratch. Or it may be mutation-based, and have a corpus of seed inputs that it modifies to produce a final input. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The test may be dumb, if it is producing unstructured, random inputs, like random strings instead of proper SQL statements. Or it may be smart, if it's aware of the expected input structure. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You may choose to use a white-, grey-, or black-box testing technique, depending on the awareness of the program-under-test structure.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you were to fuzz test a database, the test could be as simple as the following test written in Java-inspired pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testSqlEngine&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openConnection&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;randomInt&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;randomString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;executeSql&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;assertDatabaseDidNotCrash&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we generate a string filled with random characters, send it over the database connection and, finally, check if the database process is still up and running. This is a (1) generation-based, (2) dumb, (3) black-box test that makes no assumptions about the SQL syntax and only follows the database network protocol. &lt;/p&gt;

&lt;p&gt;Interestingly, it is very close to what the &lt;a href="https://en.wikipedia.org/wiki/Fuzzing#History"&gt;original authors&lt;/a&gt; of the term "fuzz" did in 1988 when they were testing Unix utilities. Of course, such an approach is not very efficient when applied to database software. At QuestDB, we prefer fuzzers to be generation-based and smart, but that is our preference. Other combinations may be useful in other software projects.&lt;/p&gt;

&lt;p&gt;With the basics covered, let's look at how fuzzing helped us make QuestDB better...&lt;/p&gt;

&lt;h2&gt;
  
  
  Our fuzzing story
&lt;/h2&gt;

&lt;p&gt;As we already mentioned, our first fuzz test was written almost two years ago. It tested the ILP protocol by sending semi-random, potentially invalid messages. The first test immediately revealed a number of critical issues, including a few segfaults.&lt;/p&gt;

&lt;p&gt;To give you an impression of that fuzzer, let's dive into our ILP protocol &lt;a href="https://questdb.io/docs/reference/api/ilp/overview/"&gt;implementation&lt;/a&gt;. Creating a single-row table with ILP is as simple as sending the following to &lt;code&gt;&amp;lt;questdb_host&amp;gt;:9009&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;weather,city&lt;span class="o"&gt;=&lt;/span&gt;Sofia &lt;span class="nv"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;27.5 1692010877000000000&lt;span class="se"&gt;\n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once sent, this message tells QuestDB to create a table with the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE 'weather' (
  city SYMBOL,
  temperature DOUBLE,
  timestamp TIMESTAMP
) timestamp (timestamp) PARTITION BY DAY WAL;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with the below row:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;city&lt;/th&gt;
&lt;th&gt;temperature&lt;/th&gt;
&lt;th&gt;timestamp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sofia&lt;/td&gt;
&lt;td&gt;27.5&lt;/td&gt;
&lt;td&gt;2023-08-14T11:01:17.000000Z&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here we have a &lt;a href="https://questdb.io/docs/concept/designated-timestamp/"&gt;partitioned&lt;/a&gt; table named &lt;code&gt;weather&lt;/code&gt;. The table structure, column names and row values are fully defined in our ILP message. Now, let's send another message over TCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;weather,city&lt;span class="o"&gt;=&lt;/span&gt;Berlin &lt;span class="nv"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;28,humidity&lt;span class="o"&gt;=&lt;/span&gt;0.42 1692011659000000000&lt;span class="se"&gt;\n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result, we got a new &lt;code&gt;humidity&lt;/code&gt; column of type &lt;code&gt;DOUBLE&lt;/code&gt; in the table. &lt;/p&gt;

&lt;p&gt;The table contents are now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;city&lt;/th&gt;
&lt;th&gt;temperature&lt;/th&gt;
&lt;th&gt;humidity&lt;/th&gt;
&lt;th&gt;timestamp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sofia&lt;/td&gt;
&lt;td&gt;27.5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2023-08-14T11:01:17.000000Z&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Berlin&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;2023-08-14T11:14:19.000000Z&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shall we send another row? Let's do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;weather,country&lt;span class="o"&gt;=&lt;/span&gt;France,CiTy&lt;span class="o"&gt;=&lt;/span&gt;Paris &lt;span class="nv"&gt;HuMiDiTy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.58,TeMpErAtUrE&lt;span class="o"&gt;=&lt;/span&gt;26 1692012370000000000&lt;span class="se"&gt;\n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we've changed the order for the &lt;code&gt;humidity&lt;/code&gt; and &lt;code&gt;temperature&lt;/code&gt; columns, added a new column named &lt;code&gt;country&lt;/code&gt; and slightly ImProVeD letter capitalization in the older column names. Again, the database should add the new column and should also ignore the capitalization difference in the column names. With all that said, the message should yield the following rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;city&lt;/th&gt;
&lt;th&gt;temperature&lt;/th&gt;
&lt;th&gt;humidity&lt;/th&gt;
&lt;th&gt;timestamp&lt;/th&gt;
&lt;th&gt;country&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sofia&lt;/td&gt;
&lt;td&gt;27.5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2023-08-14T11:01:17.000000Z&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Berlin&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;2023-08-14T11:14:19.000000Z&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paris&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;2023-08-14T11:26:10.000000Z&lt;/td&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Adding new columns, reordering columns, and changing column names are not the only things that we can potentially do over the ILP protocol. &lt;/p&gt;

&lt;p&gt;To give you an idea of what we can do, we can: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;include non-ASCII characters in table and column names, as well as column values&lt;/li&gt;
&lt;li&gt;skip existing columns and the timestamp value in the messages&lt;/li&gt;
&lt;li&gt;duplicate certain columns, so that the column name and its value is repeated multiple times in the same message &lt;/li&gt;
&lt;li&gt;continue with the list of mutations leading to valid and invalid ILP messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we can decide whether to apply or not to apply these mutations based on a RNG (Random Number Generator) and run the scenario over multiple connections. Once all messages are sent and received, we can compare the table's contents with the expected rows.&lt;/p&gt;

&lt;p&gt;The above description is basically what our first fuzz test was doing. While being simple, it revealed - as we say - a "can of worms", i.e. many issues, around our ILP code. Since the first fuzzer, we wrote 62 additional tests, most of which are aimed to stress our storage, protocol implementation, and concurrent code. Needless to say, the number keeps growing. &lt;/p&gt;

&lt;p&gt;While 62 fuzzers doesn't sound like a huge number, each of these tests produces a great number of different test scenarios, all thanks to randomization. To increase our chances of finding bugs, our CI runs the fuzzers periodically. If you'd like to see what the tests look like, &lt;a href="https://github.com/questdb/questdb/blob/e98177f8f74c35184019c008d2c332d242abddb2/core/src/test/java/io/questdb/test/griffin/wal/DedupInsertFuzzTest.java"&gt;take a look at the test&lt;/a&gt; that covers the new deduplication feature.&lt;/p&gt;

&lt;p&gt;Based on our experience, we believe that any complex software dealing with messages and events can benefit from fuzzing. Using randomness to produce combinations of messages and events along with the verification logic for the end result is a very powerful approach. And it's not only about databases, compilers, and CLI tools - you may successfully add fuzzers to applications of almost any kind.&lt;/p&gt;

&lt;p&gt;As for the QuestDB team, we want to improve our fuzzers by adding tests dedicated to our SQL engine. Luckily, in the interim, the SQLancer team has come to our rescue.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL fuzzing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sqlancer/sqlancer"&gt;SQLancer&lt;/a&gt; (Synthesized Query Lancer) is a tool to automatically test SQL Database Management Systems (DBMS) to find logic bugs in their implementation. &lt;/p&gt;

&lt;p&gt;It operates in two phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database generation.&lt;/strong&gt; During this phase, SQLancer creates a populated database and stresses the DBMS to increase the probability of causing an inconsistent state. First, it creates random tables. Then, random SQL statements are chosen to generate, modify, and delete data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing.&lt;/strong&gt; Here, SQLancer detects logic bugs based on the generated database.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both of the above phases support a number of testing approaches, depending on the supported database. For instance, the testing phase supports the so-called Non-optimizing Reference Engine Construction (NoREC) approach. NoREC is aimed to find optimization bugs. It translates a query that may be optimized by the database into another query for which optimizations are much less likely to be applicable. Then it runs both queries and compares the result. &lt;/p&gt;

&lt;p&gt;Interestingly, we do a similar thing when testing our SIMD &lt;a href="https://questdb.io/blog/2022/01/12/jit-sql-compiler/"&gt;JIT compiler&lt;/a&gt;. We also have a &lt;a href="https://github.com/questdb/questdb/blob/6335238526d445d026550c8f34a3cf7b5da02090/core/src/test/java/io/questdb/test/griffin/CompiledFilterRegressionTest.java"&gt;non-fuzz test&lt;/a&gt; that runs a query with an enabled and disabled JIT compiler that then checks if the result sets are equal. Such an approach is usually called &lt;a href="https://en.wikipedia.org/wiki/Test_oracle"&gt;test oracle&lt;/a&gt; (or just oracle).&lt;/p&gt;

&lt;p&gt;Initial QuestDB support was contributed to SQLancer by &lt;a href="https://github.com/SuriZhang"&gt;Suri Zhang&lt;/a&gt; and after that, we received &lt;a href="https://github.com/questdb/questdb/issues/created_by/YuanchengJiang"&gt;a number of GH issues&lt;/a&gt; from the SQLancer team. We're very thankful to the SQLancer team for all of the reported issues, and continue to fix them in new releases. Needless to say: we'll keep using SQLancer to find bugs in the SQL engine. We're also &lt;a href="https://github.com/sqlancer/sqlancer/pull/798"&gt;working on a patch&lt;/a&gt; to improve QuestDB support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I need fuzzing?
&lt;/h2&gt;

&lt;p&gt;We believe that the answer is "yes, you do". Fuzzers are valuable for any complex software. Fuzz tests are not only about databases, compilers, and CLI tools - you may successfully add them to applications of almost any kind. It doesn't mean that you should go all-in for this kind of test and nothing else, but writing a fuzzer, as an addition, once you've written "traditional" tests, helped us to build a more robust database and it will certainly help you.&lt;/p&gt;

&lt;p&gt;As usual, we encourage you to try the latest QuestDB release and share your feedback with our &lt;a href="https://slack.questdb.io/"&gt;Slack Community&lt;/a&gt;. You can also play with our &lt;a href="https://demo.questdb.io/"&gt;live demo&lt;/a&gt; to see how fast it executes your queries. And, of course, contributions to our &lt;a href="https://github.com/questdb/questdb"&gt;open-source database on GitHub&lt;/a&gt; are more than welcome.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Article written by Andrei Pechkurov. Follow him on &lt;a href="https://twitter.com/AndreyPechkurov"&gt;Twitter&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>database</category>
      <category>testing</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
