<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>barrucadu&#39;s memos - General</title>
  <link href="https://memo.barrucadu.co.uk/taxon/general.xml" rel="self" />
  <link href="https://memo.barrucadu.co.uk/" />
  <id>https://memo.barrucadu.co.uk/taxon/general.xml</id>
  <author>
    <name>Michael Walker</name>
    <email>mike@barrucadu.co.uk</email>
  </author>
  
  <updated>2022-04-03T00:00:00Z</updated>
  
  
  <entry>
    <title>How DNS works</title>
    <link href="https://memo.barrucadu.co.uk/how-dns-works.html" />
    <id>https://memo.barrucadu.co.uk/how-dns-works.html</id>
    <published>2022-04-03T00:00:00Z</published>
    <updated>2022-04-03T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>The Domain Name System is a huge distributed eventually-consistent database<a href="how-dns-works.html#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> mapping names, like <code>memo.barrucadu.co.uk</code>, to numbers, like <code>116.203.34.201</code>. It’s federated, with trusted entities (you may have heard of the “DNS root servers”) delegating control of segments of the DNS namespace to others. It holds hundreds of millions of records, and updates to this database are typically visible in minutes to hours.</p>
<p>And the protocol behind it is not massively different to when it was standardised in the 1980s.</p>
<p>In this memo I’ll cover:</p>
<ol type="1">
<li>The DNS protocol</li>
<li>How your browser gets from <code>memo.barrucadu.co.uk</code> to an IP address</li>
<li>What a “zone” is</li>
<li>The difference between authoritative, recursive, and forwarding nameservers</li>
<li>What happens when you update a DNS record (there’s no such thing as “propagation”)</li>
<li>Finally, whether these old standards I’m talking about are still enough, today</li>
</ol>
<p>If you want to get it straight from the horse’s mouth, <a href="https://datatracker.ietf.org/doc/html/rfc1034">RFC 1034: Domain Names - Concepts and Facilities</a> and <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035: Domain Names - Implementation and Specification</a> are the standards I’m drawing on. They’re very approachable, and I encourage you to read them.</p>
<p>You can also look at <a href="https://github.com/barrucadu/resolved">resolved</a>, the DNS server I wrote, which acts as both a recursive (or forwarding) and authoritative nameserver, and is suitable for home networks. Well, my home network. I can’t promise anything about yours.</p>
<h2 id="the-dns-protocol">The DNS protocol</h2>
<p>Let’s start with an example:<a href="how-dns-works.html#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></p>
<pre><code>$ dig memo.barrucadu.co.uk +noedns
; &lt;&lt;&gt;&gt; DiG 9.16.25 &lt;&lt;&gt;&gt; memo.barrucadu.co.uk +noedns
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 37169
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 0

;; QUESTION SECTION:
;memo.barrucadu.co.uk.          IN      A

;; ANSWER SECTION:
memo.barrucadu.co.uk.   292     IN      CNAME   barrucadu.co.uk.
barrucadu.co.uk.        292     IN      A       116.203.34.201

;; AUTHORITY SECTION:
barrucadu.co.uk.        2975    IN      NS      ns-98.awsdns-12.com.
barrucadu.co.uk.        2975    IN      NS      ns-1520.awsdns-62.org.
barrucadu.co.uk.        2975    IN      NS      ns-1828.awsdns-36.co.uk.
barrucadu.co.uk.        2975    IN      NS      ns-763.awsdns-31.net.

;; Query time: 0 msec
;; SERVER: 185.12.64.2#53(185.12.64.2)
;; WHEN: Tue Mar 22 16:42:02 GMT 2022
;; MSG SIZE  rcvd: 202</code></pre>
<p>I’ve used <code>dig</code> a lot so I’m fairly used to reading this output, but I’ve since realised I wasn’t <em>really</em> reading it.</p>
<p>What does <code>flags: qr rd ra</code> mean?</p>
<p>The <code>QUESTION SECTION</code> and <code>ANSWER SECTION</code> make sense, but what’s the point of the <code>AUTHORITY SECTION</code>? Do <em>all</em> queries have an <code>AUTHORITY SECTION</code>?</p>
<pre><code>$ dig www.google.com +noedns
; &lt;&lt;&gt;&gt; DiG 9.16.25 &lt;&lt;&gt;&gt; www.google.com +noedns
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 46676
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         102     IN      A       142.250.185.100

;; Query time: 0 msec
;; SERVER: 185.12.64.2#53(185.12.64.2)
;; WHEN: Tue Mar 22 16:49:36 GMT 2022
;; MSG SIZE  rcvd: 48</code></pre>
<p>…no <code>AUTHORITY SECTION</code> there. Is it unimportant? Or optional?</p>
<p>Also, all the domain names there have a trailing dot. What’s that about?<a href="how-dns-works.html#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a></p>
<p>Time to dig into the protocol. <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a> is our guide here.</p>
<h3 id="format-of-a-dns-message">Format of a DNS Message</h3>
<p>DNS has two types of messages, queries and responses, and uses port 53. It prefers UDP but, if a message is too long to send in a single UDP datagram, it falls back to TCP.</p>
<p>A DNS message has five parts. These are:</p>
<ol type="1">
<li><p>A header, which specifies what sort of message this is and how many entries are in the other parts. This also has those flags we saw in the <code>dig</code> output.</p></li>
<li><p>The “question section”, which specifies what sort of records the client is interested in. Did you know that you can ask multiple questions with a single DNS query? I didn’t.</p></li>
<li><p>The “answer section”, a collection of records directly answering the questions.</p></li>
<li><p>The “authority section”, a series of <code>NS</code> records pointing to an authoritative source which can answer the questions.</p></li>
<li><p>The “additional section”, a series of records which may be useful when using records from the answer and authority sections. For example, the <code>A</code> records for any nameservers given in the authority section.</p></li>
</ol>
<p>The answer, authority, and additional sections won’t be present in a query. But the question section <em>will</em> be present in a response: it’s copied over from the query.</p>
<h3 id="the-header">The Header</h3>
<p>The header is 12 bytes long and has a few different fields packed in there. <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a> has some nice ASCII art illustrations:</p>
<pre class="asciiart"><code>                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+</code></pre>
<p>Where,</p>
<ul>
<li><p><code>ID</code> is a 16-bit random identifier set by the client and copied into the response by the server. Since UDP is connectionless, this is essential for the client to know which response goes with which query.<a href="how-dns-works.html#fn4" class="footnote-ref" id="fnref4" role="doc-noteref"><sup>4</sup></a></p></li>
<li><p><code>QR</code> indicates whether this is a query (0) or a response (1).</p></li>
<li><p><code>OPCODE</code> is a four-bit field, set by the client and copied into the response by the server, indicating what type of query this message is. The most common opcode is 0, which is a “standard query”.</p></li>
<li><p><code>AA</code> (“Authoritative Answer”) is set by the server and means that this response is <em>authoritative</em>.</p>
<p>More on authority in <a href="how-dns-works.html#zones">zones?</a></p></li>
<li><p><code>TC</code> (“Truncation”) is set by the server and means that the full response couldn’t fit in a single UDP datagram, and so the client should try again using TCP.<a href="how-dns-works.html#fn5" class="footnote-ref" id="fnref5" role="doc-noteref"><sup>5</sup></a></p></li>
<li><p><code>RD</code> (“Recursion Desired”) is set by the client, and copied into the response by the server, and means that they would like the server to answer the question recursively, if they can.</p>
<p>More on recursive and non-recursive resolution in <a href="how-dns-works.html#how-resolution-happens">how resolution happens</a>.</p></li>
<li><p><code>RA</code> (“Recursion Available”) is set by the server and means that it can perform recursive resolution, if requested.</p></li>
<li><p><code>Z</code> is reserved for future use, and so should be set to zero if you don’t implement those future standards.</p></li>
<li><p><code>RCODE</code> is a four-bit field, set by the server, indicating what type of response this message is. There are a few common ones:</p>
<ul>
<li>0 means no error</li>
<li>1 means the server couldn’t understand the query</li>
<li>2 means the server encountered an error processing the query</li>
<li>3 means the domain name in the query doesn’t exist</li>
<li>4 means the server doesn’t support this sort of query</li>
<li>5 means the server refused to answer the query</li>
</ul></li>
<li><p><code>QDCOUNT</code>, <code>ANCOUNT</code>, <code>NSCOUNT</code>, and <code>ARCOUNT</code> are unsigned 16-bit (big endian) integers specifying the number of entries in the question, answer, authority, and additional sections (respectively) of the message.</p></li>
</ul>
<p>Since all the multi-byte fields in a DNS message are unsigned and big endian, I’ll not mention it from now on.</p>
<h3 id="domain-names">Domain Names</h3>
<p>Before diving into the other sections, let’s have a look at how domain names are encoded. They show up a lot, after all.</p>
<p>Let’s take the domain name <code>memo.barrucadu.co.uk.</code>, and separate it by dots. This gives us a sequence of <em>labels</em>:</p>
<ol type="1">
<li><code>memo</code></li>
<li><code>barrucadu</code></li>
<li><code>co</code></li>
<li><code>uk</code></li>
<li>(the empty label)</li>
</ol>
<p>How you actually interpret those labels is a bit confused, unfortunately.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a> says that they are sequences of arbitrary octets and that you can’t assume any particular character encoding… but it <em>also</em> says that labels are to be compared case-insensitively.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc4343">RFC 4343</a> clarifies that that means octets in the range <code>0x41</code> to <code>0x5a</code> (the upper case ASCII letters) are considered equal to corresponding octets in the range <code>0x61</code> to <code>0x7a</code> (the lower case ASCII letters), and vice versa, but that that <em>still</em> doesn’t mean that labels are ASCII, as they can also contain arbitrary non-ASCII octets.</p>
<p>But there’s also <a href="https://datatracker.ietf.org/doc/html/rfc3492">RFC 3492</a>, which defines the punycode standard for encoding internationalised, <em>i.e.</em> unicode, domain names into ASCII. So maybe domain names <em>are</em> ASCII after all?</p>
<p>There may well be a later RFC which resolves this ambiguity and says that labels are definitely ASCII, but I haven’t seen it yet.</p>
<p>Anyway, back to the topic of encoding.</p>
<p>A label is encoded as a one-octet length field followed by the octets of the label. And an encoded domain name is a sequence of encoded labels. This means that a domain name ends with <code>0x00</code>, the length of the empty label.<a href="how-dns-works.html#fn6" class="footnote-ref" id="fnref6" role="doc-noteref"><sup>6</sup></a></p>
<p>So <code>memo.barrucadu.co.uk</code> is encoded as:</p>
<pre><code>0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00</code></pre>
<p>There are two restrictions on the validity of domain names:</p>
<ul>
<li><p>A single label may be no more than 63 octets long (not including the length octet)</p></li>
<li><p>An entire encoded domain name may be no more than 255 octets long (including the label length octets)</p></li>
</ul>
<h4 id="compression">Compression</h4>
<p>Unfortunately, that’s not all.</p>
<p>Domain names get repeated a lot in DNS messages, and the 512 bytes of a UDP datagram can start to feel pretty limiting. So DNS also has a compression mechanism, where some suffix of a domain name can be replaced with a pointer to an earlier occurrence of that name.</p>
<p>So if the name <code>memo.barrucadu.co.uk.</code> appears in a message twice, the second occurrence could be represented as:</p>
<ul>
<li><code>memo.barrucadu.co.uk.</code></li>
<li><code>memo.barrucadu.co.[pointer to uk.]</code></li>
<li><code>memo.barrucadu.[pointer to co.uk.]</code></li>
<li><code>memo.[pointer to barrucadu.co.uk.]</code></li>
<li><code>[pointer to memo.barrucadu.co.uk.]</code></li>
</ul>
<p>But how do you distinguish between a regular label and a pointer? Well, remember that a label can’t be longer than 63 octets. And what’s 63 as an 8-bit binary number?</p>
<p>It’s <code>00111111</code>.</p>
<p>There’s two whole bits there at the front which are completely wasted!</p>
<p>So pointers are encoded as the two-octet sequence <code>11[14-bit index into message]</code>.</p>
<p>Pretty clever.</p>
<h3 id="questions">Questions</h3>
<pre class="asciiart"><code>                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+</code></pre>
<p>Where,</p>
<ul>
<li><p><code>QNAME</code> is the domain name, which can be any length (so long as it’s properly encoded), it’s not padded to any specific size.</p></li>
<li><p><code>QTYPE</code> is a 16-bit integer specifying the type of records the client is interested in. Which will usually be a record type (see the next subsection) or 255, meaning “all records”. There are a few other <code>QTYPE</code>s but those are less common.</p></li>
<li><p><code>QCLASS</code> is a 16-bit integer specifying which <em>network class</em> the client is interested in. These days this will always be 1, or <code>IN</code>, for “internet”.<a href="how-dns-works.html#fn7" class="footnote-ref" id="fnref7" role="doc-noteref"><sup>7</sup></a></p></li>
</ul>
<p>We can now understand the question section of our <code>dig</code> example!</p>
<pre><code>;; QUESTION SECTION:
;memo.barrucadu.co.uk.          IN      A</code></pre>
<p>Means that it’s looking for an internet address record for <code>memo.barrucadu.co.uk.</code> (yes, it shows the type and class the other way around). That question is encoded as:</p>
<pre><code>0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00  ; qname:  memo.barrucadu.co.uk.
0x00 0x01                                                   ; qtype:  A
0x00 0x01                                                   ; qclass: IN</code></pre>
<h3 id="resource-records">Resource Records</h3>
<p>The answer, authority, and additional sections are all a sequence of <em>resource records</em>:</p>
<pre class="asciiart"><code>                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+</code></pre>
<p>Where,</p>
<ul>
<li><p><code>NAME</code> is the domain name, which is variable-length like the <code>QNAME</code> of a question.</p></li>
<li><p><code>TYPE</code> is a 16-bit integer specifying what sort of record this is. There are a fair few of these, but some common ones are:</p>
<ul>
<li>1, an <code>A</code> record</li>
<li>2, a <code>NS</code> record</li>
<li>5, a <code>CNAME</code> record</li>
<li>28, a <code>AAAA</code> record (from <a href="https://datatracker.ietf.org/doc/html/rfc3596">RFC 3596</a>)</li>
<li>and plenty others</li>
</ul></li>
<li><p><code>CLASS</code> is a 16-bit integer specifying what network class this record applies to. Like the <code>QCLASS</code>, these days this will always be 1. Unless you’re specifically running some sort of old non-IP-based network for fun.</p></li>
<li><p><code>TTL</code> is a 32-bit integer specifying the number of seconds that this record is valid for. This is important for caching purposes. Zero has a special meaning: it means that you can use the record to do whatever it is you’re doing <em>right now</em>, but that you can’t cache it at all.</p></li>
<li><p><code>RDLENGTH</code> is a 16-bit integer specifying the length of the <code>RDATA</code> section.</p></li>
<li><p><code>RDATA</code> is the record data, which is type- and class-specific. For example:</p>
<ul>
<li><code>IN A</code> records hold an IPv4 address, as a 32-bit number</li>
<li><code>IN NS</code> and <code>IN CNAME</code> records hold a domain name</li>
<li><code>IN AAAA</code> records hold an IPv6 address, as a 128-bit number</li>
</ul></li>
</ul>
<p>Returning to our <code>dig</code> example, we had a few different resource records in the response. Let’s just look at the answer section:</p>
<pre><code>;; ANSWER SECTION:
memo.barrucadu.co.uk.   292     IN      CNAME   barrucadu.co.uk.
barrucadu.co.uk.        292     IN      A       116.203.34.201</code></pre>
<p>We have one <code>IN CNAME</code> record for <code>memo.barrucadu.co.uk.</code> and one <code>IN A</code> record for <code>barrucadu.co.uk.</code>. This is because, upon encountering a <code>CNAME</code>, resolution starts again with whatever name the <code>CNAME</code> refers to.<a href="how-dns-works.html#fn8" class="footnote-ref" id="fnref8" role="doc-noteref"><sup>8</sup></a></p>
<p>Leaving out the name compression for simplicity, those records are encoded as:</p>
<pre><code>0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00  ; name:     memo.barrucadu.co.uk.
0x00 0x05                                                   ; type:     CNAME
0x00 0x01                                                   ; class:    IN
0x00 0x00 0x01 0x24                                         ; ttl:      292
0x00 0x11                                                   ; rdlength: 17
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00               ; rdata:    barrucadu.co.uk.

0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00               ; name:     barrucadu.co.uk.
0x00 0x01                                                   ; type:     A
0x00 0x01                                                   ; class:    IN
0x00 0x00 0x01 0x24                                         ; ttl:      292
0x00 0x04                                                   ; rdlength: 4
0x74 0xcb 0x22 0xc9                                         ; rdata:    116.203.34.201</code></pre>
<h3 id="example-dns-query-response">Example DNS query &amp; response</h3>
<p>Returning to our <code>dig memo.barrucadu.co.uk +noedns</code> example from the beginning, we can now see the whole encoded query and response. I’ve included comments and linebreaks to make it clear what’s what.</p>
<p>Here’s the query:</p>
<pre><code>;; header
0xb6 0x54 ; ID: 46676
0x01 0x00 ; flags: RD
0x00 0x01 ; QDCOUNT: 1
0x00 0x00 ; ANCOUNT: 0
0x00 0x00 ; NSCOUNT: 0
0x00 0x00 ; ARCOUNT: 0

;; question section
; memo.barrucadu.co.uk. A IN
0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01</code></pre>
<p>And here’s the response (omitting compression):</p>
<pre><code>;; header
0xb6 0x54 ; ID: 46676
0x81 0x80 ; flags: QR, RD, RA
0x00 0x01 ; QDCOUNT: 1
0x00 0x02 ; ANCOUNT: 2
0x00 0x04 ; NSCOUNT: 4
0x00 0x00 ; ARCOUNT: 0

;; question section
; memo.barrucadu.co.uk. A IN
0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01

;; answer section
; memo.barrucadu.co.uk. CNAME IN 292 barrucadu.co.uk.
0x04 m e m o 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x05 0x00 0x01 0x00 0x00 0x01 0x24 0x00 0x11 0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00
; barrucadu.co.uk. A IN 292 116.203.34.201
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x01 0x00 0x01 0x00 0x00 0x01 0x24 0x00 0x04 0x74 0xcb 0x22 0xc9

;; authority section
; barrucadu.co.uk. NS IN 2975 ns-98.awsdns-12.com.
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x02 0x00 0x01 0x00 0x00 0x0b 0x9f 0x00 0x15 0x05 n s - 9 8 0x09 a w s d n s - 1 2 0x03 c o m 0x00
; barrucadu.co.uk. NS IN 2975 ns-1520.awsdns-62.org.
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x02 0x00 0x01 0x00 0x00 0x0b 0x9f 0x00 0x17 0x07 n s - 1 5 2 0 0x09 a w s d n s - 6 2 0x03 o r g 0x00
; barrucadu.co.uk. NS IN 2975 ns-1828.awsdns-36.co.uk.
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x02 0x00 0x01 0x00 0x00 0x0b 0x9f 0x00 0x19 0x07 n s - 1 8 2 8 0x09 a w s d n s - 3 6 0x02 c o 0x02 u k 0x00
; barrucadu.co.uk. NS IN 2975 ns-763.awsdns-31.net.
0x09 b a r r u c a d u 0x02 c o 0x02 u k 0x00 0x00 0x02 0x00 0x01 0x00 0x00 0x0b 0x9f 0x00 0x16 0x06 n s - 7 6 3 0x09 a w s d n s - 3 1 0x03 n e t 0x00</code></pre>
<p>And that’s that!</p>
<p>The DNS protocol isn’t very complicated. But it <em>is</em> somewhat fiddly, what with each record type having its own <code>RDATA</code> format, and the domain name compression. One big thing I learned implementing <a href="https://github.com/barrucadu/resolved">resolved</a> is to <em>always</em> fuzz test your serialisation and deserialisation logic.</p>
<h2 id="how-resolution-happens">How resolution happens</h2>
<p>When we ran <code>dig memo.barrucadu.co.uk +noedns</code> in the previous section, we got an answer. We found the IP address which <code>memo.barrucadu.co.uk.</code> refers to.</p>
<p>But <em>how?</em></p>
<p>Well, <code>dig</code> tells us that it talked to some server at 185.12.64.2. But how did <em>that</em> server know? Does it have a copy of the entire DNS? Unlikely, since there are hundreds of millions of records in use.</p>
<p>The answer is that the server followed a process called <em>recursive resolution</em>. This is described in section 5.3.3 of <a href="https://datatracker.ietf.org/doc/html/rfc1034">RFC 1034</a>:</p>
<ol type="1">
<li>See if we already know the answer (<em>e.g.</em> the relevant records are already cached), and return it to the client if so</li>
<li>Figure out the best nameservers to ask</li>
<li>Send them queries until one responds</li>
<li>Analyse the response:
<ul>
<li>If the response answers the question, cache it and return it to the client</li>
<li>If the response gives some better nameservers to use, cache them and go back to step 2</li>
<li>If the response gives a CNAME, and this is not the answer, cache the CNAME record and start again with the new name</li>
<li>If the response is an error or doesn’t make sense, go back to step 3</li>
</ul></li>
</ol>
<p>On the face of it this looks pretty straightforward… but on closer inspection that step 2 is doing a lot of work: how exactly do we “figure out the best nameservers to ask”?<a href="how-dns-works.html#fn9" class="footnote-ref" id="fnref9" role="doc-noteref"><sup>9</sup></a></p>
<p>Well, step 4.b gives us a clue here: <em>if the response gives some better nameservers to use, cache them and go back to step 2.</em> So we don’t need to pick the correct nameservers at the very beginning. We only need to know about a nameserver which will be able to point us to a nameserver which knows that (or is closer to knowing that).</p>
<p>There are thirteen nameservers which, transitively, know about <em>every</em> domain name. These are the root nameservers, and they’re where recursive resolution starts.</p>
<p>You can find them at <code>a.root-servers.net.</code> through <code>m.root-servers.net.</code></p>
<p>So you just point your recursive resolver at, say, <code>j.root-servers.net.</code> and… oh wait, we have a chicken-and-egg problem. Ultimately, you need to know their IP addresses. IANA, the Internet Assigned Numbers Authority, provides the <a href="https://www.iana.org/domains/root/files">“root hints” file</a>, which has the IPv4 and IPv6 addresses of these root nameservers.</p>
<p>How do you download that file if you don’t have DNS working to resolve <code>www.iana.org.</code>? Look, you just need IP addresses to get DNS and DNS to get IP addresses. Use 1.1.1.1 or something while you get your fancy recursive resolver working.</p>
<p>Alright, let’s resolve <code>memo.barrucadu.co.uk.</code> recursively! Starting with:</p>
<pre><code>$ dig memo.barrucadu.co.uk @j.root-servers.net
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; memo.barrucadu.co.uk @j.root-servers.net
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 48477
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 8, ADDITIONAL: 17
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;memo.barrucadu.co.uk.          IN      A

;; AUTHORITY SECTION:
uk.                     172800  IN      NS      dns1.nic.uk.
uk.                     172800  IN      NS      dns4.nic.uk.
uk.                     172800  IN      NS      nsa.nic.uk.
uk.                     172800  IN      NS      nsd.nic.uk.
uk.                     172800  IN      NS      nsc.nic.uk.
uk.                     172800  IN      NS      nsb.nic.uk.
uk.                     172800  IN      NS      dns3.nic.uk.
uk.                     172800  IN      NS      dns2.nic.uk.

;; ADDITIONAL SECTION:
dns1.nic.uk.            172800  IN      A       213.248.216.1
dns1.nic.uk.            172800  IN      AAAA    2a01:618:400::1
dns4.nic.uk.            172800  IN      A       43.230.48.1
dns4.nic.uk.            172800  IN      AAAA    2401:fd80:404::1
nsa.nic.uk.             172800  IN      A       156.154.100.3
nsa.nic.uk.             172800  IN      AAAA    2001:502:ad09::3
nsd.nic.uk.             172800  IN      A       156.154.103.3
nsd.nic.uk.             172800  IN      AAAA    2610:a1:1010::3
nsc.nic.uk.             172800  IN      A       156.154.102.3
nsc.nic.uk.             172800  IN      AAAA    2610:a1:1009::3
nsb.nic.uk.             172800  IN      A       156.154.101.3
nsb.nic.uk.             172800  IN      AAAA    2001:502:2eda::3
dns3.nic.uk.            172800  IN      A       213.248.220.1
dns3.nic.uk.            172800  IN      AAAA    2a01:618:404::1
dns2.nic.uk.            172800  IN      A       103.49.80.1
dns2.nic.uk.            172800  IN      AAAA    2401:fd80:400::1

;; Query time: 4 msec
;; SERVER: 2001:503:c27::2:30#53(2001:503:c27::2:30)
;; WHEN: Sat Apr 02 23:20:04 BST 2022
;; MSG SIZE  rcvd: 553</code></pre>
<p>Alright, we now know the names and IP addresses of the <code>uk.</code> nameservers. Thanks, additional section!</p>
<p>On we go:</p>
<pre><code>$ dig memo.barrucadu.co.uk @213.248.216.1
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; memo.barrucadu.co.uk @213.248.216.1
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 43056
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;memo.barrucadu.co.uk.          IN      A

;; AUTHORITY SECTION:
barrucadu.co.uk.        172800  IN      NS      ns-98.awsdns-12.com.
barrucadu.co.uk.        172800  IN      NS      ns-763.awsdns-31.net.
barrucadu.co.uk.        172800  IN      NS      ns-1520.awsdns-62.org.
barrucadu.co.uk.        172800  IN      NS      ns-1828.awsdns-36.co.uk.

;; Query time: 14 msec
;; SERVER: 213.248.216.1#53(213.248.216.1)
;; WHEN: Sat Apr 02 23:21:28 BST 2022
;; MSG SIZE  rcvd: 183</code></pre>
<p>No additional section here, so we’ll need to resolve one of those nameservers. Back to the root!</p>
<pre><code>$ dig ns-98.awsdns-12.com @j.root-servers.net
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; ns-98.awsdns-12.com @j.root-servers.net
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 8418
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 13, ADDITIONAL: 27
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;ns-98.awsdns-12.com.           IN      A

;; AUTHORITY SECTION:
com.                    172800  IN      NS      a.gtld-servers.net.
com.                    172800  IN      NS      b.gtld-servers.net.
com.                    172800  IN      NS      c.gtld-servers.net.
com.                    172800  IN      NS      d.gtld-servers.net.
com.                    172800  IN      NS      e.gtld-servers.net.
com.                    172800  IN      NS      f.gtld-servers.net.
com.                    172800  IN      NS      g.gtld-servers.net.
com.                    172800  IN      NS      h.gtld-servers.net.
com.                    172800  IN      NS      i.gtld-servers.net.
com.                    172800  IN      NS      j.gtld-servers.net.
com.                    172800  IN      NS      k.gtld-servers.net.
com.                    172800  IN      NS      l.gtld-servers.net.
com.                    172800  IN      NS      m.gtld-servers.net.

;; ADDITIONAL SECTION:
a.gtld-servers.net.     172800  IN      A       192.5.6.30
b.gtld-servers.net.     172800  IN      A       192.33.14.30
c.gtld-servers.net.     172800  IN      A       192.26.92.30
d.gtld-servers.net.     172800  IN      A       192.31.80.30
e.gtld-servers.net.     172800  IN      A       192.12.94.30
f.gtld-servers.net.     172800  IN      A       192.35.51.30
g.gtld-servers.net.     172800  IN      A       192.42.93.30
h.gtld-servers.net.     172800  IN      A       192.54.112.30
i.gtld-servers.net.     172800  IN      A       192.43.172.30
j.gtld-servers.net.     172800  IN      A       192.48.79.30
k.gtld-servers.net.     172800  IN      A       192.52.178.30
l.gtld-servers.net.     172800  IN      A       192.41.162.30
m.gtld-servers.net.     172800  IN      A       192.55.83.30
a.gtld-servers.net.     172800  IN      AAAA    2001:503:a83e::2:30
b.gtld-servers.net.     172800  IN      AAAA    2001:503:231d::2:30
c.gtld-servers.net.     172800  IN      AAAA    2001:503:83eb::30
d.gtld-servers.net.     172800  IN      AAAA    2001:500:856e::30
e.gtld-servers.net.     172800  IN      AAAA    2001:502:1ca1::30
f.gtld-servers.net.     172800  IN      AAAA    2001:503:d414::30
g.gtld-servers.net.     172800  IN      AAAA    2001:503:eea3::30
h.gtld-servers.net.     172800  IN      AAAA    2001:502:8cc::30
i.gtld-servers.net.     172800  IN      AAAA    2001:503:39c1::30
j.gtld-servers.net.     172800  IN      AAAA    2001:502:7094::30
k.gtld-servers.net.     172800  IN      AAAA    2001:503:d2d::30
l.gtld-servers.net.     172800  IN      AAAA    2001:500:d937::30
m.gtld-servers.net.     172800  IN      AAAA    2001:501:b1f9::30

;; Query time: 3 msec
;; SERVER: 2001:503:c27::2:30#53(2001:503:c27::2:30)
;; WHEN: Sat Apr 02 23:22:36 BST 2022
;; MSG SIZE  rcvd: 844</code></pre>
<p>We’ve got the <code>com.</code> nameservers. Next!<a href="how-dns-works.html#fn10" class="footnote-ref" id="fnref10" role="doc-noteref"><sup>10</sup></a></p>
<pre><code>$ dig ns-98.awsdns-12.com @192.5.6.30
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; ns-98.awsdns-12.com @192.5.6.30
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 59687
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 9
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ns-98.awsdns-12.com.           IN      A

;; AUTHORITY SECTION:
awsdns-12.com.          172800  IN      NS      g-ns-13.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-588.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-1164.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-1740.awsdns-12.com.

;; ADDITIONAL SECTION:
g-ns-13.awsdns-12.com.  172800  IN      A       205.251.192.13
g-ns-13.awsdns-12.com.  172800  IN      AAAA    2600:9000:5300:d00::1
g-ns-588.awsdns-12.com. 172800  IN      A       205.251.194.76
g-ns-588.awsdns-12.com. 172800  IN      AAAA    2600:9000:5302:4c00::1
g-ns-1164.awsdns-12.com. 172800 IN      A       205.251.196.140
g-ns-1164.awsdns-12.com. 172800 IN      AAAA    2600:9000:5304:8c00::1
g-ns-1740.awsdns-12.com. 172800 IN      A       205.251.198.204
g-ns-1740.awsdns-12.com. 172800 IN      AAAA    2600:9000:5306:cc00::1

;; Query time: 23 msec
;; SERVER: 192.5.6.30#53(192.5.6.30)
;; WHEN: Sat Apr 02 23:24:01 BST 2022
;; MSG SIZE  rcvd: 317</code></pre>
<p>Nearly there… each query gets us a step or two closer.</p>
<pre><code>$ dig ns-98.awsdns-12.com @205.251.192.13
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; ns-98.awsdns-12.com @205.251.192.13
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 43579
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 9
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ns-98.awsdns-12.com.           IN      A

;; ANSWER SECTION:
ns-98.awsdns-12.com.    172800  IN      A       205.251.192.98

;; AUTHORITY SECTION:
awsdns-12.com.          172800  IN      NS      g-ns-1164.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-13.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-1740.awsdns-12.com.
awsdns-12.com.          172800  IN      NS      g-ns-588.awsdns-12.com.

;; ADDITIONAL SECTION:
g-ns-1164.awsdns-12.com. 172800 IN      A       205.251.196.140
g-ns-1164.awsdns-12.com. 172800 IN      AAAA    2600:9000:5304:8c00::1
g-ns-13.awsdns-12.com.  172800  IN      A       205.251.192.13
g-ns-13.awsdns-12.com.  172800  IN      AAAA    2600:9000:5300:d00::1
g-ns-1740.awsdns-12.com. 172800 IN      A       205.251.198.204
g-ns-1740.awsdns-12.com. 172800 IN      AAAA    2600:9000:5306:cc00::1
g-ns-588.awsdns-12.com. 172800  IN      A       205.251.194.76
g-ns-588.awsdns-12.com. 172800  IN      AAAA    2600:9000:5302:4c00::1

;; Query time: 13 msec
;; SERVER: 205.251.192.13#53(205.251.192.13)
;; WHEN: Sat Apr 02 23:24:41 BST 2022
;; MSG SIZE  rcvd: 333</code></pre>
<p>We’ve got an IP address for <code>ns-98.awsdns-12.com.</code>! Now we can answer our original question:</p>
<pre><code>$ dig memo.barrucadu.co.uk @205.251.192.98
; &lt;&lt;&gt;&gt; DiG 9.16.27 &lt;&lt;&gt;&gt; memo.barrucadu.co.uk @205.251.192.98
;; global options: +cmd
;; Got answer:
;; -&gt;&gt;HEADER&lt;&lt;- opcode: QUERY, status: NOERROR, id: 26684
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;memo.barrucadu.co.uk.          IN      A

;; ANSWER SECTION:
memo.barrucadu.co.uk.   300     IN      CNAME   barrucadu.co.uk.
barrucadu.co.uk.        300     IN      A       116.203.34.201

;; AUTHORITY SECTION:
barrucadu.co.uk.        172800  IN      NS      ns-1520.awsdns-62.org.
barrucadu.co.uk.        172800  IN      NS      ns-1828.awsdns-36.co.uk.
barrucadu.co.uk.        172800  IN      NS      ns-763.awsdns-31.net.
barrucadu.co.uk.        172800  IN      NS      ns-98.awsdns-12.com.

;; Query time: 13 msec
;; SERVER: 205.251.192.98#53(205.251.192.98)
;; WHEN: Sat Apr 02 23:25:37 BST 2022
;; MSG SIZE  rcvd: 213</code></pre>
<p>And we’re done, after 6 requests to other nameservers. And in a real nameserver implementation, we’d be checking before each of those requests whether we already had the answer cached, so likely some of them (eg, the request to find the <code>com.</code> nameservers) wouldn’t have been needed.</p>
<h2 id="zones">Zones?</h2>
<p>In the previous section, it looked very much like the DNS was broken up into subtrees (or “zones”, if you will) based on the label structure:</p>
<ul>
<li>The <code>.</code> nameservers knew about the <code>com.</code> and <code>uk.</code> nameservers, but couldn’t answer queries about subdomains of those directly</li>
<li>Similarly, the <code>uk.</code> nameservers knew about the nameservers for <code>barrucadu.co.uk.</code>, but not any of its other records</li>
<li>And likewise for the <code>com.</code> nameservers and <code>awsdns-12.com.</code></li>
</ul>
<p>This makes sense. Imagine if the root nameservers knew every DNS record! Their databases would be <em>huge!</em> It would be infeasible to run a handful of servers which know hundreds of millions of records and which the whole world uses.</p>
<p>So <code>.</code> is a zone. And <code>uk.</code> is a zone. And <code>barrucadu.co.uk.</code> is a zone. All of the TLDs are zones, and every domain you can buy creates a new zone. A zone can be bigger than a single label, <em>e.g.</em> <code>foo.bar.baz.barrucadu.co.uk.</code> is in the <code>barrucadu.co.uk.</code> zone unless I <em>delegate</em> it to someone else, by creating some <code>NS</code> records for, say, <code>baz.barrucadu.co.uk.</code></p>
<p>That’s exactly how registering a domain name works, by the way. The registrars have privileged access to the TLD nameservers, and you pay them some money for them to send a message to the nameservers saying “please delegate <code>barrucadu</code> to these other nameservers”.</p>
<p>Zones are traditionally represented in a textual format defined in <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a>.<a href="how-dns-works.html#fn11" class="footnote-ref" id="fnref11" role="doc-noteref"><sup>11</sup></a> You’ve seen this format before: it’s the format <code>dig</code> gives its responses in and it’s the format of the root hints file (and the root zone file, also provided by IANA).</p>
<p>Here’s the zone file I use for my LAN DNS:</p>
<pre><code>$ORIGIN lan.

@ 300 IN SOA @ @ 4 300 300 300 300

router         300 IN A     10.0.0.1

nyarlathotep   300 IN A     10.0.0.3
*.nyarlathotep 300 IN CNAME nyarlathotep

help           300 IN CNAME nyarlathotep
*.help         300 IN CNAME help

nas            300 IN CNAME nyarlathotep</code></pre>
<p>It’s a list of records, but note that they all use relative domain names (no dot at the end). I could write them as absolute domain names, but that would be repetitive, and who doesn’t want to golf their zone files? The <code>$ORIGIN</code> line at the top is used to complete any relative names, and the <code>@</code> is an alias for the origin, so this zone file could also be written as:</p>
<pre><code>lan. 300 IN SOA lan. lan. 4 300 300 300 300

router.lan.         300 IN A     10.0.0.1

nyarlathotep.lan.   300 IN A     10.0.0.3
*.nyarlathotep.lan. 300 IN CNAME nyarlathotep.lan.

help.lan.           300 IN CNAME nyarlathotep.lan.
*.help.lan.         300 IN CNAME help.lan.

nas.lan.            300 IN CNAME nyarlathotep.lan.</code></pre>
<p>Zones come in two types: <em>authoritative</em> (also just called a zone, or a master zone) and <em>non-authoritative</em> (also called hints). An authoritative zone has a SOA record, and causes the nameserver to give authoritative responses to questions which fall into that zone.<a href="how-dns-works.html#fn12" class="footnote-ref" id="fnref12" role="doc-noteref"><sup>12</sup></a></p>
<p>Non-authoritative zones don’t, and are primarily useful as a sort of permanent cache. Take the root hints file for example: all recursive resolvers need to know the <code>NS</code> records for <code>.</code>. But they should <em>not</em> act as if they’re authoritative for <code>.</code>, they just know a little bit about it.</p>
<p>Since any nameserver could claim to be authoritative for any zone it wants, and I’m sure malicious nameservers often do try to claim ownership of big sites like <code>google.com.</code>, how does the DNS work?</p>
<p>It works on trust.</p>
<p>You trust that the root nameservers will give you the correct nameservers for all the TLDs. You then, in turn, trust that the TLD nameservers will give the correct nameservers for the domains registered under those TLDs. And so on, all the way down to the domain you actually want to resolve.</p>
<p>Not every nameserver operator will be equally trustworthy or competent, so that trust does erode somewhat as you move further and further away from the root, but if you do some basic validation of DNS responses (<em>e.g.</em> rejecting a response with NS records for a domain which is not a subdomain of the zone which you know this nameserver to be authoritative for), you can do pretty well.</p>
<h2 id="types-of-nameserver">Types of nameserver</h2>
<p>There are, broadly speaking, three sorts of nameservers you see:</p>
<ul>
<li><p><em>Authoritative nameservers</em> are the source of truth for records about a given zone. Typically, these refuse to answer questions for other zones. These set the <code>AA</code> flag for queries falling into their zones and return a “name error” response if a name they are authoritative for doesn’t exist.<a href="how-dns-works.html#fn13" class="footnote-ref" id="fnref13" role="doc-noteref"><sup>13</sup></a></p>
<p>In <a href="https://github.com/barrucadu/resolved">resolved</a> this is implemented by the <a href="https://github.com/barrucadu/resolved/blob/master/lib-dns-resolver/src/nonrecursive.rs"><code>dns_resolver::nonrecursive</code> module</a>.</p></li>
<li><p><em>Recursive nameservers</em> (or <em>recursive resolvers</em>) perform recursive resolution for anyone who wants it. For example: 1.1.1.1, 8.8.8.8, and the nameserver your ISP operates. Typically, these are not authoritative for any zones. Recursive nameservers are convenient because the client doesn’t need to implement the recursive algorithm themselves: they can just fire off a query and get the response.<a href="how-dns-works.html#fn14" class="footnote-ref" id="fnref14" role="doc-noteref"><sup>14</sup></a></p>
<p>In <a href="https://github.com/barrucadu/resolved">resolved</a> this is implemented by the <a href="https://github.com/barrucadu/resolved/blob/master/lib-dns-resolver/src/recursive.rs"><code>dns_resolver::recursive</code> module</a>.</p></li>
<li><p><em>Forwarding nameservers</em> (or <em>forwarding resolvers</em>) forward all queries to a recursive resolver, rather than do the recursive resolution themselves. Typically, these are not authoritative for any zones. Forwarding nameservers are simpler than recursive nameservers, and they’re useful for the same reason any other sort of proxy is: they can increase cache hit rate (by having many clients go through the forwarding resolver), and selectively falsify or block records.<a href="how-dns-works.html#fn15" class="footnote-ref" id="fnref15" role="doc-noteref"><sup>15</sup></a></p>
<p>In <a href="https://github.com/barrucadu/resolved">resolved</a> this is implemented by the <a href="https://github.com/barrucadu/resolved/blob/master/lib-dns-resolver/src/forwarding.rs"><code>dns_resolver::forwarding</code> module</a>.</p></li>
</ul>
<p>Of course, there’s no reason a single nameserver can’t do all of those things at the same time!</p>
<p>Consider bind, <em>the</em> big-name nameserver. Check out its <a href="https://www-uxsup.csx.cam.ac.uk/pub/doc/redhat/redhat7.3/rhl-rg-en-7.3/s1-bind-configuration.html">configuration documentation</a>: it says <em>any</em> zone can authoritative, forwarded, or hints, and the <code>allow-recursion</code> option configures whether recursive queries for zones the server doesn’t know about are allowed.</p>
<p>My <a href="https://github.com/barrucadu/resolved">resolved</a> server by default supports authoritative zones and recursive resolution. It may not appear to support bind-style zone-specific forwarding, but you could implement that with a hints file containing NS records for the zone you want to forward, and there is a command-line flag to forward <em>all</em> recursive queries to some other server.</p>
<p>The reason you’d want to make a nameserver do only one sort of resolution is to make operation simpler. In particular, it’s good practice for internet-facing authoritative nameservers to <em>only</em> perform non-recursive resolution. Answering or rejecting queries based only on local data makes them have much more predictable performance.</p>
<h2 id="dns-doesnt-propagate">DNS doesn’t “propagate”</h2>
<p>When I first got into all this web development stuff, the common wisdom was that DNS changes took 24 to 48 hours to propagate. But having seen some details of the DNS protocol and how recursive resolution works, does that really make sense? Shouldn’t changes be visible as soon as the TTL of the old record expires? And shouldn’t new records be visible immediately? Why do changes need to propagate? Where do they propagate to?</p>
<p>Propagation implies a push model, where you make your changes and then they get sent to the resolvers which need them. But that’s not what happens at all: instead, caches expire.</p>
<p>Ok, there <em>are</em> two cases in which DNS does propagate:</p>
<ol type="1">
<li>If you update your domain’s NS records, your registrar needs to push those changes to the TLD nameservers. Apparently this used to be kind of slow, like, 20+ years ago. These days it’s very fast.</li>
<li>If you run a very high traffic authoritative nameserver, you’ll operate multiple instances of it around the world to improve reliability and latency. So if you change a record, that change needs to be pushed out to all your servers. But this should take under a minute unless something is very wrong.</li>
</ol>
<p>My hunch is that this 24 to 48 hour window came from:</p>
<ul>
<li>Registrars being slow to update the TLD nameservers once upon a time</li>
<li>ISPs running notoriously poorly-behaving nameservers</li>
</ul>
<p>Ah, ISP DNS. Almost the first thing any self-respecting nerd changes when setting up a new home network. They often do nefarious things like redirect misspelled domain names to ad-covered search pages, trying to profit off your typos. And, as it turns out, a lot of them ignore record TTLs, and will cache something for a long period if they feel like it.</p>
<p>How long? Well, I’ve seen reports of 24 hours…</p>
<p>Well, no matter what the cause of the occasional slow DNS update is—though I can’t say I’ve experienced slow DNS updates in a very long time, and updates are evidently fast enough for changing an A record to be considered a viable failover mechanism for big sites—“propagation” is the wrong mental model.</p>
<p>DNS is pull, not push.</p>
<h2 id="are-rfcs-1034-and-1035-enough">Are RFCs 1034 and 1035 enough?</h2>
<p>I’ve been running <a href="https://github.com/barrucadu/resolved">resolved</a> for my LAN for about two and a half weeks now. And it’s working pretty well! Ok, I have implemented a few more RFCs:</p>
<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc2782">RFC 2782</a>, which defines the SRV record type, because Minecraft can use SRV records to detect the correct port number of a server (but that’s totally optional, you can also just type in the port in the game client).</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc3596">RFC 3596</a>, which defines the AAAA<a href="how-dns-works.html#fn16" class="footnote-ref" id="fnref16" role="doc-noteref"><sup>16</sup></a> type, because I wanted to be able to read the official, and unchanged, root hints file. But I don’t have IPv6 at home so I could make do without this.</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc6761">RFC 6761</a>, which defines some zones with special behaviour, which I distribute as zone files. This was actually the motivation for me to implement authoritative zones, previously I was only going to support hints and <a href="https://pi-hole.net/">Pi-hole</a>-like ad-blocking through hosts files.</li>
</ul>
<p>So there are a few things. But what I’ve covered in this memo is, more or less, enough to implement a working nameserver. You’d need to look up the formats of a few more common record types in <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a>, and also the full algorithm for non-recursive resolution in <a href="https://datatracker.ietf.org/doc/html/rfc1034">RFC 1034</a> (which I glossed over in a single sentence), but the point is that DNS is not very complicated, even today.</p>
<p>There have been new record types; there have been security extensions; there have been clarifications; some zones have been given special meaning. But all of that is optional.</p>
<p>Certainly for a home network, RFCs 1034 and 1035 are enough.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>You could even call it a “NoSQL” database, if you really must.<a href="how-dns-works.html#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>The <code>+noedns</code> flag turns off some extensions to the basic DNS protocol, which I’m not covering for simplicity.<a href="how-dns-works.html#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>Ok, I’ve actually known this one for a while, because I’m the sort of person to pedantically bring that up.<a href="how-dns-works.html#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn4"><p>Well, this plus source port matching. There are also some other security mechanisms DNS clients sometimes use to prevent spoofed responses, like randomly capitalising letters in the question names (since DNS is case-insensitive), and checking that the response from the server uses the same random capitalisation.<a href="how-dns-works.html#fnref4" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn5"><p>Fun fact, Alpine Linux doesn’t support DNS over TCP, so it can break <a href="https://christoph.luppri.ch/fixing-dns-resolution-for-ruby-on-alpine-linux">if a truncated response doesn’t include enough complete records for it to make progress</a>.<a href="how-dns-works.html#fnref5" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn6"><p>And also makes encoded domain names work as null-terminated strings in C in the (very common) case where none of the labels contain a null byte. What a fortuitous coincidence!<a href="how-dns-works.html#fnref6" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn7"><p>It feels kind of wasteful that we effectively throw away 16 whole bits for each question and record on this historical artefact. UDP messages are short, so we compress domain names to squeeze out a little extra space, but then we waste a bunch like this! Even worse, there never were very many network classes: <a href="https://datatracker.ietf.org/doc/html/rfc1035">RFC 1035</a> only defines <em>four</em>. Did the IETF really expect there to be so many non-internet networks in the future?<a href="how-dns-works.html#fnref7" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn8"><p>Unless the query was for, say, <code>IN CNAME memo.barrucadu.co.uk.</code>. More on this in <a href="how-dns-works.html#how-resolution-happens">how resolution happens</a>.<a href="how-dns-works.html#fnref8" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn9"><p>That step 1 is also doing a surprising amount of work if your nameserver supports authoritative zones (see next section). For the full gory details, see section 4.3.2 of <a href="https://datatracker.ietf.org/doc/html/rfc1034">RFC 1034</a>.<a href="how-dns-works.html#fnref9" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn10"><p>I know it’s a necessary consequence of how DNS works, but I still find it pretty cool that there are servers which know about literally every <code>com.</code> (or <code>uk.</code>, or <code>net.</code>, etc) domain name.<a href="how-dns-works.html#fnref10" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn11"><p>Like the DNS protocol, this format appears to be straightforward but is annoyingly fiddly when you come to implement it. It’s almost (but not quite!) line-oriented, just about every field is optional, and there are two fields which can be written in either order. Just why?<a href="how-dns-works.html#fnref11" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn12"><p>See the next section for more on authoritative nameservers.<a href="how-dns-works.html#fnref12" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn13"><p>Note that there’s a difference between a domain not existing and a domain existing but having no records at all (or just no records matching the current query). An authoritative nameserver should only return a name error if it <em>actually</em> doesn’t exist.<a href="how-dns-works.html#fnref13" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn14"><p>In fact, the resolver your operating system uses is probably what’s called a “stub resolver”, rather than a recursive resolver. Try configuring your DNS resolver in <code>/etc/resolv.conf</code> to be one of the root nameservers, rather than a recursive resolver: it won’t work.<a href="how-dns-works.html#fnref14" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn15"><p>The <a href="https://pi-hole.net/">Pi-hole</a> is a forwarding resolver which blocks advertising domains by returning a fake <code>A</code> record pointing to some unusable IP address, like 0.0.0.0.<a href="how-dns-works.html#fnref15" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn16"><p>I used to read this as “A-A-A-A” but, having now typed and said it a bunch, I’ve switched to the less tounge-twistery “quad-A”. I wonder what actual networking people say.<a href="how-dns-works.html#fnref16" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Implementing a size-bounded LRU cache with expiring entries for my DNS server (in Rust)</title>
    <link href="https://memo.barrucadu.co.uk/dns-cache.html" />
    <id>https://memo.barrucadu.co.uk/dns-cache.html</id>
    <published>2022-03-07T00:00:00Z</published>
    <updated>2022-03-07T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>I’ve spent the last week or so implementing <a href="https://github.com/barrucadu/resolved">a recursive DNS resolver</a> in Rust. I’m not very good at either of those things, so this has been a bit of a learning experience.</p>
<p>This memo is about how I ended up implementing the caching layer. You don’t need to know much about DNS to follow this memo, just some basics:</p>
<ul>
<li>DNS is a distributed eventually-consistent database, and timeouts are how it achieves that eventual consistency.</li>
<li>The keys in this database are <strong>domain names</strong> (like <code>www.barrucadu.co.uk</code>) and the values are <strong>resource records</strong> (RRs for short).</li>
<li>A resource record has a <strong>type</strong> (like “A”, or “CNAME”), a <strong>class</strong> (like “IN”, for INternet), a <strong>ttl</strong> (how long it’s valid for), and some <strong>data</strong>.</li>
<li>Finally, the format of that data depends on the type (but not on the class).</li>
</ul>
<p>Let’s make this a bit more concrete:</p>
<pre class="rust"><code>// we&#39;ll need these later
use priority_queue::PriorityQueue;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::time::{Duration, Instant};

/// A resource record, or RR, is something we receive from another
/// nameserver, or which we send in answer to a client&#39;s query.
#[derive(Debug)]
pub struct ResourceRecord {
    pub name: DomainName,
    pub rtype: RecordTypeWithData,
    pub rclass: RecordClass,
    pub ttl: Duration,
}

/// A domain name is a sequence of &quot;labels&quot;, eg, `www.barrucadu.co.uk`
/// is made up of the labels `[&quot;www&quot;, &quot;barrucadu&quot;, &quot;co&quot;, &quot;uk&quot;, &quot;&quot;]`.
/// The final empty label is the root domain, which we normally don&#39;t
/// bother writing, but is meaningful in some contexts.
///
/// Incidentally, the final empty label means that in the DNS wire
/// format, names are null-terminated.  I&#39;m sure this isn&#39;t a
/// coincidence.
///
/// Labels are ASCII and case-insensitive, so make sure to construct
/// them correctly!
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DomainName {
    pub labels: Vec&lt;Vec&lt;u8&gt;&gt;,
}

/// Record data depends on its type, so this enum has one variant for
/// each type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecordTypeWithData {
    A { address: Ipv4Addr },
    CNAME { cname: DomainName }, // many more omitted
}

/// We&#39;ll also need a notion of record type *without* data.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum RecordType {
    A,
    CNAME, // many more omitted
}

impl RecordTypeWithData {
    pub fn rtype(&amp;self) -&gt; RecordType {
        match self {
            RecordTypeWithData::A { .. } =&gt; RecordType::A,
            RecordTypeWithData::CNAME { .. } =&gt; RecordType::CNAME,
            // many more omitted
        }
    }
}

/// The record class identifies which sort of network the record is
/// for.  For the purposes of this memo, let&#39;s only consider the
/// internet.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum RecordClass {
    IN,
}</code></pre>
<p>Before we go any further, there’s one final prerequisite. When you ask a DNS server for some records, you don’t say,</p>
<blockquote>
<p>Give me all records of such-and-such record type and record class for <code>www.barrucadu.co.uk</code>.</p>
</blockquote>
<p>You instead ask in terms of a <strong>query type</strong> and <strong>query class</strong>.</p>
<p>In this memo, you can think of those as just the record types and classes we’ve just defined, plus a wildcard to mean “match anything”:</p>
<pre class="rust"><code>#[derive(Debug, Copy, Clone)]
pub enum QueryType {
    Record(RecordType),
    Wildcard,
}

// does a record match a query, or a query match a record?  this is
// the way &#39;round I went for, but the other choice would make just as
// much sense.
impl RecordType {
    pub fn matches(&amp;self, qtype: &amp;QueryType) -&gt; bool {
        match qtype {
            QueryType::Wildcard =&gt; true,
            QueryType::Record(rtype) =&gt; rtype == self,
        }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum QueryClass {
    Record(RecordClass),
    Wildcard,
}

impl RecordClass {
    pub fn matches(&amp;self, qclass: &amp;QueryClass) -&gt; bool {
        match qclass {
            QueryClass::Wildcard =&gt; true,
            QueryClass::Record(rclass) =&gt; rclass == self,
        }
    }
}</code></pre>
<p>There are a few more in reality, but they’re not important for our purposes.</p>
<p>So we’ll use the <code>Record*</code> types to <strong>put</strong> values into the cache and the <code>Query*</code> types to <strong>get</strong> values from the cache.</p>
<h2 id="a-simple-cache">A Simple Cache</h2>
<p>Right, what’s the simplest possible cache we could implement?</p>
<p>Perhaps something like this:<a href="dns-cache.html#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>
<pre class="rust"><code>pub struct SimpleCache {
    entries: HashMap&lt;DomainName, Vec&lt;(RecordTypeWithData, RecordClass, Instant)&gt;&gt;,
}

impl SimpleCache {
    pub fn new() -&gt; Self {
        Self {
            entries: HashMap::new(),
        }
    }</code></pre>
<p>To put something in the cache, you just add it to the appropriate <code>Vec</code>:</p>
<pre class="rust"><code>    pub fn insert(&amp;mut self, name: &amp;DomainName, rr: ResourceRecord) {
        let entry = (rr.rtype, rr.rclass, Instant::now() + rr.ttl);
        if let Some(entries) = self.entries.get_mut(name) {
            entries.push(entry);
        } else {
            self.entries.insert(name.clone(), vec![entry]);
        }
    }</code></pre>
<p>What if the user inserts the same record twice?</p>
<p>Well, what about it? This is a proof-of-concept! The DNS resolver will return duplicate records I guess! Moving swiftly on…</p>
<p>To get something from the cache, just iterate over the appropriate <code>Vec</code>, pulling out all the records with the right type and class:</p>
<pre class="rust"><code>    pub fn get(
        &amp;self,
        name: &amp;DomainName,
        qtype: QueryType,
        qclass: QueryClass,
    ) -&gt; Vec&lt;ResourceRecord&gt; {
        let now = Instant::now();
        if let Some(entries) = self.entries.get(name) {
            let mut rrs = Vec::with_capacity(entries.len());
            for (rtype_with_data, rclass, expires) in entries {
                if rtype_with_data.rtype().matches(&amp;qtype) &amp;&amp; rclass.matches(&amp;qclass) {
                    rrs.push(ResourceRecord {
                        name: name.clone(),
                        rtype: rtype_with_data.clone(),
                        rclass: *rclass,
                        ttl: expires.saturating_duration_since(now),
                    });
                }
            }
            rrs
        } else {
            Vec::new()
        }
    }
}</code></pre>
<p>What if a record has expired?</p>
<p>Proof-of-concept! The caller can deal with that by checking expiration times or something!</p>
<p>So, this was the caching implementation I started with. It works, but it has some problems:</p>
<ul>
<li>There’s no deduplication.</li>
<li>There’s no expiration.</li>
<li>There’s no limit on the number of records.</li>
<li>All domains get put in one <code>HashMap</code>, totally ignoring their hierarchical label structure.</li>
<li>Getting records of one type involves iterating through records of another type.</li>
</ul>
<p>But it’s better than no cache!</p>
<h2 id="a-better-cache">A Better Cache</h2>
<p>Ok, how do we do better? The most egregious problems with the simple cache are the duplicate entries and the unbounded growth.</p>
<p>Using something that takes the hierarchical structure of domain names into account, like a trie, would also be nice, but I’m not dealing with enough live cache entries for that to be a concern yet.</p>
<p>So, how do we remove entries?</p>
<p>Well, we could periodically iterate over the entire cache, removing all expired entries. But if entries have a long expiration time, or just get accessed frequently enough, they won’t expire. So relying on expiration isn’t enough, we also need to occasionally remove <em>live</em> entries.</p>
<p>This sounds like a job for an LRU<a href="dns-cache.html#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> cache: a size-bounded LRU cache with expiring entries for my DNS server!</p>
<p>Before jumping straight to the <code>struct</code> definition, let’s think about how to model this:</p>
<ul>
<li><p>To solve the problem of iterating through records of unrelated types, we’ll need to subdivide the entries by type as well as domain name.</p></li>
<li><p>We’ll need to keep track of the most recent time each record has been accessed, so when the cache is full of unexpired records we can work out which one to evict first.</p></li>
<li><p>But the cache may be big. There could be hundreds or thousands of domains in there, each likely with multiple records. Iterating through the whole thing to find records to evict is a bad choice. We need a more efficient data structure to map from eviction priority to domain name.</p></li>
<li><p>For similar reasons, we don’t want to have to iterate through the entire cache to work out how big it is.</p></li>
</ul>
<p>My usual mantra for designing data structures is to “make illegal states unrepresentable”, but I don’t think that will work here. To make this cache <em>efficient</em>, we’ll need to denormalise the data, and make our code ensure the correct invariants hold. Testing helps with this (and indeed testing did find some bugs in my implementation).</p>
<p>So I decided to use a pair of priority queues<a href="dns-cache.html#fn3" class="footnote-ref" id="fnref3" role="doc-noteref"><sup>3</sup></a> to efficiently track (1) which domain is next to have an expiring record, and (2) which domain has been least recently used. I also decided to keep track of sizes and times throughout the data structure, rather than just in the records.</p>
<p>Here’s the new cache data structure:</p>
<pre class="rust"><code>#[derive(Debug)]
pub struct BetterCache {
    /// Cached records, indexed by domain name.
    entries: HashMap&lt;DomainName, CachedDomainRecords&gt;,

    /// Priority queue of domain names ordered by access times.
    ///
    /// When the cache is full and there are no expired records to
    /// prune, domains will instead be pruned in LRU order.
    ///
    /// INVARIANT: the domains in here are exactly the domains in
    /// `entries`.
    access_priority: PriorityQueue&lt;DomainName, Reverse&lt;Instant&gt;&gt;,

    /// Priority queue of domain names ordered by expiry time.
    ///
    /// When the cache is pruned, expired records are removed first.
    ///
    /// INVARIANT: the domains in here are exactly the domains in
    /// `entries`.
    expiry_priority: PriorityQueue&lt;DomainName, Reverse&lt;Instant&gt;&gt;,

    /// The number of records in the cache.
    ///
    /// INVARIANT: this is the sum of the `size` fields of the
    /// entries.
    current_size: usize,

    /// The desired maximum number of records in the cache.
    desired_size: usize,
}

#[derive(Debug)]
struct CachedDomainRecords {
    /// The time this record was last read at.
    last_read: Instant,

    /// When the next RR expires.
    ///
    /// INVARIANT: this is the minimum of the expiry times of the RRs.
    next_expiry: Instant,

    /// How many records there are.
    ///
    /// INVARIANT: this is the sum of the vector lengths in `records`.
    size: usize,

    /// The records, further divided by record type.
    ///
    /// INVARIANT: the `RecordType` and `RecordTypeWithData` match.
    records: HashMap&lt;RecordType, Vec&lt;(RecordTypeWithData, RecordClass, Instant)&gt;&gt;,
}

impl BetterCache {
    pub fn new() -&gt; Self {
        Self::with_desired_size(512)
    }

    pub fn with_desired_size(desired_size: usize) -&gt; Self {
        if desired_size == 0 {
            panic!(&quot;cannot create a zero-size cache&quot;);
        }

        Self {
            // `desired_size / 2` is a compromise: most domains will
            // have more than one record, so `desired_size` would be
            // too big for the `entries`.
            entries: HashMap::with_capacity(desired_size / 2),
            access_priority: PriorityQueue::with_capacity(desired_size),
            expiry_priority: PriorityQueue::with_capacity(desired_size),
            current_size: 0,
            desired_size,
        }
    }</code></pre>
<p>There are some invariants there in the comments. I’d prefer not to have those, but I don’t think there’s any getting around it given that we want better than linear time eviction.</p>
<p>This is substantially more complex than the <code>SimpleCache</code>, and the operations we’re about to define on it are too. Make sure this all makes sense before continuing. In particular, you might notice that I’ve opted to have the LRU eviction expire entire domain names, rather than individual records within them.</p>
<p>Let’s go through the new operations in order of complexity: querying, eviction, and insertion.</p>
<h3 id="getting-things-out">Getting things out</h3>
<p>This isn’t too bad:</p>
<pre class="rust"><code>    /// Get an entry from the cache.
    ///
    /// The TTL in the returned `ResourceRecord` is relative to the
    /// current time - not when the record was inserted into the
    /// cache.
    ///
    /// This entry may have expired: if so, the TTL will be 0.
    /// Consumers MUST check this before using the record!
    pub fn get(
        &amp;mut self,
        name: &amp;DomainName,
        qtype: &amp;QueryType,
        qclass: &amp;QueryClass,
    ) -&gt; Vec&lt;ResourceRecord&gt; {
        if let Some(entry) = self.entries.get_mut(name) {
            let now = Instant::now();
            let mut rrs = Vec::new();
            match qtype {
                QueryType::Wildcard =&gt; {
                    for tuples in entry.records.values() {
                        to_rrs(name, qclass, now, tuples, &amp;mut rrs);
                    }
                }
                QueryType::Record(rtype) =&gt; {
                    if let Some(tuples) = entry.records.get(rtype) {
                        to_rrs(name, qclass, now, tuples, &amp;mut rrs);
                    }
                }
            }
            if !rrs.is_empty() {
                entry.last_read = now;
                self.access_priority
                    .change_priority(name, Reverse(entry.last_read));
            }
            rrs
        } else {
            Vec::new()
        }
    }
}</code></pre>
<p>This is quite similar to what we had before. Sure, the extra layer of indirection adds a tad more complication, and there’s now a write operation in here (updating <code>last_read</code> and <code>access_priority</code>, which takes log time), but other than that nothing complex.</p>
<p>The <code>to_rrs</code> function just exists to prevent some code duplication:</p>
<pre class="rust"><code>/// Helper for `get_without_checking_expiration`: converts the cached
/// record tuples into RRs.
fn to_rrs(
    name: &amp;DomainName,
    qclass: &amp;QueryClass,
    now: Instant,
    tuples: &amp;[(RecordTypeWithData, RecordClass, Instant)],
    rrs: &amp;mut Vec&lt;ResourceRecord&gt;,
) {
    for (rtype, rclass, expires) in tuples {
        if rclass.matches(qclass) {
            rrs.push(ResourceRecord {
                name: name.clone(),
                rtype: rtype.clone(),
                rclass: *rclass,
                ttl: expires.saturating_duration_since(now),
            });
        }
    }
}</code></pre>
<p>If you’re following along at home, put that definition <em>outside</em> the <code>impl BetterCache</code> block.</p>
<h3 id="evicting-things">Evicting things</h3>
<p>Here’s are the simplest three functions in the entire <code>impl</code>:</p>
<pre class="rust"><code>    /// Delete all expired records, and then enough
    /// least-recently-used records to reduce the cache to the desired
    /// size.
    ///
    /// Returns the number of records deleted.
    pub fn prune(&amp;mut self) -&gt; usize {
        if self.current_size &lt;= self.desired_size {
            return 0;
        }

        let mut pruned = self.remove_expired();

        while self.current_size &gt; self.desired_size {
            pruned += self.remove_least_recently_used();
        }

        pruned
    }

    /// Helper for `prune`: deletes all records associated with the
    /// least recently used domain.
    ///
    /// Returns the number of records removed.
    fn remove_least_recently_used(&amp;mut self) -&gt; usize {
        if let Some((name, _)) = self.access_priority.pop() {
            self.expiry_priority.remove(&amp;name);

            if let Some(entry) = self.entries.remove(&amp;name) {
                let pruned = entry.size;
                self.current_size -= pruned;
                pruned
            } else {
                0
            }
        } else {
            0
        }
    }

    /// Delete all expired records.
    ///
    /// Returns the number of records deleted.
    pub fn remove_expired(&amp;mut self) -&gt; usize {
        let mut pruned = 0;

        loop {
            let before = pruned;
            pruned += self.remove_expired_step();
            if before == pruned {
                break;
            }
        }

        pruned
    }</code></pre>
<p>So simple! So straightforward! If only all my code could be like this.</p>
<ul>
<li><p><code>prune</code> shrinks the cache to the desired size by removing the expired entries and then removing enough domains (in LRU order) to get below the target.</p></li>
<li><p><code>remove_least_recently_used</code> pops an entry from the <code>access_priority</code> queue, removes it from the <code>expiry_priority</code> queue (which takes log time), and deletes it from the top-level <code>entries</code> map. It also updates the <code>current_size</code>, and returns the number of records it just deleted.</p></li>
<li><p><code>remove_expired</code> is deceptively simple. It looks easy at first glance, but it’s calling this <code>remove_expired_step</code> function in a loop, until no more get removed.</p></li>
</ul>
<p>Removing an entire domain is easy, but removing individual records from a domain is harder:</p>
<ul>
<li>The <code>size</code> of the domain will change.</li>
<li>The <code>next_expiry</code> of the domain may change.</li>
<li>Those changes need to be reflected in the top-level <code>current_size</code> and <code>expiry_priority</code> fields.</li>
<li>But if it’s the last record in the domain we should remove that entirely.</li>
</ul>
<p>Additionally, the queue gives us the domain name, and there may be one or more expiring records in it (or even zero, but that would be a bug).</p>
<p>With all that said, here’s the implementation:</p>
<pre class="rust"><code>    /// Helper for `remove_expired`: looks at the next-to-expire
    /// domain and cleans up expired records from it.  This may delete
    /// more than one record, and may even delete the whole domain.
    ///
    /// Returns the number of records removed.
    fn remove_expired_step(&amp;mut self) -&gt; usize {
        if let Some((name, Reverse(expiry))) = self.expiry_priority.pop() {
            let now = Instant::now();

            if expiry &gt; now {
                self.expiry_priority.push(name, Reverse(expiry));
                return 0;
            }

            if let Some(entry) = self.entries.get_mut(&amp;name) {
                let mut pruned = 0;

                let rtypes = entry.records.keys().cloned().collect::&lt;Vec&lt;RecordType&gt;&gt;();
                let mut next_expiry = None;
                for rtype in rtypes {
                    if let Some(tuples) = entry.records.get_mut(&amp;rtype) {
                        let len = tuples.len();
                        tuples.retain(|(_, _, expiry)| expiry &gt; &amp;now);
                        pruned += len - tuples.len();
                        for (_, _, expiry) in tuples {
                            match next_expiry {
                                None =&gt; next_expiry = Some(*expiry),
                                Some(t) if *expiry &lt; t =&gt; next_expiry = Some(*expiry),
                                _ =&gt; (),
                            }
                        }
                    }
                }

                entry.size -= pruned;

                if let Some(ne) = next_expiry {
                    entry.next_expiry = ne;
                    self.expiry_priority.push(name, Reverse(ne));
                } else {
                    self.entries.remove(&amp;name);
                    self.access_priority.remove(&amp;name);
                }

                self.current_size -= pruned;
                pruned
            } else {
                self.access_priority.remove(&amp;name);
                0
            }
        } else {
            0
        }
    }</code></pre>
<p>It’s pretty complex. We could describe it in pseudocode like so:</p>
<ol type="1">
<li>Pop the next expiring domain from the queue.</li>
<li>Check the current time:
<ul>
<li>If the expiry time is in the future, put it back in the queue and return.</li>
<li>Otherwise, get the cached records:
<ul>
<li>If there are no cached records, remove the domain from the access queue and return.</li>
<li>Otherwise:
<ol type="1">
<li>Iterate through all the records and check if each should expire:
<ul>
<li>If so, remove the record.</li>
<li>Otherwise, keep track of the soonest future expiry time seen.</li>
</ul></li>
<li>Check if this removed all the records:
<ul>
<li>If so, remove the domain from the cache.</li>
<li>Otherwise, put it back in the expiry queue with the new expiry time.</li>
</ul></li>
<li>Update the size fields.</li>
</ol></li>
</ul></li>
</ul></li>
</ol>
<p>In outline, fairly simple. In implementation, not fairly simple. Maybe someone better at Rust would be able to write this in a clearer way, but this is what I’ve got.</p>
<p>Incidentally, one of the bugs found by testing (by inserting randomly generated entries, pruning the expired ones, and checking the invariants) was that I had that <code>entry.size -= pruned;</code> <em>inside</em> the <code>for rtype in rtypes</code>, which means that if a domain had multiple records <em>of different types</em> expire at the same time, the size would be wrong.</p>
<h3 id="putting-things-in">Putting things in</h3>
<p>Unfortunately, this is the most complex part. Adding a new entry to our cache involves a lot of work to maintain those invariants, especially if we also want to handle duplicate entries.</p>
<p>So before getting to the code, let’s think about what the behaviour should be.</p>
<ol type="1">
<li>If the domain name isn’t in the cache at all, we need to:
<ul>
<li>Insert a <code>CachedDomainRecords</code> containing <em>just</em> our new record.</li>
<li>Add the domain to the <code>access_priority</code> queue.</li>
<li>Add the domain to the <code>expiry_priority</code> queue.</li>
</ul></li>
<li>If the domain name is in the cache but it has no records of this type, we need to:
<ul>
<li>Add the record to the existing domain.</li>
<li>Update the domain’s <code>size</code> and <code>last_read</code>.</li>
<li>Update the <code>access_priority</code> queue.</li>
<li>Update the domain’s <code>next_expiry</code> and the <code>expiry_priority</code> queue, if this new record expires sooner than the current soonest.</li>
</ul></li>
<li>If the domain name is in the cache and it does have records of this type, we need to:
<ul>
<li>Check if there is a duplicate record, and if so:
<ul>
<li>Delete it.</li>
<li>Decrement the domain’s <code>size</code> and the <code>current_size</code>.</li>
<li>Update the domain’s <code>next_expiry</code> and the <code>expiry_priority</code> queue if the duplicate would have been the soonest record to expire.</li>
</ul></li>
<li>Then, the same as in case (2).</li>
</ul></li>
</ol>
<p>Additionally, in all cases, we need to increment the <code>current_size</code>.</p>
<p>Got all that? Here’s the code:</p>
<pre class="rust"><code>    /// Insert an entry into the cache.
    pub fn insert(&amp;mut self, record: &amp;ResourceRecord) {
        let now = Instant::now();
        let rtype = record.rtype.rtype();
        let expiry = Instant::now() + record.ttl;
        let tuple = (record.rtype.clone(), record.rclass, expiry);
        if let Some(entry) = self.entries.get_mut(&amp;record.name) {
            if let Some(tuples) = entry.records.get_mut(&amp;rtype) {
                let mut duplicate_expires_at = None;
                for i in 0..tuples.len() {
                    let t = &amp;tuples[i];
                    if t.0 == tuple.0 &amp;&amp; t.1 == tuple.1 {
                        duplicate_expires_at = Some(t.2);
                        tuples.swap_remove(i);
                        break;
                    }
                }

                tuples.push(tuple);

                if let Some(dup_expiry) = duplicate_expires_at {
                    entry.size -= 1;
                    self.current_size -= 1;

                    if dup_expiry == entry.next_expiry {
                        let mut new_next_expiry = expiry;
                        for (_, _, e) in tuples {
                            if *e &lt; new_next_expiry {
                                new_next_expiry = *e;
                            }
                        }
                        entry.next_expiry = new_next_expiry;
                        self.expiry_priority
                            .change_priority(&amp;record.name, Reverse(entry.next_expiry));
                    }
                }
            } else {
                entry.records.insert(rtype, vec![tuple]);
            }
            entry.last_read = now;
            entry.size += 1;
            self.access_priority
                .change_priority(&amp;record.name, Reverse(entry.last_read));
            if expiry &lt; entry.next_expiry {
                entry.next_expiry = expiry;
                self.expiry_priority
                    .change_priority(&amp;record.name, Reverse(entry.next_expiry));
            }
        } else {
            let mut records = HashMap::new();
            records.insert(rtype, vec![tuple]);
            let entry = CachedDomainRecords {
                last_read: now,
                next_expiry: expiry,
                size: 1,
                records,
            };
            self.access_priority
                .push(record.name.clone(), Reverse(entry.last_read));
            self.expiry_priority
                .push(record.name.clone(), Reverse(entry.next_expiry));
            self.entries.insert(record.name.clone(), entry);
        }

        self.current_size += 1;
    }</code></pre>
<p>I didn’t write this all in one go and get it right the first time. I first implemented this <em>without</em> the duplicate handling then, when it was working, I made it prevent duplicate records.</p>
<p>If you allow duplicates, the <code>if let Some(tuples)</code> block becomes much simpler:</p>
<pre class="rust"><code>if let Some(tuples) = entry.records.get_mut(&amp;rtype) {
    tuples.push(tuple);
} else {
    entry.records.insert(rtype, vec![tuple]);
}</code></pre>
<p>We’ve made it—the end of the operations!</p>
<h3 id="testing">Testing</h3>
<p>This code is pretty involved, and I’ve already said that I made at least one mistake when first writing it. So how do I know it’s correct?</p>
<p>Tests.</p>
<p>Tests, tests, tests.</p>
<p>I’m not going to go into the actual test code (<a href="https://github.com/barrucadu/resolved/blob/master/src/resolver/cache.rs">see the source if you want that</a>), but I will outline the cases.</p>
<p>The most important thing is to have a good way to generate inputs: you want distinct domains, overlapping domains, distinct types, overlapping types, overlapping but unequal records… the whole shebang. I’m generating random records, rather than trying to enumerate all the useful cases. I’m a big fan of random inputs in testing in general.</p>
<p>Some say “oh, but if my test is randomised it’ll be flaky: it might pass some times and fail other times!” In which case… good? If your test fails, you’ve found a bug: fix it!</p>
<p>Anyway, here are my test cases:</p>
<ul>
<li>Insert a record and then check I can retrieve it:
<ul>
<li>With <code>QueryType::Record(_)</code> and <code>QueryClass::Record(_)</code></li>
<li>With <code>QueryType::Wildcard</code> and <code>QueryClass::Record(_)</code></li>
<li>With <code>QueryType::Record(_)</code> and <code>QueryClass::Wildcard</code></li>
<li>With <code>QueryType::Wildcard</code> and <code>QueryClass::Wildcard</code></li>
</ul></li>
<li>Insert the same record twice and check the <code>current_size</code> only goes up by 1, and that the invariants hold.</li>
<li>Insert 100 random records and check that the invariants hold.</li>
<li>Insert 100 random records, check that they can all be retrieved, and that the invariants hold.</li>
<li>Insert 100 random records into a cache with a <code>desired_size</code> of 25, call <code>prune</code>, and check that 25 records remain and that the invariants hold.</li>
<li>Insert 100 random records, 49 of which have a TTL of 0, call <code>remove_expired</code>, and check that 51 remain and that the invariants hold.</li>
<li>Insert 100 random records into a cache with a <code>desired_size</code> of 99, 49 of which have a TTL of 0, call <code>prune</code>, and check that 51 remain and that the invariants hold.</li>
</ul>
<p>In most of those tests I check that the data structure invariants hold, there I:</p>
<ul>
<li>Check that the <code>current_size</code> is equal to the total number of records.</li>
<li>Check that the <code>entries</code> and the <code>access_priority</code> are the same size.</li>
<li>Check that the <code>entries</code> and the <code>expiry_priority</code> are the same size.</li>
<li>Check the <code>next_expiry</code> for each domain is equal to the minimum of its records’ expiry times.</li>
<li>Build a new <code>access_priority</code> from the domains and check it’s the same as the stored one.</li>
<li>Build a new <code>expiry_priority</code> from the domains and check it’s the same as the stored one.</li>
</ul>
<p>I feel pretty confident that my tests cover a variety of different cases and sequences of operations, and that I would have found any significant bugs. There could always be subtle bugs lurking, but that’s true of all code.</p>
<h3 id="periodic-pruning">Periodic pruning</h3>
<p>I’ve opted to prune the cache in two places.</p>
<p>Firstly, in my actual code, this cache is inside an <code>Arc&lt;Mutex&lt;_&gt;&gt;</code>, so it can be shared across threads. There’s not much point in having an unshared cache, after all. Anyway, this wrapper has some helper methods to get and insert entries, and the get helper calls <code>remove_expired</code> if it fetches any expired records:</p>
<pre class="rust"><code>impl SharedCache {
    pub fn get(
        &amp;self,
        name: &amp;DomainName,
        qtype: &amp;QueryType,
        qclass: &amp;QueryClass,
    ) -&gt; Vec&lt;ResourceRecord&gt; {
        let mut rrs = self.get_without_checking_expiration(name, qtype, qclass);
        let len = rrs.len();
        rrs.retain(|rr| rr.ttl &gt; Duration::ZERO);
        if rrs.len() != len {
            self.remove_expired();
        }
        rrs
    }

    // ... more omitted
}</code></pre>
<p>Secondly, I spawn a <a href="https://tokio.rs/">tokio</a> task to periodically remove expired entries, and then do additional pruning if need be:</p>
<pre class="rust"><code>async fn prune_cache_task(cache: SharedCache) {
    loop {
        sleep(Duration::from_secs(60 * 5)).await;

        let expired = cache.remove_expired();
        let pruned = cache.prune();

        println!(
            &quot;[CACHE] expired {:?} and pruned {:?} entries&quot;,
            expired, pruned
        );
    }
}</code></pre>
<p>It was very satisfying when I added this and first saw that <code>[CACHE]</code> output with non-zero expired and pruned records.</p>
<h2 id="what-next">What Next?</h2>
<p>This cache works, and it works well. I get nice and fast responses from my DNS server for queries which are wholly or partially cached, and <a href="https://github.com/barrucadu/resolved/blob/master/benches/cache.rs">the benchmarks I’ve written</a> look promising:</p>
<pre><code>insert/unique/1         time:   [1.0965 us 1.1001 us 1.1044 us]
                        thrpt:  [905.51 Kelem/s 909.00 Kelem/s 912.01 Kelem/s]
insert/unique/100       time:   [115.72 us 115.96 us 116.24 us]
                        thrpt:  [860.27 Kelem/s 862.39 Kelem/s 864.15 Kelem/s]
insert/unique/1000      time:   [1.1769 ms 1.1787 ms 1.1807 ms]
                        thrpt:  [846.96 Kelem/s 848.36 Kelem/s 849.67 Kelem/s]

insert/duplicate/1      time:   [1.1927 us 1.1964 us 1.2003 us]
                        thrpt:  [833.13 Kelem/s 835.86 Kelem/s 838.44 Kelem/s]
insert/duplicate/100    time:   [56.880 us 57.047 us 57.221 us]
                        thrpt:  [1.7476 Melem/s 1.7529 Melem/s 1.7581 Melem/s]
insert/duplicate/1000   time:   [541.33 us 542.10 us 542.93 us]
                        thrpt:  [1.8419 Melem/s 1.8447 Melem/s 1.8473 Melem/s]

get_without_checking_expiration/hit/1
                        time:   [1.4057 us 1.4249 us 1.4425 us]
                        thrpt:  [693.22 Kelem/s 701.81 Kelem/s 711.40 Kelem/s]
get_without_checking_expiration/hit/100
                        time:   [84.651 us 84.999 us 85.322 us]
                        thrpt:  [1.1720 Melem/s 1.1765 Melem/s 1.1813 Melem/s]
get_without_checking_expiration/hit/1000
                        time:   [991.64 us 997.89 us 1.0030 ms]
                        thrpt:  [996.98 Kelem/s 1.0021 Melem/s 1.0084 Melem/s]

get_without_checking_expiration/miss/1
                        time:   [948.17 ns 961.92 ns 974.39 ns]
                        thrpt:  [1.0263 Melem/s 1.0396 Melem/s 1.0547 Melem/s]
get_without_checking_expiration/miss/100
                        time:   [45.399 us 46.116 us 46.671 us]
                        thrpt:  [2.1426 Melem/s 2.1684 Melem/s 2.2027 Melem/s]
get_without_checking_expiration/miss/1000
                        time:   [570.42 us 577.92 us 583.75 us]
                        thrpt:  [1.7131 Melem/s 1.7303 Melem/s 1.7531 Melem/s]

remove_expired/1        time:   [1.2796 us 1.2983 us 1.3151 us]
                        thrpt:  [760.38 Kelem/s 770.26 Kelem/s 781.52 Kelem/s]
remove_expired/100      time:   [55.622 us 56.761 us 57.895 us]
                        thrpt:  [1.7273 Melem/s 1.7618 Melem/s 1.7978 Melem/s]
remove_expired/1000     time:   [786.47 us 794.30 us 800.89 us]
                        thrpt:  [1.2486 Melem/s 1.2590 Melem/s 1.2715 Melem/s]

prune/1                 time:   [1.3455 us 1.3539 us 1.3617 us]
                        thrpt:  [734.36 Kelem/s 738.63 Kelem/s 743.24 Kelem/s]
prune/100               time:   [41.584 us 41.676 us 41.774 us]
                        thrpt:  [2.3938 Melem/s 2.3995 Melem/s 2.4048 Melem/s]
prune/1000              time:   [613.73 us 617.63 us 620.87 us]
                        thrpt:  [1.6106 Melem/s 1.6191 Melem/s 1.6294 Melem/s]</code></pre>
<p>But could it be better?</p>
<p>The only optimisation that really comes to mind is using a trie instead of the <code>HashMap</code> for domains. Another possibility is turning it into a more generic size-bounded-LRU-cache-with-expiration data structure with type parameters, and so making the DNS usage just a specialisation of that; perhaps genericising the code would make it easier to see improvements.</p>
<p>But nothing <em>needs</em> to be done, it works pretty well as it is. When I start using my DNS server for my LAN, and it starts to get much more traffic than my test instance, I’m sure performance problems will start to crop up, but hopefully they won’t be with this cache.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>Not just “perhaps”: this is more-or-less copied straight from my original code.<a href="dns-cache.html#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Least Recently Used<a href="dns-cache.html#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn3"><p>From the <a href="https://crates.io/crates/priority_queue">priority-queue</a> crate. I started out trying to build something on top of <a href="https://doc.rust-lang.org/stable/std/collections/struct.BinaryHeap.html"><code>std::collections::BinaryHeap</code></a> directly, but didn’t get very far.<a href="dns-cache.html#fnref3" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Continuous Integration and Continuous Deployment</title>
    <link href="https://memo.barrucadu.co.uk/ci-cd.html" />
    <id>https://memo.barrucadu.co.uk/ci-cd.html</id>
    <published>2021-03-20T00:00:00Z</published>
    <updated>2021-04-17T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>Once upon a time I used a self-hosted instance of <a href="https://www.jenkins.io/">Jenkins</a> and the free-for-open-source <a href="https://travis-ci.org/">Travis CI</a> for continuous integration (CI) and continuous deployment (CD). It <em>worked</em>, but had some undesirable traits:</p>
<ul>
<li>There wasn’t any rhyme or reason over what ended up where.</li>
<li>Travis often took a long time to run jobs.</li>
<li>Jenkins was almost all hand-configured, with little config in version control.</li>
</ul>
<p>I’m a big fan of configuration-as-code, and when I was exposed to <a href="https://concourse-ci.org/">Concourse CI</a> at work, which does <em>everything</em> through configuration files and environment variables, I decided to replace my Jenkins set-up and migrate some of my Travis projects as a learning experience.</p>
<p>Eventually I ended up with Concourse doing continuous deployment, and Travis solely for continuous integration. This worked well, until the future of the free-for-open-source Travis became uncertain, and I decided to move away.</p>
<p>As luck would have it, we were discussing using <a href="https://github.com/features/actions">GitHub Actions</a> for CI at work at the time. I decided to switch to Actions as another learning experience.</p>
<p>Now I have GitHub Actions for CI on pull requests (PRs), and Concourse for CD of master branches. It works pretty well.</p>
<p>This memo talks through my practices, using <a href="https://github.com/barrucadu/memo.barrucadu.co.uk">this blog</a> and <a href="https://github.com/barrucadu/dejafu">dejafu</a> as running examples. I’ll also cover how I run Concourse on <a href="https://nixos.org/">NixOS</a>, other related tools I use, and what my plans for future work are.</p>
<h2 id="github-actions">GitHub Actions</h2>
<figure>
<img src="ci-cd/github-actions.png" alt="GitHub Actions checks running on a PR." />
<figcaption aria-hidden="true">GitHub Actions checks running on a PR.</figcaption>
</figure>
<p><a href="https://github.com/features/actions">GitHub Actions</a> is GitHub’s hosted CI/CD tool. It’s got good support for both official and community-maintained Actions (which are <a href="https://docs.github.com/en/actions/creating-actions">Docker images conforming to a simple specification</a>), is as well-integrated into the rest of GitHub as you’d expect, and has a config file syntax not entirely unlike Travis.</p>
<p>Currently I’m inconsistent across my repos whether I require Actions to pass before a commit can make it into master. I tend to have that for my Haskell packages, because master gets deployed to <a href="http://hackage.haskell.org/">Hackage</a>, but allow pushing straight to master for other things.</p>
<h3 id="example-memo.barrucadu.co.uk">Example: memo.barrucadu.co.uk</h3>
<p><a href="https://github.com/barrucadu/memo.barrucadu.co.uk/blob/master/.github/workflows/ci.yaml">See the configuration file</a>.</p>
<p>This is fairly typical of my Python projects: I have two <code>jobs</code>, which show up as two separate checks with their own logs in a PR, one to check for linting errors and one to check that the dependencies all install.</p>
<p>I’ve found that <a href="https://pypi.org/project/pip/">pip</a> doesn’t have the most robust dependency solver, and can sometimes get confused and install mutually incompatible versions of packages. So for any PR which upgrades the dependencies, I like to ensure that the freeze file has a consistent set of versions.</p>
<p>If I wrote tests they would solve this problem too. But I don’t.</p>
<h3 id="example-dejafu">Example: dejafu</h3>
<p><a href="https://github.com/barrucadu/dejafu/blob/master/.github/workflows/ci.yaml">See the configuration file</a>.</p>
<p>This is rather more complicated. I want to build the code and run the tests against all the supported versions of <a href="https://www.haskell.org/ghc/">GHC</a>, but for linting and <a href="https://hackage.haskell.org/package/doctest">doctests</a> I just want to use the latest version. And I want the linting, doctests, and each of the main tests to run as separate jobs. This makes them run in parallel, and means that a failure in one doesn’t prevent the rest from running.</p>
<p>Like Travis, GitHub Actions supports <a href="https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix">matrix builds</a>. The <code>strategy</code> part of the configuration means “run this job with each of these options; and don’t kill the rest if one fails”:</p>
<pre class="yaml"><code>strategy:
  fail-fast: false
  matrix:
    resolver:
      - lts-9.0 # ghc-8.0
      - lts-10.0 # ghc-8.2
      - lts-12.0 # ghc-8.4
      - lts-13.3 # ghc-8.6
      - lts-15.0 # ghc-8.8
      - lts-17.0 # ghc-8.10</code></pre>
<p>Another nice feature of GitHub Actions is that the documentation is well-written and easy to follow. Just about every option has a short example.</p>
<h2 id="concourse-ci">Concourse CI</h2>
<figure>
<img src="ci-cd/concourse-ci.png" alt="Visualisation of the dejafu Concourse pipeline." />
<figcaption aria-hidden="true">Visualisation of the dejafu Concourse pipeline.</figcaption>
</figure>
<p><a href="https://concourse-ci.org/">Concourse CI</a> is an opinionated “continuous thing-doer”. Everything is containerised and pure. No state is shared between jobs without you explicitly managing it, in the form of a “resource” (like a git remote, or an S3 bucket).</p>
<p>This was a big change when I came from Jenkins, which is just about as impure as you can get, but I’ve become a big fan of it. It makes jobs (potentially) reproducible, as they only depend on their inputs and on the pipeline configuration. You can have nondeterminism in your configuration, but you can’t get into trouble because of a previous build leaving things in a weird state.</p>
<p>I currently have 16 Concourse pipelines deploying a variety of things:</p>
<ul>
<li>My Haskell packages (by uploading a package to Hackage)</li>
<li>My <a href="https://github.com/barrucadu/bookdb">bookdb</a> and <a href="https://github.com/barrucadu/bookmarks">bookmarks</a> (by uploading a Docker image to my registry, and SSHing into a server to restart a systemd unit)</li>
<li>A bunch of static websites</li>
<li>My <a href="https://github.com/barrucadu/ops">AWS and DNS configuration</a> (these jobs automatically <em>plan</em>, but don’t <em>apply</em> until I click a button)</li>
</ul>
<h3 id="example-memo.barrucadu.co.uk-1">Example: memo.barrucadu.co.uk</h3>
<p><a href="https://github.com/barrucadu/memo.barrucadu.co.uk/blob/master/concourse/pipeline.yml">See the configuration file</a>.</p>
<p>This is another fairly typical pipeline, all of my static websites look largely like this. The one unusual feature is that it builds a Docker image: I need a few dependencies to deploy this site, like <a href="https://pandoc.org/">pandoc</a>, so rather than install them on every deploy I build an image.</p>
<p>The deploy uses a custom <a href="https://github.com/barrucadu/ops#rsync-resource"><code>rsync-resource</code></a> that I took from somewhere and slightly tweaked. It also uses <code>((secrets))</code> in a few places.</p>
<p>The configuration is rather more verbose than GitHub Actions. It is doing more, but it also requires more to be spelled out. This can make large pipelines a bit difficult to read.</p>
<h3 id="example-dejafu-1">Example: dejafu</h3>
<p><a href="https://github.com/barrucadu/dejafu/blob/master/concourse/pipeline.yml">See the configuration file</a>.</p>
<p>This is significantly more complicated. dejafu is a monorepo containing four Haskell packages and one set of tests, so this pipeline has jobs for testing &amp; releasing each of those packages, as well as a job to run a nightly build when <a href="https://www.stackage.org/">Stackage</a> updates.</p>
<p>I use YAML anchors to reduce the repetition, which helps a bit, but it’s still a pretty long file.</p>
<p>This pipeline shows off Concourse’s task dependencies. All builds are triggered by a “resource” changing, but a job can specify that it should <em>only</em> be called for resources which passed a previous job.</p>
<p>For example, the <code>release-concurrency</code> job will be triggered by changes to the <code>concurrency-cabal-git</code> resource, but only after they pass the <code>test-concurrency</code> job:</p>
<pre class="yaml"><code>- name: test-concurrency
  plan:
    - get: concurrency-cabal-git
      trigger: true
    - task: build-and-test
      input_mapping:
        source-git: concurrency-cabal-git
      config:
        &lt;&lt;: *task-build-and-test

- name: release-concurrency
  plan:
    - get: concurrency-cabal-git
      trigger: true
      passed:
        - test-concurrency
    - task: prerelease-check
      params:
        PACKAGE: concurrency
      input_mapping:
        source-git: concurrency-cabal-git
      config:
        &lt;&lt;: *task-prerelease-check
    - task: release
      params:
        PACKAGE: concurrency
      input_mapping:
        source-git: concurrency-cabal-git
      config:
        &lt;&lt;: *task-release</code></pre>
<p>These dependencies are what make up the visualisation in the screenshot above.</p>
<h2 id="other-tools-dependabot">Other tools: Dependabot</h2>
<figure>
<img src="ci-cd/dependabot.png" alt="A PR opened by Dependabot." />
<figcaption aria-hidden="true">A PR opened by Dependabot.</figcaption>
</figure>
<p><a href="https://dependabot.com/">Dependabot</a> is a handy little tool for automatically checking if you have any outdated dependencies, <a href="https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem">for a variety of ecosystems</a>, and opening a PR to update them. It’s another tool we use at work (spotting a pattern?), but I didn’t pick this up to learn anything: it’s so simple there’s nothing really <em>to</em> learn, and its utility far outweighs the small configuration file you might want to write.</p>
<h3 id="example-memo.barrucadu.co.uk-2">Example: memo.barrucadu.co.uk</h3>
<p><a href="https://github.com/barrucadu/memo.barrucadu.co.uk/blob/master/.github/dependabot.yml">See the configuration file</a>.</p>
<p>This is one of my more complex Dependabot config files, which should hopefully convince you of how straightforward it is. It specifies I want PRs to update any official or community Actions, Dockerfile base images, or pip dependencies, that I’m using. And I want it to check daily (at 5AM UTC by default).</p>
<p>That’s it!</p>
<h3 id="example-dejafu-2">Example: dejafu</h3>
<p><a href="https://github.com/barrucadu/dejafu/blob/master/.github/dependabot.yml">See the configuration file</a>.</p>
<p>Unlike the other cases, this time dejafu has a simpler configuration than the blog. Dependabot doesn’t support Haskell, so all it’s doing is ensuring any Actions I’m using are kept up to date.</p>
<p>Since my Haskell packages are on Stackage, the Stackage maintainers let me know if I need to update a dependency.</p>
<h2 id="secrets-management">Secrets Management</h2>
<p>I don’t make a practice of needing secrets to build or run code in my public repos, so I don’t need to give GitHub Actions any secrets. <a href="https://docs.github.com/en/actions/reference/encrypted-secrets">It’s supported though</a>, you can have both organisation-level and repository-level secrets.</p>
<p>My Concourse pipelines, however, <em>do</em> regularly need secrets. The password for my private Docker registry; the password to upload Haskell packages to Hackage; the SSH key to deploy this blog; and more!</p>
<p>Concourse has support for a few secret stores. I’m using <a href="https://concourse-ci.org/aws-ssm-credential-manager.html">the AWS SSM integration</a>, mostly because it’s incredibly cheap, and means I don’t have to host and secure anything myself. It works well, I just need to set some environment variables giving Concourse an AWS access key hooked up to <a href="https://github.com/barrucadu/ops/blob/master/aws/concourse.tf">an IP-restricted policy granting SSM and KMS permissions</a>. Almost no effort at all to set up if you already have an AWS account.</p>
<h2 id="running-concourse-ci-on-nixos">Running Concourse CI on NixOS</h2>
<p><a href="https://nixos.org/">NixOS</a> is my Linux distribution of choice and, while it has packages for many things, it does not have one for Concourse. However, there is <a href="https://hub.docker.com/r/concourse/concourse">an official docker image for Concourse</a>.</p>
<p>I’ve got a systemd unit running Concourse in docker-compose:</p>
<pre class="nix"><code>systemd.services.concourse =
  let
    yaml = import ./concourse.docker-compose.nix {
      httpPort = concourseHttpPort;
      githubClientId     = fileContents /etc/nixos/secrets/concourse-clientid.txt;
      githubClientSecret = fileContents /etc/nixos/secrets/concourse-clientsecret.txt;
      enableSSM = true;
      ssmAccessKey = fileContents /etc/nixos/secrets/concourse-ssm-access-key.txt;
      ssmSecretKey = fileContents /etc/nixos/secrets/concourse-ssm-secret-key.txt;
    };
    dockerComposeFile = pkgs.writeText &quot;docker-compose.yml&quot; yaml;
  in
    {
    enable = true;
    wantedBy = [ &quot;multi-user.target&quot; ];
    requires = [ &quot;docker.service&quot; ];
    environment = { COMPOSE_PROJECT_NAME = &quot;concourse&quot;; };
    serviceConfig = {
      ExecStart = &quot;${pkgs.docker_compose}/bin/docker-compose -f &#39;${dockerComposeFile}&#39; up&quot;;
      ExecStop  = &quot;${pkgs.docker_compose}/bin/docker-compose -f &#39;${dockerComposeFile}&#39; stop&quot;;
      Restart   = &quot;always&quot;;
    };
  };</code></pre>
<p>Where the <a href="https://github.com/barrucadu/nixfiles/blob/master/services/docker-compose-files/concourse.docker-compose.nix">concourse.docker-compose.nix</a> file is just some templated YAML. I’ve heard that you shouldn’t use systemd units to run Docker containers, for some reason, but it works and I run a few different services on a bunch of servers like this. Running Concourse in Docker also makes it easy to upgrade to a newer version, without needing to wait for an official package to be updated.</p>
<h2 id="future-work">Future Work</h2>
<p>I’m pretty happy with how things are working right now. Until recently I didn’t have Concourse secrets set up, and I was handling secrets by doing variable interpolation in my pipeline deployment script, and also I’d written everything in <a href="https://jsonnet.org/">jsonnet</a> for some reason. Setting up secrets, just using YAML, and removing the deployment script simplified things a lot.</p>
<p>I see GitHub advertising <a href="https://github.blog/2020-09-30-code-scanning-is-now-available/">code scanning</a> to me in all of my repositories, so maybe I’ll look into that next. I’m a big fan of static analysis, so having something which automatically scans my code for issues is very attractive.</p>
<p>The main thing I don’t have continuous deployment for is <a href="https://github.com/barrucadu/nixfiles">my NixOS configuration</a>. I SSH into servers, run <code>git pull &amp;&amp; sudo nixos-rebuild switch</code> like some sort of <em>caveman!</em> But automatically deploying that makes me a bit nervous, what if it goes wrong? Still, I switched to automatic updates recently, and nothing has broken yet, so maybe automatic configuration deployments are fine too.</p>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>At home for one year</title>
    <link href="https://memo.barrucadu.co.uk/at-home-for-one-year.html" />
    <id>https://memo.barrucadu.co.uk/at-home-for-one-year.html</id>
    <published>2021-03-19T00:00:00Z</published>
    <updated>2021-03-19T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p><a href="https://weeknotes.barrucadu.co.uk/notes/079.html">The 19th of March, 2020 was the last time I visited the office</a>, and there were only a couple of other people in.</p>
<p>Lockdowns have come and gone, restrictions have changed frequently and unexpectedly, and so I’ve lived the last 12 months as a hermit. Since that final day in the office, one year ago today, I’ve only left my flat once or twice a week, and that only to go shopping.</p>
<p>There is a vaccine now but, <a href="https://www.bbc.co.uk/news/health-55045639">judging from the timeline</a>, I’ll still be at home for a few more months.</p>
<h2 id="the-good-and-the-bad">The Good and the Bad</h2>
<p>It feels a bit selfish to type this, but frankly I’ve been having a great time:</p>
<ul>
<li><p><strong>My sleep has improved.</strong> The lack of commute means I get an extra hour or so to lie in bed.</p></li>
<li><p><strong>I’ve saved money.</strong> Partly due to the lack of commute, but also due to not going out to buy lunch. Even one or two lunches a week add up.</p></li>
<li><p><strong>I’ve been reading more.</strong> I now have more energy in the evenings after the work day ends, so I’ve got back into the habit of reading before bed. And over 2020 I read 99 books.</p></li>
<li><p><strong>I can cook whenever I want.</strong> I used to get hungry in the afternoons almost every day. One day a thought hit me: if I’m at home all the time now, I can cook a proper meal for lunch! And so I switched to having my main meal of the day for lunch, and a smaller meal in the evening.</p></li>
<li><p><strong>I’ve started a second RPG group.</strong> I did get a bit bored after a couple of months, and so I reached out to some online friends to see if anyone wanted to play games. I’ve now got a group which has been going strong since May, and I’ve deepened those friendships.</p></li>
<li><p><strong>I’m not in an open office any more.</strong> I don’t like open office layouts. I always feel like someone is peering over my shoulder and watching my screen. It’s not an issue with just my current job, it’s been an issue everywhere. At home, I <em>know</em> there is nobody watching me, and I feel much more relaxed, even when I’m not slacking off.</p></li>
</ul>
<p>Of course, there have been a handful of downsides too:</p>
<ul>
<li><p><strong>I’ve not seen any friends.</strong> I’ve got a small group of friends who meet up a couple of times a year, and we’ve missed a few of those meetings. We’ve made do with Zoom calls, but it’s not the same.</p></li>
<li><p><strong>I’ve not seen any family.</strong> I normally only visit home at Christmas, and Christmas got cancelled.</p></li>
<li><p><strong><a href="https://weeknotes.barrucadu.co.uk/notes/107.html">I came down with shingles</a>.</strong> Not very fun, possibly caused by stress. I’ve got a few small scars on my forehead which, now that it’s been nearly 6 months, will likely not heal. However, other than that one week of illness, my health has been great.</p></li>
</ul>
<p>But the upsides definitely outweigh these. I was already only physically meeting friends and family three or four times a year, so missing one year isn’t a huge change. It’s not like I’ve gone from hanging out with people at the pub every week to never seeing anyone.</p>
<h2 id="and-the-strange">…and the Strange</h2>
<p>The weirdest part of the past year, <em>by far</em>, has been the discovery that a significant number of people just cannot cope with being alone, and break down after spending even a fortnight by themselves.</p>
<p>I was regularly spending weeks by myself even before covid!</p>
<p>It makes some sense though. I fill my time with reading, programming, playing RPGs, and socialising with online friends. Most people don’t do any of those to any significant degree (or at all). If everything you do for fun requires the physical presence of other people, the past year will have been tough.</p>
<p>I also have appropriate desk space, and don’t have noisy children or housemates. Being a loner with a nice flat during lockdown is life in easy mode.</p>
<p>I’m sure I will have to return to the office at some point, but I’ll fully enjoy being at home until then.</p>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Quick Code Improvements</title>
    <link href="https://memo.barrucadu.co.uk/quick-code-improvements.html" />
    <id>https://memo.barrucadu.co.uk/quick-code-improvements.html</id>
    <published>2021-03-05T00:00:00Z</published>
    <updated>2021-03-05T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>Here are 21 small improvements you can make to your code or the tooling around it, taken from the <a href="https://www.codequalitychallenge.com/">Code Quality Challenge</a> in February 2021. If you find yourself with 20 minutes spare, pick one and see how far you can get.</p>
<ol type="1">
<li><p><strong>Improve your README</strong></p>
<p>For example, document the philosophy behind your project and how it fits into the larger ecosystem; give a comparison to similar projects; give usage examples; explain how it’s developed and tested; how it’s deployed (if it’s a program); and your approach to outside contributions.</p></li>
<li><p><strong>Nuke TODO comments</strong></p>
<p>Grep for <code>TODO</code> and: if out of date, delete; if still relevant, fix or turn into an issue; and if you’re unsure, find someone who <em>is</em> sure.</p></li>
<li><p><strong>Get rid of a warning</strong></p>
<p>Whether it’s in the code proper or just in the tests, fix at least one.</p></li>
<li><p><strong>Delete some unused code</strong></p>
<p>Tools like <a href="https://unused.codes/">unused</a> or test coverage metrics can help you track down dead code.</p></li>
<li><p><strong>Trim your (git) branches</strong></p>
<p>Run <code>git remote prune origin</code> to delete any tracking branches which have since been merged or deleted. If you have any old branches of your own, get rid of them with <code>git push origin  --delete &lt;branch&gt;</code>.</p></li>
<li><p><strong>Extract a compound conditional</strong></p>
<p>Look for complex conditionals of multiple terms and see if they can be extracted into a function or a variable whose name clearly expresses what is being checked.</p></li>
<li><p><strong>Slim down an overgrown class</strong></p>
<p>Look at your largest classes (or modules if you’re using a class-less language) and see if there are any bits of code which can be refactored. Extract a new class (or module), delete a stray comment, improve a name, tighten the visibility of a method (or function, type, etc), split apart a long method, and so on.</p></li>
<li><p><strong>Help new starters get up to speed</strong></p>
<p>The actual challenge was to <strong>create a setup script</strong>, but you might have a different approach to solving this problem. So create a setup script, or a Dockerfile, add instructions to your README, or however you do it.</p></li>
<li><p><strong>Run your tests with no network connection</strong></p>
<p>Tests which rely on an external service are slow and brittle, so try to get your tests passing without any such dependencies.</p></li>
<li><p><strong>Investigate your slowest tests</strong></p>
<p>Find your 10 slowest tests or so and have a look through them. Are any duplicates? Can any be replaced by a faster variant? Are they actually useful?</p></li>
<li><p><strong>Improve one name</strong></p>
<p>Find one poorly-named thing and make it better. Any thing.</p></li>
<li><p><strong>Audit your dependencies</strong></p>
<p>Are they still needed? Is everything up to date? Can a runtime dependency be turned into a build or test dependency?</p></li>
<li><p><strong>Audit your PRs and issues</strong></p>
<p>Have any been hanging around for a while? If so, are they still relevant? If you’re not sure, ask the reporter if they can confirm, and close the issue if they don’t get back to you in a week or so.</p></li>
<li><p><strong>Investigate long parameter lists</strong></p>
<p>Long parameter lists, particularly if they occur in multiple methods (or functions), might indicate that there’s a useful type you’re missing, or that some of the parameters should be instance data. Some parameters, like booleans, may indicate that you’ve got one method doing the work of several, and it should be split up.</p></li>
<li><p><strong>Automate something repetitive</strong></p>
<p>Find something you do repeatedly and automate it. For example, write shell aliases for some commands you run a lot.</p></li>
<li><p><strong>Audit your database schema</strong></p>
<p>You might look for inconsistent column names, missing indices, or missing null or foreign key constraints.</p></li>
<li><p><strong>RTFM</strong></p>
<p>Look at the docs for something you use a lot—whether that’s a development tool (like your text editor), or a backing service (like a database), or a framework, or something else—and see if there’s anything which you can apply.</p></li>
<li><p><strong>Investigate high-churn files</strong></p>
<p>Files which change a lot can point to a good refactoring opportunity. With git you can see the number of commits each file has with:</p>
<pre class="bash"><code>git log --all -M -C --name-only --format=&#39;format:&#39;  \
    | sort \
    | grep -v &#39;^$&#39; \
    | uniq -c \
    | sort -n \
    | awk &#39;BEGIN {print &quot;count\tfile&quot;} {print $1 &quot;\t&quot; $2}&#39;</code></pre></li>
<li><p><strong>Create or update your snippets</strong></p>
<p>If your text editor has support for snippets, make sure you have some for any code patterns you type a lot.</p></li>
<li><p><strong>Begin plugging a knowledge gap</strong></p>
<p>There’s probably something you know you don’t know. Start doing something about it: spend 20 minutes researching it and start to chip away at your lack of knowledge.</p></li>
<li><p><strong>Extract a method</strong></p>
<p>Look at your larger methods (or functions), are there any groups of functionality which could be pulled out into smaller units of code with their own clear names?</p></li>
</ol>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Indoor Air Quality</title>
    <link href="https://memo.barrucadu.co.uk/indoor-air-quality.html" />
    <id>https://memo.barrucadu.co.uk/indoor-air-quality.html</id>
    <published>2021-02-06T00:00:00Z</published>
    <updated>2021-02-06T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>I strongly suspect my thermostat is lying to me.</p>
<p>Some days I will be shivering, and it says the temperature is 28C.</p>
<p>Some days I will be sweating, and it says the temperature is 18C.</p>
<p>It’s as if it’s measuring the temperature of somewhere else, but the thermostat is in my living room.</p>
<p>So to put the issue to rest, I wanted to get a smart ambient thermometer, to compare measurements. Ideally something with an API which I can use to get the data into <a href="https://prometheus.io/">Prometheus</a> and graph it.</p>
<figure>
<img src="indoor-air-quality/awair.jpg" alt="The Awair Element, on a bookcase." />
<figcaption aria-hidden="true">The Awair Element, on a bookcase.</figcaption>
</figure>
<p>This is the <a href="https://uk.getawair.com/">Awair Element</a>, an indoor air quality monitoring smart device, which measures a bunch of things—including temperature.<a href="indoor-air-quality.html#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a> I’ve got one in my living room, and I plan to get one for my bedroom.</p>
<p><a href="https://docs.developer.getawair.com/#local-api">It has an API</a>, so I can <a href="https://github.com/barrucadu/prometheus-awair-exporter/">scrape the data</a>:</p>
<pre class="bash"><code>$ curl http://10.0.20.117/air-data/latest | json_pp

{
   &quot;abs_humid&quot; : 9.06,
   &quot;co2&quot; : 799,
   &quot;co2_est&quot; : 693,
   &quot;dew_point&quot; : 10.05,
   &quot;humid&quot; : 49.91,
   &quot;pm10_est&quot; : 4,
   &quot;pm25&quot; : 3,
   &quot;score&quot; : 90,
   &quot;temp&quot; : 20.89,
   &quot;timestamp&quot; : &quot;2021-02-06T20:39:13.338Z&quot;,
   &quot;voc&quot; : 422,
   &quot;voc_baseline&quot; : 2562694386,
   &quot;voc_ethanol_raw&quot; : 38,
   &quot;voc_h2_raw&quot; : 27
}</code></pre>
<p>And stick it on a dashboard; here’s my Saturday night gaming session:</p>
<figure>
<img src="indoor-air-quality/grafana.png" alt="A dashboard showing various air quality metrics." />
<figcaption aria-hidden="true">A dashboard showing various air quality metrics.</figcaption>
</figure>
<p>It’s February now, so it’s cold. I had all the windows and my living room door shut from a little before 16:00. You can see the <abbr
title="Carbon Dioxide">CO2</abbr> and <abbr title="Volatile Organic
Compounds">VOC</abbr> levels creeping up.</p>
<p>We had a 15-minute break in the middle. I opened the windows and door. You can see the levels drop back down. And then creep up again after the break.</p>
<p>The percentage in the top-left is an overall score based on the other metrics. 80%+ means your air is great, I’ve been aiming to keep it above 90%. The thresholds on the other graphs are based on the thresholds the Awair Element uses: it goes from 1 to 5 dots, which I’ve condensed into three sets of regions (ideal, good, bad).</p>
<p>I find myself glancing at the device (and the dashboard) throughout the day, and opening a window if it looks like I could do with a bit more ventilation.</p>
<p>Even if it’s making the numbers up<a href="indoor-air-quality.html#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a> it’s making me get more fresh air, which can only be a good thing.</p>
<p>And yes, my thermostat <em>is</em> lying to me.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p><a href="https://www.youtube.com/watch?v=MRqh8oLY7Ik">Here’s a great video</a> on why you should care about the quality of your air.<a href="indoor-air-quality.html#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
<li id="fn2"><p>Though I hope it’s not, and the movements on the graphs do correlate with when I have windows open.<a href="indoor-air-quality.html#fnref2" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Benchmarking WSGI servers</title>
    <link href="https://memo.barrucadu.co.uk/benchmarking-wsgi-servers.html" />
    <id>https://memo.barrucadu.co.uk/benchmarking-wsgi-servers.html</id>
    <published>2020-12-23T00:00:00Z</published>
    <updated>2020-12-23T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>I’ve been using <a href="https://flask.palletsprojects.com/en/1.1.x/">flask</a>’s built in WSGI server for <a href="https://bookdb.barrucadu.co.uk/search">bookdb</a> and <a href="https://bookmarks.barrucadu.co.uk/search">bookmarks</a> for a while now. The very same built in server that it warns you to not use in production because it scales badly.</p>
<p>But how badly? Fortunately, the flask docs <a href="https://flask.palletsprojects.com/en/1.1.x/deploying/wsgi-standalone/">list some better servers</a>, so I decided to try out a few of them.</p>
<h2 id="testing-methodology">Testing methodology</h2>
<p>I decided to use <a href="https://github.com/JoeDog/siege">siege</a>, because it can take a list of URLs in a text file. I’ve got <a href="https://github.com/alphagov/govuk-load-testing">some prior experience of Gatling</a>, but didn’t feel like writing Scala.</p>
<p>I produced a list of 30 bookdb URLs:</p>
<ul>
<li>2 variations of the search page with no parameters (both HTML and JSON endpoints)</li>
<li>7 variations of the search page with parameters (all HTML)</li>
<li>1 book JSON endpoint</li>
<li>9 book cover images</li>
<li>9 book cover thumbnail images</li>
<li>2 static files (css and javascript)</li>
</ul>
<p>And then I ran siege for 10s with 2, 4, and 8 workers, against:</p>
<ul>
<li>The default Werkzeug WSGI server</li>
<li><a href="https://gunicorn.org/">Gunicorn</a>, with 4 processes</li>
<li><a href="https://uwsgi-docs.readthedocs.io/en/latest/">uWSGI</a>, with 4 processes</li>
<li><a href="http://www.gevent.org/">Gevent</a></li>
</ul>
<h2 id="results">Results</h2>
<figure>
<img src="benchmarking-wsgi-servers/transaction-rate.png" alt="Graph showing transaction rate for various servers and siege configurations." />
<figcaption aria-hidden="true">Graph showing transaction rate for various servers and siege configurations.</figcaption>
</figure>
<p>The results are in, the default Werkzeug server is bad at scaling! The number of transactions (completed requests) per second doesn’t really change, even when the number of siege workers (clients) goes up by a factor of 4. I suspect it’s processing requests synchronously in a single thread.</p>
<p>Every other server shows a good increase in throughput when the number of clients goes up. Though Gevent starts even slower than Werkzeug!</p>
<p>Gunicorn looks like a slight winner over uWSGI, so that’s the server I’ll be using going forwards.</p>
<h2 id="appendix-raw-data">Appendix: raw data</h2>
<h3 id="werkzeug">Werkzeug</h3>
<pre><code>+ siege -q -t 10S -c 2 -f urls.txt

{       &quot;transactions&quot;:                         2309,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                        10.00,
        &quot;data_transferred&quot;:                     8.21,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   230.90,
        &quot;throughput&quot;:                           0.82,
        &quot;concurrency&quot;:                          1.98,
        &quot;successful_transactions&quot;:              2309,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.61,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 4 -f urls.txt

{       &quot;transactions&quot;:                         2648,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.99,
        &quot;data_transferred&quot;:                     7.80,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   265.07,
        &quot;throughput&quot;:                           0.78,
        &quot;concurrency&quot;:                          3.96,
        &quot;successful_transactions&quot;:              2648,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.87,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 8 -f urls.txt

{       &quot;transactions&quot;:                         2503,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                    11.85,
        &quot;response_time&quot;:                        0.03,
        &quot;transaction_rate&quot;:                   250.80,
        &quot;throughput&quot;:                           1.19,
        &quot;concurrency&quot;:                          7.96,
        &quot;successful_transactions&quot;:              2503,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.89,
        &quot;shortest_transaction&quot;:                 0.01
}</code></pre>
<h3 id="gunicorn">Gunicorn</h3>
<pre><code>+ siege -q -t 10S -c 2 -f urls.txt

{       &quot;transactions&quot;:                         2833,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.11,
        &quot;data_transferred&quot;:                     9.21,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   310.98,
        &quot;throughput&quot;:                           1.01,
        &quot;concurrency&quot;:                          1.95,
        &quot;successful_transactions&quot;:              2833,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.62,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 4 -f urls.txt

{       &quot;transactions&quot;:                         4175,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                    16.54,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   418.34,
        &quot;throughput&quot;:                           1.66,
        &quot;concurrency&quot;:                          3.94,
        &quot;successful_transactions&quot;:              4175,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  1.24,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 8 -f urls.txt

{       &quot;transactions&quot;:                         5665,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                    18.92,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   567.64,
        &quot;throughput&quot;:                           1.90,
        &quot;concurrency&quot;:                          7.86,
        &quot;successful_transactions&quot;:              5665,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  1.54,
        &quot;shortest_transaction&quot;:                 0.00
}</code></pre>
<h3 id="uwsgi">uWSGI</h3>
<pre><code>+ siege -q -t 10S -c 2 -f urls.txt

{       &quot;transactions&quot;:                         2875,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.86,
        &quot;data_transferred&quot;:                     9.46,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   291.58,
        &quot;throughput&quot;:                           0.96,
        &quot;concurrency&quot;:                          1.97,
        &quot;successful_transactions&quot;:              2875,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.58,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 4 -f urls.txt

{       &quot;transactions&quot;:                         3983,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                    16.38,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   399.10,
        &quot;throughput&quot;:                           1.64,
        &quot;concurrency&quot;:                          3.94,
        &quot;successful_transactions&quot;:              3983,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  1.03,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 8 -f urls.txt

{       &quot;transactions&quot;:                         5394,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                    16.36,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   540.48,
        &quot;throughput&quot;:                           1.64,
        &quot;concurrency&quot;:                          7.91,
        &quot;successful_transactions&quot;:              5394,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  1.32,
        &quot;shortest_transaction&quot;:                 0.00
}</code></pre>
<h3 id="gevent">Gevent</h3>
<pre><code>+ siege -q -t 10S -c 2 -f urls.txt

{       &quot;transactions&quot;:                         2076,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.70,
        &quot;data_transferred&quot;:                     7.91,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   214.02,
        &quot;throughput&quot;:                           0.82,
        &quot;concurrency&quot;:                          1.97,
        &quot;successful_transactions&quot;:              2076,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.65,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 4 -f urls.txt

{       &quot;transactions&quot;:                         2796,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.98,
        &quot;data_transferred&quot;:                     8.97,
        &quot;response_time&quot;:                        0.01,
        &quot;transaction_rate&quot;:                   280.16,
        &quot;throughput&quot;:                           0.90,
        &quot;concurrency&quot;:                          3.96,
        &quot;successful_transactions&quot;:              2796,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.63,
        &quot;shortest_transaction&quot;:                 0.00
}
+ siege -q -t 10S -c 8 -f urls.txt

{       &quot;transactions&quot;:                         3143,
        &quot;availability&quot;:                       100.00,
        &quot;elapsed_time&quot;:                         9.99,
        &quot;data_transferred&quot;:                    12.68,
        &quot;response_time&quot;:                        0.03,
        &quot;transaction_rate&quot;:                   314.61,
        &quot;throughput&quot;:                           1.27,
        &quot;concurrency&quot;:                          7.95,
        &quot;successful_transactions&quot;:              3143,
        &quot;failed_transactions&quot;:                     0,
        &quot;longest_transaction&quot;:                  0.59,
        &quot;shortest_transaction&quot;:                 0.01
}</code></pre>
<h2 id="appendix-urls.txt">Appendix: urls.txt</h2>
<pre><code>http://127.0.0.1:3000/search
http://127.0.0.1:3000/search?keywords=flatland&amp;author%5B%5D=&amp;location=&amp;match=&amp;category=
http://127.0.0.1:3000/search?keywords=flatland&amp;author%5B%5D=Ian+Stewart&amp;location=&amp;match=&amp;category=
http://127.0.0.1:3000/search?keywords=&amp;author%5B%5D=&amp;location=f256ed66-4c09-4207-86de-adc8e9fb86ec&amp;match=&amp;category=
http://127.0.0.1:3000/search?keywords=&amp;author%5B%5D=&amp;location=f256ed66-4c09-4207-86de-adc8e9fb86ec&amp;match=only-unread&amp;category=
http://127.0.0.1:3000/search?keywords=Before+Dawn&amp;author%5B%5D=&amp;location=f256ed66-4c09-4207-86de-adc8e9fb86ec&amp;match=only-unread&amp;category=
http://127.0.0.1:3000/search?keywords=Before+AND+Dawn&amp;author%5B%5D=&amp;location=f256ed66-4c09-4207-86de-adc8e9fb86ec&amp;match=only-unread&amp;category=
http://127.0.0.1:3000/search?keywords=&amp;author%5B%5D=Zzarchov+Kowolski&amp;location=&amp;match=only-read&amp;category=70196ec9-dd61-4241-afc9-dd6be7be30a6
http://127.0.0.1:3000/search.json
http://127.0.0.1:3000/book/9780486272634
http://127.0.0.1:3000/book/9780486272634/cover
http://127.0.0.1:3000/book/9780262510875/cover
http://127.0.0.1:3000/book/9780575082014/cover
http://127.0.0.1:3000/book/9780575079793/cover
http://127.0.0.1:3000/book/9780141397726/cover
http://127.0.0.1:3000/book/9780575086159/cover
http://127.0.0.1:3000/book/9780199535644/cover
http://127.0.0.1:3000/book/9780575077324/cover
http://127.0.0.1:3000/book/9781421578798/cover
http://127.0.0.1:3000/book/9780486272634/thumb
http://127.0.0.1:3000/book/9780262510875/thumb
http://127.0.0.1:3000/book/9780575082014/thumb
http://127.0.0.1:3000/book/9780575079793/thumb
http://127.0.0.1:3000/book/9780141397726/thumb
http://127.0.0.1:3000/book/9780575086159/thumb
http://127.0.0.1:3000/book/9780199535644/thumb
http://127.0.0.1:3000/book/9780575077324/thumb
http://127.0.0.1:3000/book/9781421578798/thumb
http://127.0.0.1:3000/static/style.css
http://127.0.0.1:3000/static/script.js</code></pre>
<h2 id="appendix-graph-script">Appendix: graph script</h2>
<pre class="python"><code>#! /usr/bin/env nix-shell
#! nix-shell -i python -p &quot;python3.withPackages (ps: [ps.matplotlib ps.numpy])&quot;

import matplotlib.pyplot as plt
import numpy as np

plt.xkcd()
plt.figure(figsize=(12,6))

labels = [&quot;Werkzeug&quot;, &quot;Gunicorn&quot;, &quot;uWSGI&quot;, &quot;Gevent&quot;]
bars = [(&quot;2 workers&quot;, [230.90, 310.98, 291.58, 214.02]),
        (&quot;4 workers&quot;, [265.07, 418.34, 399.10, 280.16]),
        (&quot;8 workers&quot;, [250.80, 567.64, 540.48, 314.61])]

bar_width = 0.25

rs = [np.arange(len(labels))]
for i in range(len(bars)-1):
    rs.append([x + bar_width for x in rs[-1]])

for i in range(len(bars)):
    plt.bar(rs[i], bars[i][1], width=bar_width, label=bars[i][0])

plt.ylabel(&quot;Transactions per second (higher is better)&quot;)
plt.xlabel(&quot;Server&quot;)
plt.xticks([r + bar_width for r in range(len(labels))], labels)

plt.legend()
plt.savefig(&quot;transaction-rate.png&quot;)</code></pre>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Migrate GOV.UK to Puma</title>
    <link href="https://memo.barrucadu.co.uk/migrate-govuk-to-puma.html" />
    <id>https://memo.barrucadu.co.uk/migrate-govuk-to-puma.html</id>
    <published>2020-12-23T00:00:00Z</published>
    <updated>2020-12-23T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>Mere hours after going on leave for the festive period, I’ve got back in the mood to do complicated tech things for fun, and the topic which came to mind is “how hard would it be to migrate GOV.UK from <a href="https://yhbt.net/unicorn/">Unicorn</a> (old and busted) to <a href="https://puma.io/">Puma</a> (new hotness)?”</p>
<p>Not only does Puma have a much prettier website, it’s also the Rails default web server (and has been for a while). So the wider ecosystem has decided it’s a better server. Furthermore, Puma potentially solves an awkward problem we have with Unicorn: <strong>memory usage</strong>.</p>
<p>Unicorn runs multiple worker processes, which can each take up quite a bit of RAM. It adds up quickly if you have multiple apps running on the same server. If a process is IO bound rather than CPU bound, this means scaling is more awkward, we either have to bring up new servers, or embiggen our current ones.</p>
<p>Puma, on the other hand, runs multiple threads within each worker process. Threads can be very lightweight, sharing almost all of their memory. So we can pack far more threads on the same server, so long as our application is not CPU bound.</p>
<p>I’ve done some thinking on how we could try out Puma on GOV.UK. These steps are untested, and are based on reading old puppet code and init scripts at 2AM, so follow them at your peril. But I think it would be something like this.</p>
<h2 id="configure-the-app">Configure the app</h2>
<p>The app needs a Puma config file. Eventually we would want <a href="https://github.com/alphagov/govuk_app_config/blob/master/lib/govuk_app_config/govuk_unicorn.rb">something shared in govuk_app_config</a>, but if we’re trying this out with a single app to start with, a config file in the app would do.</p>
<p>I think we’ll want something like this:</p>
<pre class="ruby"><code># frozen_string_literal: true

max_threads_count = ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 1 }
min_threads_count = ENV.fetch(&quot;RAILS_MIN_THREADS&quot;) { max_threads_count }
threads min_threads_count, max_threads_count

port ENV.fetch(&quot;PORT&quot;) { 3000 }

environment ENV.fetch(&quot;RAILS_ENV&quot;) { &quot;development&quot; }

workers ENV.fetch(&quot;UNICORN_WORKER_PROCESSES&quot;) { 2 }

preload_app!</code></pre>
<p>Puma concurrency is <code>threads * workers</code>. We can run Puma in the same way as we run Unicorn–configure the number of workers, but only give each 1 thread—which will let us see the performance impact of Puma by itself. We can also set <code>workers</code> lower and <code>threads</code> higher to start to get the memory savings. There’s probably some tweaking to be done.</p>
<h2 id="add-puma-support-to-unicornherder">Add Puma support to unicornherder</h2>
<p>Our <a href="https://github.com/gds-operations/unicornherder"><code>unicornherder</code></a> tool is a common abstraction over Unicorn and <a href="https://gunicorn.org/">Gunicorn</a>. We can add Puma support to it too:</p>
<pre class="python"><code>COMMANDS = {
    &#39;unicorn&#39;: &#39;unicorn -D -P &quot;{pidfile}&quot; {args}&#39;,
    &#39;unicorn_rails&#39;: &#39;unicorn_rails -D {args}&#39;,
    &#39;unicorn_bin&#39;: &#39;{unicorn_bin} -D -P &quot;{pidfile}&quot; {args}&#39;,
    &#39;gunicorn&#39;: &#39;gunicorn -D -p &quot;{pidfile}&quot; {args}&#39;,
    &#39;gunicorn_django&#39;: &#39;gunicorn_django -D -p &quot;{pidfile}&quot; {args}&#39;,
    &#39;gunicorn_bin&#39;: &#39;{gunicorn_bin} -D -p &quot;{pidfile}&quot; {args}&#39;
    &#39;puma&#39;: &#39;pumactl start -P &quot;{pidfile}&quot; {args}&#39;
}</code></pre>
<p>There’s also some logic around restarts, waiting for the old master process to terminate its workers gracefully and then kill it. I don’t think that will do anything useful under Puma, but I don’t think it’ll cause any problems either.</p>
<p><strong>Note:</strong> <code>unicornherder</code> sends a SIGUSR2 which, for Puma, will perform something like what we call a “deploy with hard restart”, where the old processes get killed and new ones brought up. However, the <a href="https://github.com/puma/puma/blob/master/docs/restart.md#hot-restart">puma docs</a> describe how things are handled gracefully:</p>
<ul>
<li>Any in-flight requests get handled before the server is shut down.</li>
<li>Any requests which start just as the server restarts will experience some latency, but will not be dropped.</li>
</ul>
<p>Since this is a full process restart, any new Ruby version will be used, and any change to the Puma config will be applied. This means we will no longer need to do a separate hard restart for Puma apps when upgrading Ruby!</p>
<p>Puma also offers a <a href="https://github.com/puma/puma/blob/master/docs/restart.md#phased-restart">phased restart</a> approach, which restarts one worker at a time, but that doesn’t reload the Puma master process, and so won’t pick up a new Ruby version or new Puma config. It’s also incompatible with the <code>preload_app!</code> option.</p>
<h2 id="add-puma-support-to-govuk_spinup">Add Puma support to govuk_spinup</h2>
<p>The confusing initialisation of a GOV.UK app begins in a sysvinit script, …</p>
<p>Which calls <a href="https://github.com/alphagov/govuk-puppet/blob/master/modules/govuk/files/usr/local/bin/govuk_spinup"><code>govuk_spinup</code></a>, …</p>
<p>Which calls <code>start-stop-daemon</code>, …</p>
<p>Which calls <code>unicornherder</code>, …</p>
<p>Which finally calls the app server.</p>
<p>I think the changes needed here are to <code>govuk_spinup</code>. We’ll need a new app type, let’s call it “puma”:</p>
<pre class="bash"><code>  puma)
    status &quot;Spawning rack app under puma&quot;

    if [ ! -e &#39;${GOVUK_APP_ROOT}/config/puma.rb&#39; ]; then
      error &quot;Missing Puma config file&quot;
    fi

    CMD=&quot;bundle exec unicornherder -u puma -p &#39;${GOVUK_APP_RUN}/app.pid&#39; -- -C &#39;${GOVUK_APP_ROOT}/config/puma.rb&#39;&quot;
    ;;</code></pre>
<p>There’s also a <a href="https://github.com/alphagov/govuk-puppet/blob/master/modules/govuk/files/usr/local/bin/govuk_unicorn_reload"><code>govuk_unicorn_reload</code></a> script, called during deploys, but I don’t think that needs to change.</p>
<h2 id="set-up-monitoring-for-puma-apps">Set up monitoring for Puma apps</h2>
<p>The <a href="https://github.com/alphagov/govuk-puppet/blob/master/modules/govuk/manifests/app/config.pp"><code>govuk::app::config</code></a> class in <a href="https://github.com/alphagov/govuk-puppet">govuk-puppet</a> defines a bunch of Icinga alerts which’ll need changing, or copying, for our new “puma” app type to be as monitored as it should be.</p>
<p>This:</p>
<pre class="puppet"><code>  # Set up monitoring
  if $app_type in [&#39;rack&#39;, &#39;bare&#39;, &#39;procfile&#39;] {
    $default_collectd_process_regex = $app_type ? {
      &#39;rack&#39; =&gt; &quot;unicorn (master|worker\\[[0-9]+\\]).* -P ${govuk_app_run}/app\\.pid&quot;,
      &#39;bare&#39; =&gt; inline_template(&#39;&lt;%= Regexp.escape(@command) + &quot;$&quot; -%&gt;&#39;),
      &#39;procfile&#39; =&gt; &quot;gunicorn .* ${govuk_app_run}/app\\.pid&quot;,
    }</code></pre>
<p>And this:</p>
<pre class="puppet"><code>  if ($app_type == &#39;rack&#39;) or $monitor_unicornherder {
    @@icinga::check { &quot;check_app_${title}_unicornherder_up_${::hostname}&quot;:
      ensure              =&gt; $ensure,
      check_command       =&gt; &quot;check_nrpe!check_proc_running_with_arg!unicornherder /var/run/${title}/app.pid&quot;,
      service_description =&gt; &quot;${title} app unicornherder not running&quot;,
      host_name           =&gt; $::fqdn,
      notes_url           =&gt; monitoring_docs_url(unicorn-herder),
      contact_groups      =&gt; $additional_check_contact_groups,
    }
  }</code></pre>
<p>And this:</p>
<pre class="puppet"><code>  if $app_type == &#39;rack&#39; {
    include icinga::client::check_unicorn_ruby_version
    @@icinga::check { &quot;check_app_${title}_unicorn_ruby_version_${::hostname}&quot;:
      ensure              =&gt; $ensure,
      check_command       =&gt; &quot;check_nrpe!check_unicorn_ruby_version!${title}&quot;,
      service_description =&gt; &quot;${title} is not running the expected ruby version&quot;,
      host_name           =&gt; $::fqdn,
      notes_url           =&gt; monitoring_docs_url(ruby-version),
      contact_groups      =&gt; $additional_check_contact_groups,
    }
  }</code></pre>
<h2 id="change-the-app-to-a-puma-app">Change the app to a Puma app</h2>
<p>Now that we’ve got our new app type, we need to stick <code>app_type =&gt; 'puma'</code> in the relevant call to <code>govuk::app</code> elsewhere in govuk-puppet.</p>
<p>And that’s it!</p>
<h2 id="finally-deploy-the-change">Finally, deploy the change</h2>
<p>Since we’re using proper init scripts with pidfile management, I <em>think</em> that deploying Puppet will be a graceful change:</p>
<ol type="1">
<li>Puppet will trigger a restart of the app due to the change to its config and <code>govuk_spinup</code>.</li>
<li>The init script will read the existing pidfile and stop the old Unicorn process in the usual SIGINT / SIGKILL way.</li>
<li>The init script will start the app up with Puma via the modified <code>govuk_spinup</code> / <code>unicornherder</code>.</li>
</ol>
<p>If not, I think the steps to deploy will be:</p>
<ol type="1">
<li>Pause Puppet on the affected (perhaps afflicted?) machines</li>
<li>Deploy Puppet</li>
<li>For each machine:
<ol type="1">
<li>Manually stop the app</li>
<li>Unpause and run Puppet</li>
</ol></li>
</ol>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>Automatically tagging audio files (using systemd and inotify)</title>
    <link href="https://memo.barrucadu.co.uk/automatically-tagging-music.html" />
    <id>https://memo.barrucadu.co.uk/automatically-tagging-music.html</id>
    <published>2020-10-14T00:00:00Z</published>
    <updated>2020-10-14T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>I follow several podcasts and, if you do the same, you may have noticed that podcast creators are <em>terrible</em> at consistently tagging their files. For example, is the artist the name of the podcast, the names of the presenters, just one of the presenters, some abbreviation, or the names of the presenters in a different order? Probably all of those, and more, get used inconsistently across the lifetime of a multi-year-old podcast.</p>
<p>Inconsistent tagging makes it a pain to use tools which use that information, which is pretty much every audio player.</p>
<p>For some years, my solution was a script which retagged all my podcasts. This can be done because I use a standard directory and file naming convention. But the downside is that it retagged every file of every podcast when ran, even though I’d only be adding one new file at a time.</p>
<h2 id="systemd-path-units">systemd path units</h2>
<p>I recently discovered <a href="https://www.freedesktop.org/software/systemd/man/systemd.path.html">systemd path units</a>, which seemed like the solution to this problem: I could have a script which was triggered by a file being created, tagged it, and moved it to the right place. Path units turned out not to be the solution to <em>this</em> problem, but they were a solution to a slightly different one.</p>
<p>My first attempt was to add a subdirectory to every podcast directory called <code>in</code>, and to write this path unit:</p>
<pre><code>[Unit]
Description=Automatically tag new podcast files
RequiresMountsFor=/mnt/nas

[Path]
PathExistsGlob=/mnt/nas/music/Podcasts/*/in/*.mp3</code></pre>
<p>And this service file:</p>
<pre><code>[Unit]

[Service]
Environment=&quot;PATH=&lt;...&gt;&quot;
ExecStart=/usr/local/bin/tag-podcasts.sh
Group=users
User=barrucadu
WorkingDirectory=/mnt/nas/music/Podcasts/</code></pre>
<p>And this bash script:</p>
<pre class="bash"><code>#!/usr/bin/env bash

for mp3file in */in/*.mp3; do
  dir=&quot;$(echo &quot;$mp3file&quot; | sed &#39;s:/in/.*::&#39;)&quot;
  f=&quot;$(basename &quot;$mp3file&quot;)&quot;

  artist=&quot;$(echo &quot;$dir&quot; | sed &#39;s: - .*::&#39;)&quot;
  album=&quot;$(echo &quot;$dir&quot; | sed &#39;s:.* - ::&#39;)&quot;

  if [[ -z &quot;$album&quot; ]]; then
    album=&quot;$artist&quot;
  fi

  n=&quot;$(echo &quot;$f&quot; | sed &#39;s:\..*::&#39;)&quot;
  track=&quot;$(echo &quot;$f&quot; | sed &#39;s:^[0-9]*\. \(.*\)\.mp3:\1:&#39;)&quot;

  echo &quot;===== $mp3file&quot; &gt;&amp;2
  echo $artist &gt;&amp;2
  echo $album &gt;&amp;2
  echo $n &gt;&amp;2
  echo $track &gt;&amp;2
  echo &quot;$(echo &quot;$mp3file&quot; | sed &#39;s:/in/:/:&#39;)&quot; &gt;&amp;2
  echo &gt;&amp;2

  id3v2 -D &quot;$mp3file&quot;
  id3v2 -2 --song   &quot;$track&quot;  &quot;$mp3file&quot;
  id3v2 -2 --track  &quot;$n&quot;      &quot;$mp3file&quot;
  id3v2 -2 --artist &quot;$artist&quot; &quot;$mp3file&quot;
  id3v2 -2 --album  &quot;$album&quot;  &quot;$mp3file&quot;
  mv &quot;$mp3file&quot; &quot;$(echo &quot;$mp3file&quot; | sed &#39;s:/in/:/:&#39;)&quot;
done</code></pre>
<p>This turned out not to work. The unit just didn’t pick up any file changes. Any one podcast would work<a href="automatically-tagging-music.html#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a>, but I didn’t really want to have to make a unit for each of my podcasts… I’d need to update my system configuration if I started following a new podcast; that feels like too much mixing of global configuration and how I (admittedly the single user of the system) use it.</p>
<p>So I had to give up on path units for tagging my podcasts.</p>
<h2 id="tagging-podcasts">Tagging podcasts</h2>
<p>I was too invested at this point to give up entirely, I wanted automatic tagging.</p>
<p>So I turned to <code>inotifywatch</code>, and stuck this at the end of my script:</p>
<pre class="bash"><code># this can&#39;t be done as a systemd path unit because it doesn&#39;t seem to
# support multiple *s in a pattern
inotifywait --recursive --timeout 3600 --include &#39;/mnt/nas/music/Podcasts/.*/in/.*\.mp3&#39; $(pwd) &gt;&amp;2

# this script is run in a loop by systemd.</code></pre>
<p>The next step was to make a systemd unit which just runs that script in a loop. Which is defined in my NixOS config as:</p>
<pre class="nix"><code>systemd.services.tag-podcasts = {
  enable = true;
  description = &quot;Automatically tag new podcast files&quot;;
  wantedBy = [&quot;multi-user.target&quot;];
  path = with pkgs; [ inotifyTools id3v2 ];
  unitConfig.RequiresMountsFor = &quot;/mnt/nas&quot;;
  serviceConfig = {
    WorkingDirectory = &quot;/mnt/nas/music/Podcasts/&quot;;
    ExecStart = pkgs.writeShellScript &quot;tag-podcasts.sh&quot; (fileContents ./tag-podcasts.sh);
    User = &quot;barrucadu&quot;;
    Group = &quot;users&quot;;
    Restart = &quot;always&quot;;
  };
};</code></pre>
<p>And now I’ve got a script which, once an hour (or on detecting a file change, whichever is sooner) tags all new podcast files and moves them to the correct directories. No more SSHing in and running my tagging script, I can just save a file as, eg, <code>How We Roll - Masks of Nyarlathotep/in/{number}. {title}.mp3</code>, over Samba or NFS, and within a few seconds it gets picked up, tagged, and organised. Nice.</p>
<h2 id="tagging-albums">Tagging albums</h2>
<p>I couldn’t use a single path unit to trigger my script for tagging podcasts, but that’s not the only time I want to tag some audio files. I have a collection of CDs, which I very infrequently add to, and I have those CDs ripped and stored as FLAC files. Appropriately tagged, of course.</p>
<p>I use <a href="http://exactaudiocopy.de/">Exact Audio Copy (EAC)</a> to rip my CDs to WAV, which uses a predictable directory layout and file naming convention. I already had a script to take an EAC directory and produce a tagged and organised FLAC directory, I just needed to make it automatic.</p>
<p>First, here’s my systemd configuration:</p>
<pre class="nix"><code>systemd.paths.flac-and-tag-album = {
  enable = true;
  description = &quot;Automatically flac and tag new albums&quot;;
  wantedBy = [&quot;multi-user.target&quot;];
  unitConfig.RequiresMountsFor = &quot;/mnt/nas&quot;;
  pathConfig.PathExistsGlob = &quot;/mnt/nas/music/to_convert/in/*&quot;;
};
systemd.services.flac-and-tag-album = {
  path = with pkgs; [ flac ];
  serviceConfig = {
    WorkingDirectory = &quot;/mnt/nas/music/to_convert/in/&quot;;
    ExecStart = pkgs.writeShellScript &quot;flac-and-tag-album.sh&quot; (fileContents ./flac-and-tag-album.sh);
    User = &quot;barrucadu&quot;;
    Group = &quot;users&quot;;
  };
};</code></pre>
<p>An album consists of multiple files, but I don’t want to try to convert an album where some of the files are still part-way through being copied to the NAS; that sounds like an easy way to end up with incomplete FLACs. So I came up with this workflow:</p>
<ol type="1">
<li>A CD is ripped to WAV with EAC on my desktop</li>
<li>The EAC directory is copied over to <code>/mnt/nas/music/to_convert</code> over Samba (which will take a few seconds)</li>
<li>Then the directory moved to <code>/mnt/nas/music/to_convert/in</code> (which will be instantaneous)</li>
<li>The path unit notices the new subdirectory, and triggers the script.</li>
</ol>
<p>And here’s the script:</p>
<pre class="bash"><code>#!/usr/bin/env bash

set -e

for artist in *; do
  if [[ -d $artist ]]; then
    pushd $artist
    for album in *; do
      if [[ -d $album ]]; then
        echo &quot;===== $artist - $album&quot; &gt;&amp;2
        pushd $album
        if [[ ! -e &quot;$artist - $album.log&quot; ]]; then
          echo &quot;(missing log file)&quot; &gt;&amp;2
        fi
        if [[ ! -e &quot;cover.jpg&quot; ]] &amp;&amp; [[ ! -e &quot;cover.png&quot; ]] &amp;&amp; [[ ! -e &quot;cover.gif&quot; ]]; then
          echo &quot;(missing cover file)&quot; &gt;&amp;2
        fi
        flac *.wav
        rm *.wav
        for flacfile in *.flac; do
          n=&quot;$(echo &quot;$flacfile&quot; | sed &#39;s:\..*::&#39;)&quot;
          track=&quot;$(echo &quot;$flacfile&quot; | sed &#39;s:^[0-9]*\. \(.*\)\.flac:\1:&#39;)&quot;
          metaflac --set-tag=&quot;tracknumber=$n&quot; &quot;$flacfile&quot;
          metaflac --set-tag=&quot;title=$track&quot;   &quot;$flacfile&quot;
          metaflac --set-tag=&quot;artist=$artist&quot; &quot;$flacfile&quot;
          metaflac --set-tag=&quot;album=$album&quot;   &quot;$flacfile&quot;
        done
        popd
        echo
        mv $album &quot;../../out/$artist - $album&quot;
      fi
    done
    popd
    rmdir $artist
  fi
done</code></pre>
<p>Nice and straightforward. I’ve not ripped any new CDs since setting this up earlier this week, but I converted some FLACs back to WAVs, shuffled the directory layout around, and tested that they got picked up and re-converted properly.</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>For example, if I changed the <code>PathExistsGlob</code> to something like <code>/mnt/nas/music/Podcasts/The H. P. Lovecraft Literary Podcast/in/*.mp3</code><a href="automatically-tagging-music.html#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>

      ]]>
    </summary>
  </entry>
  
  <entry>
    <title>AI Dungeon Fiction: The Red Sign</title>
    <link href="https://memo.barrucadu.co.uk/ai-dungeon-the-red-sign.html" />
    <id>https://memo.barrucadu.co.uk/ai-dungeon-the-red-sign.html</id>
    <published>2020-07-26T00:00:00Z</published>
    <updated>2020-07-26T00:00:00Z</updated>
    <summary type="html">
      <![CDATA[
<p>This story was generated by <a href="https://play.aidungeon.io/">AI Dungeon</a>. <strong>Bold text</strong> was written by me, the rest by the AI.</p>
<hr />
<p><strong>Your good friend Jackson Elias calls for your help with a telegram scarce on details. Something about the ill-fated Carlyle Expedition of 1920, where everyone ended up dead. He said he’s found something. You arrive at his hotel room and knock on the door, but get no response. The door is ajar. You push it open and see</strong> a man in a dark suit standing there.</p>
<p>“I’m here to help you,” says the stranger. “You’re not going to believe what I’ve found.”</p>
<p>He leads you into an empty office and closes the door behind him.</p>
<p><strong>You say “Where’s Jackson Elias?”</strong></p>
<p>“Jackson? What happened to him?”</p>
<p>The man turns around and looks directly at you. His face is pale, his eyes are bloodshot and his hair is disheveled. “I don’t know anything about that,”</p>
<p><strong>You say “This is his room! He should be here!”</strong></p>
<p>“The less you know about him the better. But I need your help.”</p>
<p>He takes you into the next room, where you see an ancient stone pedestal with an irregularly-shaped gem placed on it. A strange pattern of symbols surrounds it.</p>
<p><strong>You examine the pedestal.</strong></p>
<p>You approach the pedestal and examine the gem. It’s cut in an irregular fashion, with a dull gray color. You notice strange red markings etched into the surface of the gem.</p>
<p>“The Red Sign,” he says. “The key to uniting the worlds.”</p>
<p>“What?” you ask. “What does that mean?”</p>
<p>“You’re not a scholar, are you?”</p>
<p>“No. I’m a traveler.”</p>
<p>“Just as well. They’d skin you alive if they knew what you were saying. These are old secrets, forgotten by most.”</p>
<p>“I’m not afraid,” you say, puffing out your chest.</p>
<p>“Maybe you should be,” he says. ”</p>
<p><strong>You say “What’s all this about?”</strong></p>
<p>“The old ones. The Great Old Ones. The Lords of the Outer Rims. The Elder Gods. Gods from before the gods, some say. They dwell in a realm of twisted madness that lies on the far edges of reality, just outside the material world.”</p>
<p><strong>You say “Look man, I just came here because my friend Jackson Elias needed my help. If you won’t tell me where he is, I’ll call the police.”</strong></p>
<p>He stares at you, his eyes filled with a mixture of fear and desperation.</p>
<p>“Please, don’t do that,” he says. “They’re everywhere. And if they know I’ve betrayed them, they’ll take it out on my family.”</p>
<p>You look him up and down.</p>
<p><strong>You say “Who are you?”</strong></p>
<p>“Pardon me. I am Julian Cross. Jackson is my… brother in all but blood. We grew up together.”</p>
<p>He hands you a calling card, which you examine. It simply reads “Julian Cross, Antiquarian.”</p>
<p><strong>You say “Jackson never mentioned you”</strong></p>
<p>“No. We’ve had… a bit of a falling out. It’s a long story.”</p>
<p>“What happened?”</p>
<p>“I’ll tell you once we’re out of here.”</p>
<p>He approaches the pedestal.</p>
<p><strong>You say “This… Red Sign has something to do with Jackson?”</strong></p>
<p>“This is what they used. A powerful artifact, steeped in ancient and forgotten lore. It was to be the key to opening a gate to their realm.”</p>
<p>You stare at the Red Sign. You don’t even know what to say. This is madness.</p>
<p><strong>You say “Look, I don’t believe in any of this magic stuff. If it’ll make you feel better, I can draw an Elder Sign on that Red Sign. Jackson always said an Elder Sign would cancel out magic. Then you’ll tell me where he is, right?”</strong></p>
<p>He hesitates.</p>
<p>“Go ahead,” he says.</p>
<p>You pull out a pen and paper and begin drawing the Elder Sign. You step towards the pedestal and, on second thought, switch to a pencil. No need to take chances.</p>
<p>You step up to the pedestal and draw the Elder Sign on the Red Sign.</p>
<p>“There,” you say, stepping back. Suddenly, the lights begin to flicker.</p>
<p>“They know,” he says. “I’ve betrayed them. I have to go.”</p>
<p>The lights flicker again, then go out completely. You feel the door slam shut, then the sound of several bolts sliding into place.</p>
<p>“Julian?” you call out. Then you hear his terrified scream. Then, another voice… many voices… crying out in an incomprehensible language.</p>
<p>Then, a low rumbling. The sound of thousands of crawling insects… the sound of a hurricane… the sound of a crash of breaking glass…</p>
<p>Then, silence.</p>
<p>You continue to call out for Julian, but receive no answer.</p>
<p>The next time you wake up, you find yourself lying on the floor of the office. The desk has been overturned, and there’s a large hole in the window. A quick check reveals that it’s the next morning, and you’ve lost a full day.</p>

      ]]>
    </summary>
  </entry>
  
</feed>