<?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: Marvin Strauch</title>
    <description>The latest articles on DEV Community by Marvin Strauch (@mstrauch).</description>
    <link>https://dev.to/mstrauch</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4006562%2Fc7f5aae8-ecb4-4e2b-be3e-842a87923e9a.jpg</url>
      <title>DEV Community: Marvin Strauch</title>
      <link>https://dev.to/mstrauch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mstrauch"/>
    <language>en</language>
    <item>
      <title>Two Megabytes for Four Lines</title>
      <dc:creator>Marvin Strauch</dc:creator>
      <pubDate>Fri, 03 Jul 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/mstrauch/two-megabytes-for-four-lines-131p</link>
      <guid>https://dev.to/mstrauch/two-megabytes-for-four-lines-131p</guid>
      <description>&lt;p&gt;An unremarkable list in the customer area, a handful of editorially maintained entries. Just a few lines of text, a date and a heading. &lt;em&gt;In theory&lt;/em&gt;, no big deal.&lt;/p&gt;

&lt;p&gt;And yet everyone who switched to the page with the list in the customer area saw a blank page for a brief moment, then everything appeared at once. A story about how you push two megabytes through the internet for a small list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blank page
&lt;/h2&gt;

&lt;p&gt;When something is slow, my first port of call is the network tab. There I found something that didn't fit four lines of text at all. The browser was loading a WebAssembly file of around 840 kilobytes, &lt;code&gt;sqlite3.wasm&lt;/code&gt;, plus a compressed block of data of a little over a megabyte. Together, just under two megabytes. What's going on here?&lt;/p&gt;

&lt;p&gt;A look at the content framework in use, &lt;a href="https://content.nuxt.com/" rel="noopener noreferrer"&gt;Nuxt Content&lt;/a&gt;, explained the two megabytes. Since version 3 the content lives in &lt;a href="https://content.nuxt.com/blog/v3" rel="noopener noreferrer"&gt;an embedded SQLite database&lt;/a&gt;. On the server that's quick, since the database saves re-parsing Markdown on every request.&lt;/p&gt;

&lt;p&gt;But this database doesn't have to stay on the server. If you query Content in the browser, &lt;a href="https://content.nuxt.com/docs/advanced/database" rel="noopener noreferrer"&gt;Nuxt downloads the database dump and builds a SQLite database in the browser from it via WebAssembly&lt;/a&gt;. After that all queries run locally. For an application that has to work offline, that's a good trade-off.&lt;/p&gt;

&lt;p&gt;It was easy to miss, because the first request comes from the server as finished HTML, with no WebAssembly at all. Only when you then navigate within the running application, via a link for instance, do you trigger the query in the browser itself. That's exactly what happens when you switch to the page with the list.&lt;/p&gt;

&lt;p&gt;The engine alone weighs 840 kilobytes, and that cost is incurred every time, before the first response, whether the database holds four entries or four thousand. Only the block of data on top grows with the content. But all of that would only have slowed the page down, not blocked its loading. There had to be a second bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  How top-level await and Suspense blocked the page
&lt;/h2&gt;

&lt;p&gt;So I took a closer look at the list component. Reduced to the essentials, it looked like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;changelog&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;queryCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;changelog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem sits in the &lt;code&gt;await&lt;/code&gt; on the first line, and to see it you have to know what Vue does with it.&lt;/p&gt;

&lt;p&gt;A top-level &lt;code&gt;await&lt;/code&gt; in &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; makes the component asynchronous. For such components Vue requires a &lt;a href="https://vuejs.org/guide/built-ins/suspense.html" rel="noopener noreferrer"&gt;Suspense&lt;/a&gt; block higher up the tree, and Nuxt wraps that block around the whole page. Suspense works on an all-or-nothing principle and shows its content only once every asynchronous component inside it is done. Until then the page stays blank. Used deliberately it's useful, for instance to wait for important data before showing a half-finished page.&lt;/p&gt;

&lt;p&gt;Here, though, the download of two megabytes and the building of a SQLite database in the browser hung off that one &lt;code&gt;await&lt;/code&gt;. As long as that was running, the component wasn't done, so Suspense held the whole page back. Only once everything was there did it appear all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving queryCollection to the server with useFetch and lazy
&lt;/h2&gt;

&lt;p&gt;The first step was to get rid of the two megabytes. Nuxt runs on the server engine &lt;a href="https://nuxt.com/docs/4.x/guide/concepts/server-engine" rel="noopener noreferrer"&gt;"Nitro"&lt;/a&gt;, through which you can also provide your own API endpoints, and &lt;code&gt;queryCollection&lt;/code&gt; works &lt;a href="https://content.nuxt.com/docs/utils/query-collection" rel="noopener noreferrer"&gt;on the server side&lt;/a&gt; too. So I moved querying the database to the server. A small route fetches the latest entries and returns them as JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;queryCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;changelog&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="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date&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="s1"&gt;DESC&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="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&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;The engine stays on the server, only four records land in the browser as JSON, limited to the fields the list actually needs. The WebAssembly is no longer loaded, because no content query runs in the browser anymore.&lt;/p&gt;

&lt;p&gt;Now the blocking &lt;code&gt;await&lt;/code&gt; still has to go. The component now fetches the data in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/changelog&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="na"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;lazy: true&lt;/code&gt; makes the difference, because when you switch to the page the component no longer waits for the response. It renders straight away, and the list appears as soon as the few kilobytes arrive. Two megabytes and a blocked page turned into a request that hopefully no one notices anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a database in the browser at all?
&lt;/h2&gt;

&lt;p&gt;When data is loaded is an architectural decision in its own right. It can happen at build time (static site generation), per request on the server (server-side rendering), or only in the browser (client-side rendering). The later you load the data, the more dynamic the content and the more load shifts to the user. &lt;a href="https://docs.astro.build/en/guides/content-collections/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; and Next.js mostly stay right at the front, at build time, and deliver finished HTML. Nuxt Content goes all the way to the client end and lets &lt;code&gt;queryCollection&lt;/code&gt; run in the browser too, where the same line drags in a whole database engine. For an application that filters there constantly, it can pay off. For my list, admittedly a questionable choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A common stumbling block with sqlite3.wasm
&lt;/h2&gt;

&lt;p&gt;The real problem is the "convenient" access to this feature and the hidden cost of the abstraction. A content query directly in the component is the most obvious approach and shows up in every example. That it drags a database engine into the browser appears in the &lt;a href="https://content.nuxt.com/docs/advanced/database" rel="noopener noreferrer"&gt;docs&lt;/a&gt; only as an advantage, without the price. Whoever pays it often notices only at deployment time. The WASM file &lt;a href="https://github.com/nuxt/content/issues/2992" rel="noopener noreferrer"&gt;breaks under strict content security policies&lt;/a&gt;, &lt;a href="https://github.com/nuxt/content/issues/3051" rel="noopener noreferrer"&gt;demands its own COOP and COEP headers&lt;/a&gt;, or &lt;a href="https://github.com/nuxt/content/issues/3290" rel="noopener noreferrer"&gt;blows past the 3 MB limit of a worker on Cloudflare&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I use it again?
&lt;/h2&gt;

&lt;p&gt;The bug is fixed. Would I reach for Nuxt Content again? Yes, with one caveat. The start is unmatched in speed. You drop Markdown into the project and query it, with no separate CMS you'd first have to set up and deploy. For a small site or an early stage, it's a perfect fit.&lt;/p&gt;

&lt;p&gt;But the easy start comes at a cost. Content and code live in the same repository and ship with the same build, so even a text correction costs a full release. A dedicated CMS would separate the two. For a magazine that publishes several times a day, that's a no-go. For an application with a few editorial updates a month, it's perfectly enough.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Idempotency and reconciliation, learned the hard way</title>
      <dc:creator>Marvin Strauch</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:05:00 +0000</pubDate>
      <link>https://dev.to/mstrauch/idempotency-and-reconciliation-learned-the-hard-way-2n88</link>
      <guid>https://dev.to/mstrauch/idempotency-and-reconciliation-learned-the-hard-way-2n88</guid>
      <description>&lt;p&gt;Two bugs, both mine, with symptoms that looked unrelated and the same cause underneath, code that only works when everything goes as planned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The machine that shouldn't exist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="err"&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;    &lt;span class="k"&gt;WARNING&lt;/span&gt;: REMOTE HOST IDENTIFICATION HAS CHANGED!    @
&lt;span class="err"&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&lt;/span&gt;
&lt;span class="k"&gt;IT&lt;/span&gt; IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
&lt;span class="k"&gt;The&lt;/span&gt; fingerprint for the ED25519 key sent by the remote host is
&lt;span class="k"&gt;SHA256&lt;/span&gt;:8pLZ0r3nKqVxe2Tj9wMb1c...
&lt;span class="k"&gt;Host&lt;/span&gt; key verification failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone who works over SSH a lot knows this warning, and most of the time it's harmless. The server was reinstalled, the key changed, you clear a line out of &lt;a href="https://man.openbsd.org/ssh#VERIFYING_HOST_KEYS" rel="noopener noreferrer"&gt;known_hosts&lt;/a&gt; and move on. This time it wasn't so easy to resolve.&lt;/p&gt;

&lt;p&gt;The message showed up on a system that had been neither reinstalled nor changed in any way. The SSH login a few minutes earlier had worked fine. So the warning had to be taken seriously, because the fingerprint for that address really didn't match the machine that was supposed to be there. You connect over the IP address, and it points unambiguously to exactly one VM. So my first reaction, against better judgment, was that this simply couldn't be.&lt;/p&gt;

&lt;p&gt;But that's exactly what it was. A little digging turned up a second VM answering on the same address. Two different servers were responding under one IP, sometimes one, sometimes the other. That switch is exactly what the SSH warning above reports.&lt;/p&gt;

&lt;p&gt;Digging in, the second VM turned out to be an artifact that was never cleaned up. In theory it should have been gone weeks ago. I could trace that the VM had been right in the middle of being deleted when, weeks earlier, I ran a deployment and had to kill a few containers abruptly. That shouldn't normally happen, but it does, and not only through human action. Systems fail, networks drop out. A deletion job should never be left in a corrupt state, whatever the cause.&lt;/p&gt;

&lt;p&gt;Deleting a VM consists of several sub-steps that span quite different systems, among them the database, billing, DNS, and the hypervisor. Simplified, the flow looks roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DeleteVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vmID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vmID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HostID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InstanceID&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;You could say everything about it is wrong. First the shared resource, the IP, is released while the machine it's assigned to is still running. Then the system deletes the database entry that records which machine on which hypervisor belongs to this IP. If a process dies at exactly this moment, after the database delete and before the delete on the hypervisor, an orphaned VM is left behind.&lt;/p&gt;

&lt;p&gt;The delete action does have a retry mechanism, but it gets stuck too. Asked about the machine, the system can only answer "doesn't exist," because the entry that linked database and hypervisor is already gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clone that got in its own way
&lt;/h2&gt;

&lt;p&gt;The second story begins in the logs, where the same line kept stacking up. "snapshot 'clone-…' already exists." A VM needed to be cloned several times, just several copies of one source VM, nothing exotic.&lt;/p&gt;

&lt;p&gt;Technically it works like this. For each clone, a temporary snapshot of the source VM is created, the copy is made from it, and finally the snapshot is deleted. The snapshot's name is derived deterministically from the job ID. Simplified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;cloneFromSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;snapName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"clone-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"snapshot cleanup failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetID&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;At first glance that looks reasonable. The defer runs in almost every error case, but only almost. And it is the only place that clears the snapshot away again, so that cleanup can fail in two ways.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The defer doesn't run at all.&lt;/li&gt;
&lt;li&gt;It runs, but the DeleteSnapshot call inside it fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And both are common in practice. Off the top of my head, three scenarios lead into one branch or the other.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If several clone operations start against the same source VM, the hypervisor refuses. It allows only one operation at a time and locks the source VM while a clone is running, so the jobs pile up behind that lock.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If a worker that is cloning a VM exits "ungracefully," the cloning is cut off mid-process. A defer then no longer runs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A transient error keeps the cleanup call from getting through. That sort of thing just happens.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which scenario struck first here, I can't say for sure. The result is the same every time. A snapshot is left behind on the source.&lt;/p&gt;

&lt;p&gt;And from that first leftover on, every new attempt of the same job derived the same name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1: create snapshot ✓ → clone ✗ (source locked) → cleanup ✗ (source locked)
Attempt 2: create snapshot ✗ "already exists"
Attempt 3: create snapshot ✗ "already exists"
…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At some point the system gave up. The snapshot stayed put on the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why distributed systems are so hard to get right
&lt;/h2&gt;

&lt;p&gt;Deletion thought through too naively, and cloning that blocks itself. Both flows consist of several steps across system boundaries, and both were built only for the case where every sub-step succeeds reliably. Dissect the code like this and the problems behind it seem almost obvious. So how does it happen anyway?&lt;/p&gt;

&lt;p&gt;A good part of the reason is in us. We think in sequences, first this step, then the next, all on a single timeline. On a single machine that picture works quite well. If a program crashes, the operating system reclaims the memory and the database rolls back its open transaction, and afterwards everything is consistent again, as if nothing had happened.&lt;/p&gt;

&lt;p&gt;A distributed system doesn't give you that safety. A command like "delete this VM" touches several systems at once, the database, the IP pool, and the hypervisor. No transaction ties them together. If the command breaks off after the second of three steps, the first step stays in effect and the third never happens. Across system boundaries there is also no rewind button that restores the consistent state from before. And even if you rebuild the rollback by hand, it is subject to the same limits as the original command. No one guarantees it runs cleanly. That is not intuitive to us at first.&lt;/p&gt;

&lt;p&gt;This partial failure stays invisible until it hits. Jim Waldo and his colleagues called this the real difference back in 1994, in &lt;a href="https://github.com/papers-we-love/papers-we-love/blob/main/distributed_systems/a-note-on-distributed-computing.pdf" rel="noopener noreferrer"&gt;"A Note on Distributed Computing"&lt;/a&gt;. One component fails, the rest keeps running, and no one knows reliably how far the failed part got. The path where everything succeeds is simply the easiest to picture, which is why the half-finished states in between tend to slip through. But every step that can fail on its own multiplies the number of possible states, and that space grows faster than anyone can hold in their head. The first of the famous &lt;a href="https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing" rel="noopener noreferrer"&gt;fallacies of distributed computing&lt;/a&gt; already says it, "the network is reliable," and it isn't. And that applies just as much to the call that's supposed to clean up at the end.&lt;/p&gt;

&lt;p&gt;There's a handful of proven patterns against all this. None of them is new, and three are enough for our two stories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency.&lt;/strong&gt; An operation is idempotent if its second, third, nth call does nothing more than the first. That makes a retry harmless, even when the previous attempt broke off mid-work. The next run just copes with whatever the last attempt left behind. Amazon made this a house rule in &lt;a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/" rel="noopener noreferrer"&gt;"Making retries safe with idempotent APIs"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconciliation.&lt;/strong&gt; Instead of relying on every single event arriving and being processed, a loop regularly compares the desired state with the actual state and corrects the difference, no matter how it came about. Kubernetes, for example, is built around exactly this &lt;a href="https://kubernetes.io/docs/concepts/architecture/controller/" rel="noopener noreferrer"&gt;principle&lt;/a&gt;. The technical term is &lt;a href="https://hackernoon.com/level-triggering-and-reconciliation-in-kubernetes-1f17fe30333d" rel="noopener noreferrer"&gt;level-triggered rather than edge-triggered&lt;/a&gt;. In plain terms, you work from the current state itself instead of trying to catch every individual signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordination.&lt;/strong&gt; Where several actors touch the same resource, one place has to decide who may act and when. The obvious solution is a dedicated lock, but it brings a whole class of new bugs with it. In &lt;a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" rel="noopener noreferrer"&gt;"How to do distributed locking"&lt;/a&gt;, Martin Kleppmann took apart what makes such locks fail, from paused processes that keep writing after their lock has expired to missing fencing tokens and clocks you can't trust. Often it is more robust to avoid a second system altogether and coordinate through the database you already trust. It orders competing writes reliably, so its own state can take over the role of the lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The machine first, then the address
&lt;/h2&gt;

&lt;p&gt;The fix turns the order around and makes every step idempotent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DeleteVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vmID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vmID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&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="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HostID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InstanceID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&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="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrAlreadyFree&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="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteVM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;Now the system only releases the IP once the machine it was assigned to is really gone. If the process breaks off somewhere, the entry stays, and with it the information about what's still to be done. A retry runs through cleanly, because what's already done counts as success, not as an error.&lt;/p&gt;

&lt;p&gt;That still leaves one problem. How many such artifacts exist already, and how do you catch the ones the fixed code still misses? The answer is reconciliation. I built a check that compares the desired state in the database against the actual state on the hypervisors and reports every discrepancy. Some get fixed right away, some need a look by hand. That way orphaned machines surface reliably instead of running on unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete before create
&lt;/h2&gt;

&lt;p&gt;The first bug in the cloning was that creation wasn't idempotent. That's exactly what changed. If a temporary snapshot is left over from an earlier attempt, every new attempt now cleans it up before creating its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;prepareSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;snapName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"clone-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;

    &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSnapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="p"&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"clone-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;snapName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanupOrphan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&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="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hypervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapName&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;The second bug was that the hypervisor's lock mechanism was ignored. You can't clone from the same source at the same time, so the system has to order the jobs itself and work through them one after another, without any extra locking. The order comes from the job table itself. Per source, the worker that atomically claims the oldest active clone is the one that runs, and the rest wait until it's done. The job's status doubles as its lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains
&lt;/h2&gt;

&lt;p&gt;None of these problems needs millions of jobs in the queue. The challenges of distributed systems hit early, often with the first handful of workers.&lt;/p&gt;

&lt;p&gt;No single fix covers all of this. The patterns help, but they don't guarantee complete safety, distributed systems are too complex for that. What's left is discipline. With every multi-step operation, assume the worst case. The process dies right after the step where it can't afford to stop. Two jobs hit the same resource at once. The cleanup call never arrives.&lt;/p&gt;




&lt;p&gt;I recently started writing about building and running infrastructure. If that is your kind of thing, follow me here on DEV or subscribe to new posts at &lt;a href="https://marvinstrauch.de/newsletter/" rel="noopener noreferrer"&gt;marvinstrauch.de/newsletter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>distributedsystems</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
