<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <title>Fragments — brandur.org</title>
  <id>tag:brandur.org,2013:/fragments</id>
  <updated>2026-04-12T11:41:03-05:00</updated>
  <link rel="self" type="application/atom+xml" href="https://brandur.org/fragments.atom"></link>
  <link rel="alternate" type="text/html" href="https://brandur.org"></link>
  <entry>
    <title>Caveman</title>
    <summary>In 1980, Michael Crichton characters in &lt;em&gt;Congo&lt;/em&gt; spoke like cavemen to save satellite bandwidth. It was absurd. Ridiculous! Forty-five years later, we&amp;rsquo;re doing the same thing with LLMs to save tokens.</summary>
    <content type="html"><![CDATA[<p>An excerpt from Michael Crichton&rsquo;s <a href="https://en.wikipedia.org/wiki/Congo_(novel)">Congo (1980)</a>:</p>

<blockquote>
<p>“I don&rsquo;t understand,&rdquo; Elliot said. Ross explained that the &ldquo;M&rdquo; meant that there was more message, and he had to press the transmit button again. He pushed the button several times before he got the message, which in its entirety read:</p>

<blockquote>
<p>REVUWD ORGNL TAPE HUSTN NU FINDNG RE AURL SIGNL INFO-COMPUTR ANLYSS COMPLTE THNK ITS LNGWGE.</p>
</blockquote>

<p>Elliot found he could read the compressed shortline language by speaking it aloud: &ldquo;Reviewed original tape Houston, new finding regarding aural signal information, computer analysis complete think it&rsquo;s language.&rdquo; He frowned. &ldquo;Language?”</p>
</blockquote>

<p>Crichton was a gear guy. The story&rsquo;s protagonists took high tech satellite uplinks into the field, allowing transmission back to HQ, but due to the extreme expense of satellite bandwidth, having to read messages in shorthand like, &ldquo;REVUWD ORGNL TAPE HUSTN NU FINDNG&rdquo;.</p>

<p>I always found it ridiculous. Although these words have had their vowels removed, they&rsquo;re still uniquely intelligible in the English language. It&rsquo;d be trivial to write a short algorithm that&rsquo;d use a dictionary to expand the message back to uncompressed English on the receiving end. Or better yet, stop with the vowel thing and use a standard compression algorithm <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup>. You&rsquo;d get better results.</p>

<hr />

<p>Yesterday, I came across <a href="https://github.com/JuliusBrussee/caveman">Caveman</a>. Its job is to save tokens in Claude by having the LLM speak like a caveman, removing filler words and other niceties that make up a more fluently legible human language.</p>

<p>Before:</p>

<blockquote>
<p>&ldquo;Sure! I&rsquo;d be happy to help you with that. The issue you&rsquo;re experiencing is most likely caused by your authentication middleware not properly validating the token expiry. Let me take a look and suggest a fix.&rdquo;</p>
</blockquote>

<p>After:</p>

<blockquote>
<p>&ldquo;Bug in auth middleware. Token expiry check use <code>&lt;</code> not <code>&lt;=</code>. Fix:&rdquo;</p>
</blockquote>

<p>Crichton would&rsquo;ve loved it. 45 years later we&rsquo;ve come full circle, are back to speaking like cavemen again, and as an at-least-somewhat legitimate technical workaround. I don&rsquo;t know what I thought I knew anymore.</p>


]]></content>
    <published>2026-04-12T11:41:03-05:00</published>
    <updated>2026-04-12T11:41:03-05:00</updated>
    <link href="https://brandur.org/fragments/caveman"></link>
    <id>tag:brandur.org,2026-04-12:fragments/caveman</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>&#34;Somewhere&#34; (2010) review</title>
    <summary>Short review of Sofia Coppola&amp;rsquo;s 2010 movie &lt;em&gt;Somewhere&lt;/em&gt;. Unless I miss my mark, it&amp;rsquo;s the same movie as &lt;em&gt;Lost in Translation&lt;/em&gt;?</summary>
    <content type="html"><![CDATA[<p>I&rsquo;ve often cited Sofia Coppola&rsquo;s <em>Lost in Translation</em> (2003) as one of my favorite movies. I&rsquo;d never dug much into Coppola&rsquo;s other work, so imagine my delight to discover that she&rsquo;s made another movie, <em>Somewhere</em> (2010) with a similar premise.</p>

<p>I excitedly got to watching it, but was ultimately disappointed. There&rsquo;s room for two movies to have similar premises, but <em>Somewhere</em> takes that to another level. It&rsquo;s functionally the same film.</p>

<p>The macro/themes are the same &ndash; disengaged, burned-out actor stays long-term at a hotel. A young woman comes into his life with whom he feels a genuine human connection. She helps break his sad routine and rediscover joy. One is in Tokyo, one is in LA. In one the woman is a much younger stranger, in the other his daughter.</p>

<p>But overarching story aside, even specific scenes are strongly derivative:</p>

<ul>
<li>There&rsquo;s an absurdist foreign interview of the lead in each.</li>
<li>Both heavily feature scenes of characters lying in beds.</li>
<li>Each has meta-scenes of leads watching TV.</li>
<li>Both include a scene of another woman sleeping over with the lead, and the awkward morning after interaction with the young woman about it.</li>
<li>There are scenes of the characters swimming around in upscale hotel pools.</li>
<li>Each has a scene of the lead watching strippers.</li>
</ul>

<p>I understand having a few callbacks in there to the filmmaker&rsquo;s previous work, but this is something else.</p>

<p><em>Lost in Translation</em> is clearly the distantly better movie. My takeaway is that although it had a good script, Bill Murray and the overwhelming chemistry between him and Scarlett Johansson carried that movie. Switch out those two leads, and it&rsquo;s very possible that like <em>Somewhere</em>, almost no one would have heard of it.</p>
]]></content>
    <published>2026-04-06T19:09:44-05:00</published>
    <updated>2026-04-06T19:09:44-05:00</updated>
    <link href="https://brandur.org/fragments/somewhere"></link>
    <id>tag:brandur.org,2026-04-06:fragments/somewhere</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>The special hell of Bolt, Europe&#39;s Uber clone</title>
    <summary>I don&amp;rsquo;t like this app.</summary>
    <content type="html"><![CDATA[<p>I was in Latvia a few weeks ago. Riga&rsquo;s one of the Europeans cities without a good transit link from the airport into city. Snooping around online, I found that the recommended way to get a ride was the use of an app called Bolt, a European clone of Uber. I realize now that I didn&rsquo;t actually check that Uber wasn&rsquo;t available in Latvia, but I&rsquo;m not against experimenting with a new app here and there.</p>

<p>I used it twice to get to and from the city center, and it worked perfectly. Neither of my drivers spoke English and I didn&rsquo;t speak a word of Latvian, but that&rsquo;s what technology&rsquo;s for. The rides went off without a hitch and I got exactly where I was supposed to be both times.</p>

<p>I arrived in Lyon recently and figured, hey, this is Europe, why not try the European app again, and used Bolt.</p>

<h2 id="ride-attempt-1" class="link"><a href="#ride-attempt-1">Ride attempt no. 1</a></h2>

<p>Car pulls into airport, drives to the waiting spot, stops up ahead of me, I walk over to it, driver pulls away, and leaves the airport. Mystified, I photograph the guy&rsquo;s license plate as he drives off figuring I might need it for dispute evidence.</p>

<p>The driver doesn&rsquo;t cancel the ride as he rides off into the distance, leaving me to do it, presumably so it falls to me to pay the app&rsquo;s €7 cancellation fee.</p>

<h2 id="ride-attempt-2" class="link"><a href="#ride-attempt-2">Ride attempt no. 2</a></h2>

<p>I cancel and try again. I get a ride parked not far off, but with a message: &ldquo;This is an automated acceptance. This car is set to charge for another 45 minutes.&rdquo; Sure enough, it&rsquo;s unmoving and unresponsive, and eventually the ride times out (thankfully, avoiding another €7 charge).</p>

<h2 id="ride-attempt-3" class="link"><a href="#ride-attempt-3">Ride attempt no. 3</a></h2>

<p>No message this time, but another car that appears to be charging and/or long term parked (it&rsquo;s a Tesla, so I suspect charging again). I leave the app, waiting for the pick up to time out.</p>

<h2 id="ride-attempt-4" class="link"><a href="#ride-attempt-4">Ride attempt no. 4</a></h2>

<p>I give up on Bolt, and switch to Uber. I match a driver right away. It&rsquo;s almost suspicious how quickly I matched him. But this is good! Progress. He drives over and I walk up to meet him. I get in the car and we start moving. Finally, this fiasco is over.</p>

<p>But then a guy runs up to the driver&rsquo;s window. Hey, he shouts, you&rsquo;re our ride! We booked you on Bolt. We just talked about on the phone a few minutes ago, remember?</p>

<p>Knowing that his license plate and photo matches what&rsquo;s on their screen, the driver doesn&rsquo;t bother denying it, and instead just points to his phone&rsquo;s screen and says, I pick up Brandur. See?</p>

<p>Even as the car&rsquo;s &ldquo;winner&rdquo; (I&rsquo;m not sure if this was because I got to the car first or the Uber fare was more favorable for the driver), I have principles, and of course don&rsquo;t love this situation either, but my only alternative would be to get out and cancel the ride, for which I&rsquo;d surely get hit with another fee. Unfortunately my best option is to stay quiet about it, let the Bolt user get another ride, and give the driver a low rating later. Naturally, the driver didn&rsquo;t cancel the other guy&rsquo;s Bolt ride (at least as far as I observed from the back seat), which would&rsquo;ve left the user to eat the €7 fee.</p>

<p>As we drove away from the airport, I suddenly realized: wait! this must be what happened to <em>me</em> during my first ride.</p>

<h2 id="ride-attempt-1-part-2" class="link"><a href="#ride-attempt-1-part-2">Ride attempt no. 1, part 2</a></h2>

<p>I go back into the Bolt app and open a support conversation. This option is purposely hidden deep inside submenus of submenus of submenus, so it took me five minutes to find it. I explain what happened and include the photographic evidence. From the first response it&rsquo;s obvious they have me talking to an AI. I drop all formality, and type only the minimum viable number of characters to get the next response. The AI promises me a refund for my €7 cancellation fee, then proceeds to provide no refund.</p>

<p>Eventually I&rsquo;m escalated to a human operator, who somehow manages to be worse than the AI. After explaining the situation again, I&rsquo;m told that fine, in this extremely rare, never-before-seen, once-in-a-cosmic-era situation, they&rsquo;ll refund the €7 fee. But don&rsquo;t fuck up again!</p>

<p>Don&rsquo;t worry Bolt, I won&rsquo;t. My days of using you scam peddlers are over.</p>

<p>When something works well enough, it&rsquo;s easy to take it for granted. As much flak as Uber and Lyft take, my experience with Bolt made me stop and think that even given 10+ years and hundreds of rides on both apps, my bad experiences have numbered like maybe, two? That sort of quality bar isn&rsquo;t an easy thing to maintain.</p>
]]></content>
    <published>2025-07-15T07:50:08-07:00</published>
    <updated>2025-07-15T07:50:08-07:00</updated>
    <link href="https://brandur.org/fragments/special-hell-of-bolt-app"></link>
    <id>tag:brandur.org,2025-07-15:fragments/special-hell-of-bolt-app</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Occasionally injected clocks in Postgres</title>
    <summary>Using a coalescable parameter to stub time as necessary in tests, but otherwise use the shared database clock across all operations.</summary>
    <content type="html"><![CDATA[<p>In a standard app deployment that&rsquo;s scaled horizontally across many nodes, we can expect the clocks to be a little askew across the fleet. It&rsquo;s generally not a huge problem these days because our <a href="https://en.wikipedia.org/wiki/Network_Time_Protocol">use of NTP</a> is so good and so widespread, but minor drift is still present.</p>

<p>Where a single source of time authority is desired, a nice trick is to use the database. A single database is shared across all deployed nodes, so by using the database&rsquo;s <code>now()</code> function instead of <code>time.Now()</code> in code, we can expect perfect consistency across all created records.</p>

<p>But a downside of this approach is that it makes time hard to stub because Postgres&rsquo; time is hard to stub. Stubbing time is often a necessity in tests and not being able to do so is a deal breaker.</p>

<p>We&rsquo;ve been using a hybrid approach with some success. A call to <code>coalesce</code> prefers an injected timestamp if there is one, but falls back on <code>now()</code> most of the time (including in production) to share a clock.</p>

<h2 id="sql-sqlc" class="link"><a href="#sql-sqlc">Step 1: SQL + sqlc</a></h2>

<p>Here&rsquo;s a sample query showing the <code>coalesce</code> in action. <code>sqlc.narg</code> defines a parameter as nullable.</p>

<pre><code class="language-sql">-- name: QueuePause :execrows
UPDATE queue
SET paused_at = CASE
                WHEN paused_at IS NULL THEN coalesce(
                    sqlc.narg('now')::timestamptz,
                    now()
                )
                ELSE paused_at
                END
WHERE name = @name;
</code></pre>

<p>In <code>sqlc.yaml</code>, tell sqlc to emit nullable timestamps as <code>*time.Time</code> pointers:</p>

<pre><code class="language-yaml">version: &quot;2&quot;
sql:
  - engine: &quot;postgresql&quot;
    queries: ...
    schema: ...
    gen:
      go:
        overrides:
          - db_type: &quot;timestamptz&quot;
            go_type:
              type: &quot;time.Time&quot;
              pointer: true
            nullable: true
</code></pre>

<p>Which generates this code:</p>

<pre><code class="language-go">const queuePause = `-- name: QueuePause :execrows
UPDATE queue
SET
    paused_at = CASE WHEN paused_at IS NULL THEN coalesce($1::timestamptz, now()) ELSE paused_at END
WHERE CASE WHEN $2::text = '*' THEN true ELSE name = $2 END
`

type QueuePauseParams struct {
    Now  *time.Time
    Name string
}

func (q *Queries) QueuePause(ctx context.Context, db DBTX, arg *QueuePauseParams) (int64, error) {
    result, err := db.Exec(ctx, queuePause, arg.Now, arg.Name)
    if err != nil {
        return 0, err
    }
    return result.RowsAffected(), nil
}
</code></pre>

<h2 id="stubbable-time-generator" class="link"><a href="#stubbable-time-generator">Step 2: Stubabble time generator</a></h2>

<p>Working in Go, define a <code>TimeGenerator</code> interface:</p>

<ul>
<li>When unstubbed, it returns the current time from <code>NowUTC()</code> or <code>nil</code> from <code>NowUTCOrNil()</code>.</li>
<li>When stubbed, it returns the stubbed time from <code>NowUTC()</code> or a pointer version of the same from <code>NowUTCOrNil()</code>.</li>
</ul>

<pre><code class="language-go">// TimeGenerator generates a current time in UTC. In test
// environments it's implemented by TimeStub which lets the
// current time be stubbed. Otherwise, it's implemented as
// UnstubbableTimeGenerator which doesn't allow stubbing.
type TimeGenerator interface {
    // NowUTC returns the current time. This may be a stubbed
    // time if the time has been actively stubbed in a test.
    NowUTC() time.Time

    // NowUTCOrNil returns if the currently stubbed time _if_
    // the current time is stubbed, and returns nil otherwise.
    // This is generally useful in cases where a component may
    // want to use a stubbed time if the time is stubbed, but
    // to fall back to a database time default otherwise.
    NowUTCOrNil() *time.Time
}
</code></pre>

<p>A stubbable implementation for tests:</p>

<pre><code class="language-go">type TimeStub struct {
    nowUTC *time.Time
}

func (t *TimeStub) NowUTC() time.Time {
    if t.nowUTC == nil {
        return time.Now().UTC()
    }

    return *t.nowUTC
}

func (t *TimeStub) NowUTCOrNil() *time.Time {
    return t.nowUTC
}

func (t *TimeStub) StubNowUTC(nowUTC time.Time) time.Time {
    t.nowUTC = &amp;nowUTC
    return nowUTC
}
</code></pre>

<p>An unstubbable time generator for production:</p>

<pre><code class="language-go">type UnstubbableTimeGenerator struct{}

func (g *UnstubbableTimeGenerator) NowUTC() time.Time       { return time.Now() }
func (g *UnstubbableTimeGenerator) NowUTCOrNil() *time.Time { return nil }

func (g *UnstubbableTimeGenerator) StubNowUTC(nowUTC time.Time) time.Time {
    panic(&quot;time not stubbable outside tests&quot;)
}
</code></pre>

<h3 id="shared-time-generator" class="link"><a href="#shared-time-generator">Step 3: Distributing a shared time generator</a></h3>

<p>The next key aspect is that all code needs to share a single instance of <code>TimeGenerator</code> so that when it&rsquo;s stubbed from a test, all services and subservices get the same stubbed value.</p>

<p>We put a <code>TimeGenerator</code> on a base service archetype that&rsquo;s automatically injected from top-level services to subservices:</p>

<pre><code class="language-go">func (c *Client[TTx]) QueuePauseTx(ctx context.Context, tx TTx, name string, opts *QueuePauseOpts) error {
    executorTx := c.driver.UnwrapExecutor(tx)

    if err := executorTx.QueuePause(ctx, &amp;QueuePauseParams{
        Name:   name,
        Now:    c.baseService.Time.NowUTCOrNil(), // &lt;-- accessed here
        Schema: c.config.Schema,
    }); err != nil {
        return err
    }
</code></pre>

<p>By default, it&rsquo;s instantiated as <code>UnstubbableTimeGenerator</code>. From tests, it&rsquo;s a <code>TimeStub</code>:</p>

<pre><code class="language-go">func BaseServiceArchetype(tb testing.TB) *baseservice.Archetype {
    tb.Helper()

    return &amp;baseservice.Archetype{
        Logger: Logger(tb),
        Time:   &amp;TimeStub{},
    }
}
</code></pre>

<p>In a test, time is stubbed like:</p>

<pre><code class="language-go">stubbedNow := client.baseService.Time.StubNowUTC(time.Now().UTC())
</code></pre>

<h2 id="loose-conviction" class="link"><a href="#loose-conviction">Loose conviction</a></h2>

<p>Consider this one a loose recommendation. It&rsquo;s useful in some situations where timestamp consistency is critically important, but not in others where it isn&rsquo;t. Server clocks tend to be pretty good nowadays, and it&rsquo;s a lot of code to avoid a few tens of microseconds worth of drift.</p>

<p>Also, consider that there might be a downside to using the database clock. In SQL, <code>CURRENT_TIMESTAMP</code> and <code>now()</code> in Postgres represent the current time <em>at the start of the current transaction</em> rather than the current time. This might be a benefit as all records created during a transaction are assigned the same created time, but it&rsquo;s just as often undesirable because depending on the duration of the transaction, timestamps can be wildly unrepresentative of when things actually happened.</p>
]]></content>
    <published>2025-06-29T10:48:16-07:00</published>
    <updated>2025-06-29T10:48:16-07:00</updated>
    <link href="https://brandur.org/fragments/postgres-clocks"></link>
    <id>tag:brandur.org,2025-06-29:fragments/postgres-clocks</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Testing the graceful handling of request cancellation in Go, 499s</title>
    <summary>Using built-in &lt;code&gt;net/http&lt;/code&gt; facilities to make sure that canceled requests are abandoned immediately to save time and resources.</summary>
    <content type="html"><![CDATA[<p>We had a situation a few days ago where a lazy loading problem in our Ruby code led to long running requests that our Dashboard, with an optimistic five second deadline on backend requests, was timing out. This raised a question in Slack: if our frontend does time out a backend request, does the request keep running? Or does the API know how to save resources by abandoning it midway through?</p>

<p>If the API stack&rsquo;s being bombarded by expensive requests that are largely being canceled early, it&rsquo;s a huge optimization to make sure that they only use the resources that they absolutely to. Requests discarded early stop executing immediately and no further effort is put toward servicing them.</p>

<p>In most code I&rsquo;ve ever worked in, I could quite confidently answer the question above with a definitive and resounding &ldquo;no&rdquo;. Doing a good job of request cancellation requires it be baked quite deeply into language and low level libraries, which isn&rsquo;t common. And even when those handle it well, userland code usually doesn&rsquo;t. Also, cancelling a request midway in services that don&rsquo;t use transactions would be unacceptably dangerous &ndash; <a href="/acid#atomicity">mutated state would be left mutated</a>, and that&rsquo;d cause untold trouble later on.</p>

<h2 id="go-cancellation" class="link"><a href="#go-cancellation">Cancellation in Go</a></h2>

<p>But in a Go stack, the built-in HTTP server <a href="https://pkg.go.dev/net/http#Request.Context">should handle cancellations using context</a>:</p>

<blockquote>
<p>For incoming server requests, the context is canceled when the client&rsquo;s connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns.</p>
</blockquote>

<p>And with our code being widely safeguarded by transactions, the feature should even be safe to use!</p>

<h2 id="prove-it" class="link"><a href="#prove-it">Now prove it</a></h2>

<p>Theory is one thing, but reality is another. If request cancellations indeed work, we should be able to prove it, so I set up a little bootstrap in pursuit of that. To make testing easy, add an artificial API endpoint waiting on sleep or context finished:</p>

<pre><code class="language-go">select {
case &lt;-time.After(5 * time.Second):
case &lt;-ctx.Done():
        return nil, ctx.Err()
}
</code></pre>

<p>Start the API server. Then from another terminal, run cURL and interrupt it after a few seconds:</p>

<pre><code class="language-sh">$ curl -i http://localhost:5222/sleep
^C
</code></pre>

<p>I found that we were handling canceled requests reasonably well, but that the error we were logging wasn&rsquo;t right. The code was checking context cancellation, but getting confused between context that was canceled from the HTTP server versus one canceled by our built-in timeout middleware, improperly sending a <code>408 Request timeout</code> to logs.</p>

<h2 id="local-vs-request" class="link"><a href="#local-vs-request">Local vs. request context</a></h2>

<p>After a little refactoring, I ended up with this code:</p>

<pre><code class="language-go">func (e *APIEndpoint[TReq, TResp]) Execute(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Add a default timeout for all API requests to ensure there's
    // always a backstop in case of degenerate behavior. Rescued
    // below and turned into a more user-friendly error.
    ctx, cancel := context.WithTimeout(ctx, RequestTimeout)
    defer cancel()
    
    ...
    
    ret, err := e.serviceHandler(ctx, req)
    if err != nil {
        if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
            // Distinct error message when the request itself was
            // canceled above the API stack versus we had a
            // cancellation/timeout occur within the API endpoint.
            if r.Context().Err() != nil {
                // This is a non-standard status code (499), but
                // fairly widespread because Nginx defined it.
                err = apierror.NewClientClosedRequestError(ctx, errMessageRequestCanceled).WithSpecifics(ctx, err)
            } else {
                err = apierror.NewRequestTimeoutError(ctx, errMessageRequestTimeout).WithSpecifics(ctx, err)
            }
        }

        WriteError(ctx, w, err)
        return
    }
</code></pre>

<p>Should a context error occur, we return a <code>408 Request timeout</code> in case of a timeout on local <code>ctx</code>, but a <code>499 Client closed request</code> if context was canceled upstream by the HTTP server canceling <code>r.Context()</code>.</p>

<p><code>499</code> isn&rsquo;t real status code, but rather one invented by Nginx which happens to be useful here. It doesn&rsquo;t really matter what status code we use because the end user (who canceled the request before the status code returned) will never see it. It&rsquo;s purely for our own logging and telemetry.</p>

<p>Looking at local logs running the sleep/cancel routine, I now see this:</p>

<pre><code class="language-txt">canonical_api_line GET /sleep -&gt; 499 (4.162702459s)
    api_error_cause=&quot;context canceled&quot;
    api_error_internal_code=client_closed_request
    api_error_message=&quot;Context of incoming request canceled; API endpoint stopped executing.&quot;
</code></pre>

<h3 id="generalizing-cancellation" class="link"><a href="#generalizing-cancellation">Generalizing cancellation handling</a></h3>

<p>Although our demo uses an artificial sleep statement, importantly this still works for any normal requests. Our code isn&rsquo;t littered with <code>&lt;-ctx.Done()</code> checks all over the place, but it does have a great many database operations like this one:</p>

<pre><code class="language-go">account, err := dbsqlc.New().AccountTouchLastSeenAt(ctx, e, apiKey.AccountID)
if err != nil {
    return nil, xerrors.Errorf(&quot;error looking up account: %w&quot;, err)
}
</code></pre>

<p>These call into Sqlc which call into Pgx, and Pgx detects a canceled context and sends back an error. In the event of a canceled request, the first database operation will come back with an error that&rsquo;ll bubble back up the stack to our API endpoint infrastructure. There it&rsquo;ll be turned it into a <code>499</code>. Subsequent database operations won&rsquo;t run, saving time and resources.</p>

<pre><code class="language-go">// API service handler error handling. Repeated from above.
ret, err := e.serviceHandler(ctx, req)
if err != nil {
    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
        // Distinct error message when the request itself was
        // canceled above the API stack versus we had a
        // cancellation/timeout occur within the API endpoint.
        if r.Context().Err() != nil {
            // This is a non-standard status code (499), but
            // fairly widespread because Nginx defined it.
            err = apierror.NewClientClosedRequestError(ctx, errMessageRequestCanceled).WithSpecifics(ctx, err)
        } else {
            err = apierror.NewRequestTimeoutError(ctx, errMessageRequestTimeout).WithSpecifics(ctx, err)
        }
    }

    WriteError(ctx, w, err)
    return
}
</code></pre>

<p>Pgx is one example of a library that&rsquo;ll check context cancellation, but it&rsquo;ll generally occur in any low level library that&rsquo;s doing I/O. As another example, SDKs like AWS or Stripe will usually go through <code>net/http</code>, which will catch them.</p>

<p>With code exercised (and adequate new testing in place), I was confident returning to Slack and declaring that &ldquo;yes&rdquo;, request cancellation is handled smoothly. I can&rsquo;t say the same about our Ruby code, but that&rsquo;s an adventure for another day.</p>
]]></content>
    <published>2025-06-20T00:16:09+02:00</published>
    <updated>2025-06-20T00:16:09+02:00</updated>
    <link href="https://brandur.org/fragments/testing-request-cancellation"></link>
    <id>tag:brandur.org,2025-06-20:fragments/testing-request-cancellation</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Be careful with Dropbox</title>
    <summary>Fun times with Dropbox&amp;rsquo;s new &lt;code&gt;~/Library/CloudStorage&lt;/code&gt; location and File Provider API integration.</summary>
    <content type="html"><![CDATA[<p>I&rsquo;ve been a Dropbox users going on fifteen years now. It&rsquo;s one of the most frustrating products in my arsenal because fifteen years ago it was <em>perfect</em>, but every new release just makes it a little bit worse than it was before. It&rsquo;s still fine to use, but you can see the writing on the wall as the long term trend is all in the wrong direction.</p>

<p>Despite that, I previously would&rsquo;ve lavished it with praise in that I&rsquo;ve never once had trouble with data loss or data integrity. Despite increasing feature bloat, it did what it was supposed to, syncing files to the right places, and doing so <em>reliably</em>, which is pretty much all I need out of it.</p>

<p>That ended Friday, when I was installing Dropbox on a new laptop. My Dropbox size runs ~500 GB, so when credentialing a new machine, I copy it from another computer on the network for speed, and to conserve precious bandwidth <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup> :</p>

<ul>
<li>Rsync <code>~/Dropbox</code> from an existing computer to the new one.</li>
<li><code>brew install dropbox</code>. Open it, log in, close it.</li>
<li>Replace the contents of <code>~/Dropbox</code> with the <code>rsync</code>ed copy.</li>
<li>Open Dropbox, let it sync against the new data. It should find everything it needs already there.</li>
</ul>

<p>Dropbox made a change in the last couple years wherein they moved the standard <code>~/Dropbox</code> on Mac to a new <code>~/Library/CloudStorage/Dropbox</code> location. I now know that folders in this directory are meant for use with Apple&rsquo;s <a href="https://developer.apple.com/documentation/fileprovider/">File Provider API</a>.</p>

<p>Apparently the change had been introduced for macOS Ventura (two major versions ago), but there must&rsquo;ve been an incremental roll out because I set up a computer last year and didn&rsquo;t run into it then. Once you&rsquo;ve been opted into the feature, you cannot opt out. Changing back to <code>~/Dropbox</code> is not an option.</p>

<p>Normally I do a wholesale swap of <code>~/Dropbox</code> with my locally copied version, but seeing this new magic folder in <code>~/Library</code>, I worried there&rsquo;d be some irreversible effect if I did it the normal way. Instead, I closed Dropbox, <code>cd</code>ed into the folder to <code>rm</code> all the files acting as cloud &ldquo;stubs&rdquo;, intending to replace them with materialized versions from my local copy.</p>

<p>What a mistake. I dumbly assumed that with Dropbox closed, any changes I made to the folder would be safe, just like they were in every previous version of Dropbox. Not so. At all.</p>

<p>I got suspicious after about ten seconds. Normally an <code>rm</code> even on gigantic directories is near instant, but this one was running long. I <code>SIGINT</code>ed it, but the damage was done.</p>

<p>I&rsquo;m sure you guessed what happened already. <code>~/Library/CloudStorage</code> is a magic location, and folders in it use macOS extension voodoo to make arbitrary changes in a cloud storage API. Despite Dropbox not being open, it&rsquo;d used a Mac API to intercept the <code>rm</code> and started to remove everything. My other computers had already synced the deletions. 100s of GBs gone in seconds.</p>

<p>Dropbox has a good &ldquo;undelete&rdquo; function, so I was able to log into their web UI and recover all the deleted files, but I was left with the problem of all my other computers having purged their local contents, with potentially 100s of GBs on each needing to be synced back down (and I thought I was <em>saving</em> bandwidth when I started doing this). Worse yet, Dropbox puts any files it deletes into a <code>~/Dropbox/.dropbox.cache</code> directory, but can&rsquo;t reuse any of that data when files are recovered, so it just makes a copy. Dropbox doesn&rsquo;t purge its cache often, even if disk space gets critically low, so every computer potentially needed 2 * 500 GB =~ 1 TB of free space for the full recovery, which they didn&rsquo;t have.</p>

<p>Two days later, I got everything back to where it should be, but all I could think afterwards was what a stupid, unforced error this all was. A mandatory move to <code>~/Library/CloudStorage/Dropbox</code>/File Provider API has no marginal utility for the user <sup id="footnote-2-source"><a href="#footnote-2">2</a></sup>, even if it makes product managers at the company feel good about themselves.</p>

<p>Being particular incensed at this moment, I started looking into alternatives immediately. There&rsquo;s dozens out there, but my approximate evaluation is that there isn&rsquo;t one that&rsquo;s a crystal clear, unambiguous win that&rsquo;d I&rsquo;d be excited about doing the work to switch over to, which is too bad.</p>

<p>What I&rsquo;m really looking for is Dropbox circa 2011. The one without the gratuitous/dangerous product changes, without an Electron app, and without the nags to upgrade my account which I already pay $120/year for.</p>

<p>Anyway, I doubt most users will run into this one as it was a confluence of stupid things that led me down this path, but I&rsquo;d just caution like the title says: be careful with Dropbox. Don&rsquo;t <code>rm</code> too much. Don&rsquo;t assume intuitive cause and effect. Don&rsquo;t assume operations are safe even if the app is closed.</p>


]]></content>
    <published>2025-05-26T00:28:04-06:00</published>
    <updated>2025-05-26T00:28:04-06:00</updated>
    <link href="https://brandur.org/fragments/careful-with-dropbox"></link>
    <id>tag:brandur.org,2025-05-26:fragments/careful-with-dropbox</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Optimizing JPEGs with MozJPEG for local archival</title>
    <summary>Writing a wrapper script around MozJPEG to achieve ~80% compression on large JPEGs with little downside.</summary>
    <content type="html"><![CDATA[<p>Call me old fashioned, but I like to keep my photo collection as local files on disk rather than symbolic pointers in the cloud, or sent off to deep storage on large archival drives, neither of which I&rsquo;m likely to ever look at again. It&rsquo;s nice having quick access to them that still works over a bad internet link or on an airplane.</p>

<p>It&rsquo;s a great system, but it&rsquo;s been getting more difficult as time goes by. My photo collection grows year by year, but Apple&rsquo;s hard drive sizes stay frozen circa 2012. I&rsquo;m running the same 1 TB drive that I was five years ago, which is only incrementally larger than five years before that (and even the mizerly 1 TB is still a $200 upcharge over the default <em>512 GB</em> that&rsquo;s somehow a thing that Apple sells in 2025).</p>

<p>Realistically, I know that I&rsquo;ll never look at the majority of these photos again, so I already prune the collections aggressively to keep only the highlights, but was looking for storage opportunities beyond that. Years ago I wrote about <a href="/fragments/libjpeg-mozjpeg">optimizing JPEGs for this site using MozJPEG</a>, and knowing that a lot of cameras produce suboptimally compressed JPEGs, realized there was a similar opportunity for archival.</p>

<p>I ended up writing a wrapper around around MozJPEG that saves about 80% of space compared to what comes out of my camera. Here&rsquo;s a sample run:</p>

<pre><code>
    $ optimize 001-ana-nuevo/*
    created: 001-ana-nuevo/2W4A6210.jpg (9.02MB -&gt; 2.11MB / saved 77%)
    created: 001-ana-nuevo/2W4A6212.jpg (8.21MB -&gt; 1.79MB / saved 78%)
    created: 001-ana-nuevo/2W4A6216.jpg (11.0MB -&gt; 2.68MB / saved 76%)
    created: 001-ana-nuevo/2W4A6218.jpg (6.36MB -&gt; 1.29MB / saved 80%)
    created: 001-ana-nuevo/2W4A6219.jpg (12.11MB -&gt; 3.01MB / saved 75%)
    created: 001-ana-nuevo/2W4A6224.jpg (7.3MB -&gt; 1.69MB / saved 77%)
    created: 001-ana-nuevo/2W4A6228.jpg (7.75MB -&gt; 1.72MB / saved 78%)
    created: 001-ana-nuevo/2W4A6230.jpg (8.62MB -&gt; 1.99MB / saved 77%)
    created: 001-ana-nuevo/2W4A6236.jpg (8.14MB -&gt; 1.87MB / saved 77%)
    created: 001-ana-nuevo/2W4A6237.jpg (6.65MB -&gt; 1.48MB / saved 78%)
    created: 001-ana-nuevo/2W4A6238.jpg (7.59MB -&gt; 1.69MB / saved 78%)
    created: 001-ana-nuevo/2W4A6240.jpg (9.38MB -&gt; 2.21MB / saved 76%)
    created: 001-ana-nuevo/2W4A6242.jpg (9.26MB -&gt; 2.22MB / saved 76%)
    created: 001-ana-nuevo/2W4A6243.jpg (10.17MB -&gt; 2.44MB / saved 76%)
    created: 001-ana-nuevo/2W4A6247.jpg (10.49MB -&gt; 2.56MB / saved 76%)
    created: 001-ana-nuevo/2W4A6251.jpg (7.92MB -&gt; 1.84MB / saved 77%)
    created: 001-ana-nuevo/2W4A6252.jpg (8.97MB -&gt; 2.12MB / saved 76%)
    created: 001-ana-nuevo/2W4A6253.jpg (7.74MB -&gt; 1.75MB / saved 77%)
    created: 001-ana-nuevo/2W4A6254.jpg (9.43MB -&gt; 2.3MB / saved 76%)
    created: 001-ana-nuevo/2W4A6255.jpg (10.78MB -&gt; 2.65MB / saved 75%)
    created: 001-ana-nuevo/2W4A6258-pups.jpg (9.13MB -&gt; 2.22MB / saved 76%)
    created: 001-ana-nuevo/2W4A6259.jpg (10.46MB -&gt; 2.55MB / saved 76%)
    created: 001-ana-nuevo/2W4A6260.jpg (8.54MB -&gt; 2.04MB / saved 76%)
    created: 001-ana-nuevo/2W4A6262.jpg (10.3MB -&gt; 2.59MB / saved 75%)
    created: 001-ana-nuevo/2W4A6266.jpg (8.81MB -&gt; 2.19MB / saved 75%)
    created: 001-ana-nuevo/2W4A6267.jpg (9.64MB -&gt; 2.31MB / saved 76%)
    created: 001-ana-nuevo/2W4A6268.jpg (9.83MB -&gt; 2.33MB / saved 76%)
    created: 001-ana-nuevo/2W4A6269.jpg (8.93MB -&gt; 2.14MB / saved 76%)
    created: 001-ana-nuevo/2W4A6271.jpg (7.38MB -&gt; 1.74MB / saved 76%)
    created: 001-ana-nuevo/2W4A6272.jpg (7.19MB -&gt; 1.68MB / saved 77%)
    created: 001-ana-nuevo/2W4A6283-water-fight.jpg (7.65MB -&gt; 1.73MB / saved 77%)
    created: 001-ana-nuevo/2W4A6284.jpg (8.02MB -&gt; 1.77MB / saved 78%)
    created: 001-ana-nuevo/2W4A6286.jpg (5.82MB -&gt; 1.11MB / saved 81%)
    created: 001-ana-nuevo/2W4A6287.jpg (6.03MB -&gt; 1.14MB / saved 81%)
</code></pre>

<p>I&rsquo;m sure there&rsquo;s some subtle downside to the extra compression, but I&rsquo;ve tried zooming all the way in on a couple samples before and after, and I can see differences right at the pixel level, but the optimized version isn&rsquo;t clearly worse to my eye.</p>

<p>My script&rsquo;s use-at-your-own-risk me-ware that I&rsquo;m not publishing in any official sense, but <a href="https://gist.github.com/brandur/8a7a7c7870fce52bcf1ac0c34d66af30">here it is for reference</a>.</p>

<p>Some gotchas I ran into and which might save someone else time/trouble:</p>

<ul>
<li><p>The MozJPEG binary to compress JPEGs is called <code>cjpeg</code>. This is an old Linux style project, and naming the binary after the project would make things too easy and too obvious for users. Under the strict edicts of 1970s Unix philosophy, that&rsquo;s completely unacceptable.</p></li>

<li><p>You might have multiple packages on your system providing <code>cjpeg</code>. Make sure you&rsquo;re using MozJPEG&rsquo;s because it offers much better compression than libjpeg or libjpeg-turbo. You can see here that my default <code>cjpeg</code> is <em>not</em> MozJPEG&rsquo;s:</p></li>
</ul>

<pre><code class="language-sh">$ which cjpeg
/opt/homebrew/bin/cjpeg

$ ls -l /opt/homebrew/bin/cjpeg
lrwxr-xr-x@ 1 brandur  admin    36B Feb 10 11:45 /opt/homebrew/bin/cjpeg -&gt; ../Cellar/jpeg-turbo/3.1.0/bin/cjpeg
</code></pre>

<ul>
<li><p>The original libjpeg <code>cjpeg</code> didn&rsquo;t support <em>reading</em> JPEGs, only writing them, and would encourage you to read JPEGs with another binary called <code>djpeg</code> and pipe that into <code>cjpeg</code> (again, the wonders of Unix philosophy). You can do that with MozJPEG too, but DO NOT DO THAT! Piping will strip EXIF data, which <a href="/fragments/stop-stripping-exif">you shouldn&rsquo;t do</a>. Unlike libjpeg&rsquo;s version, MozJPEG&rsquo;s <code>cjpeg</code> does read JPEGs, so piping is not necessary.</p></li>

<li><p>If you&rsquo;re writing to a new a file and then replacing the original after (which you probably should for safety), make sure to copy the original create/modify timestamps to the new file. The easiest way to do this is with <code>touch -r &lt;original&gt; &lt;new</code>&gt;`</p></li>
</ul>

<pre><code class="language-txt">TOUCH(1)						      General Commands Manual							  TOUCH(1)

NAME
     touch – change file access and modification times

SYNOPSIS
     touch [-A [-][[hh]mm]SS] [-achm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] [-d YYYY-MM-DDThh:mm:SS[.frac][tz]] file ...

DESCRIPTION
     The touch utility sets the modification and access times of
     files. If any file does not exist, it is created with default
     permissions.

     By default, touch changes both modification and access times.
     The -a and -m flags may be used to select the access time or
     the modification time individually.  Selecting both is
     equivalent to the default.  By default, the timestamps are set
     to the current time. The -d and -t flags explicitly specify a
     different time, and the -r flag specifies to set the times
     those of the specified file.  The -A flag adjusts the values
     by a specified amount.

     The following options are available:

     ...

     -r      Use the access and modifications times from the
             specified file instead of the current time of day.
</code></pre>

<p>Another approach would be to do away with JPEG completely and go to HEIC or WebP, but I&rsquo;m still finding support for those a little spotty, and navigating them in a file browser feels slow because the compression takes longer to render. I&rsquo;ll check in on that again in a year or two.</p>
]]></content>
    <published>2025-03-29T12:35:10-07:00</published>
    <updated>2025-03-29T12:35:10-07:00</updated>
    <link href="https://brandur.org/fragments/optimizing-jpegs-for-archival"></link>
    <id>tag:brandur.org,2025-03-29:fragments/optimizing-jpegs-for-archival</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>The right way to do data fixtures in Go</title>
    <summary>A safe, succinct test data fixtures pattern using sqlc and validator.</summary>
    <content type="html"><![CDATA[<p>Every test suite should start early in building a strong convention to generate data fixtures. If it doesn&rsquo;t, data fixtures will still emerge (they&rsquo;re that necessary), but in a way that&rsquo;s poorly designed, with no API (or a poorly designed one), and not standardized.</p>

<p>Other languages tend to have common libraries for fixture generation. As if often does, Go goes its own way and doesn&rsquo;t have a ubiquitous fixtures package, but especially when combining sqlc and <a href="https://github.com/go-playground/validator">validator</a>, it does well without one.</p>

<p>Here&rsquo;s one of our project&rsquo;s 130 fixtures:</p>

<pre><code class="language-go">package dbfactory

type MultiFactorOpts struct {
    ID          *uuid.UUID              `validate:&quot;-&quot;`
    AccountID   uuid.UUID               `validate:&quot;required&quot;`
    ActivatedAt *time.Time              `validate:&quot;-&quot;`
    ExpiresAt   *time.Time              `validate:&quot;-&quot;`
    Kind        *dbsqlc.MultiFactorKind `validate:&quot;-&quot;`
}

func MultiFactor(ctx context.Context, t *testing.T, e db.Executor, opts *MultiFactorOpts) *dbsqlc.MultiFactor {
    t.Helper()

    validateOpts(t, opts)

    var (
        num          = nextNumSeq()
        numFormatted = formatNumSeq(num)
    )

    multiFactor, err := dbsqlc.New().MultiFactorInsert(ctx, e, dbsqlc.MultiFactorInsertParams{
        ID:          ptrutil.ValOrDefaultFunc(opts.ID, func() uuid.UUID { return ptesting.ULID(ctx).New() }),
        AccountID:   opts.AccountID,
        ActivatedAt: ptrutil.TimeSQLNull(opts.ActivatedAt),
        ExpiresAt:   ptrutil.TimeSQLNull(opts.ExpiresAt),
        Kind:        string(ptrutil.ValOrDefault(opts.Kind, dbsqlc.MultiFactorKindTOTP)),
        Name:        fmt.Sprintf(&quot;%s no. %s&quot;, ptrutil.ValOrDefault(opts.Kind, dbsqlc.MultiFactorKindTOTP), numFormatted),
    })
    require.NoError(t, err)

    return multiFactor
}
</code></pre>

<p>The minimum viable use of the fixture needs only <code>AccountID</code>:</p>

<pre><code class="language-go">mf := dbfactory.MultiFactor(ctx, t, tx, &amp;dbfactory.MultiFactorOpts{
    AccountID: account.ID,
})
</code></pre>

<p>But all salient properties are settable, so a more elaborate use just involves sending more overrides:</p>

<pre><code class="language-go">expiredMF := dbfactory.MultiFactor(ctx, t, bundle.tx, &amp;dbfactory.MultiFactorOpts{
    AccountID: account.ID,
    ExpiresAt: ptrutil.Ptr(time.Now().Add(-5 * time.Minute)),
    Kind:      ptrutil.Ptr(dbsqlc.MultiFactorKindWebAuthn),
})
</code></pre>

<h2 id="observations" class="link"><a href="#observations">Observations</a></h2>

<p>A few aspects worth calling out:</p>

<ul>
<li><p>Under the principle of not mocking the database, fixtures are real live data records. They&rsquo;re queryable using the full expressiveness of SQL, are valid according to the schema&rsquo;s data types/checks/triggers, and satisfy foreign keys.</p></li>

<li><p>Fixtures never return an error, instead failing their input <code>t</code> so that generating a fixture is a one liner for the caller and doesn&rsquo;t need an <code>if err != nil { ... }</code> check.</p></li>

<li><p>Inputs are annotated with <a href="https://github.com/go-playground/validator">the Go validate framework</a> to demarcate required versus non-required or more complex validations as needed. This is a godsend because it keeps validations short (zero additional lines instead of a minimum of three for an <code>if</code> statement) and fast/easy to write.</p></li>

<li><p>As few properties are made <code>validate:&quot;required&quot;</code> as possible, with non nullable fields given defaults instead of marked mandatory for the caller to fill. This makes fixtures easier to use and reduces boilerplate at call sites. e.g. <code>name</code> is a required property on <code>multi_factor</code> above, but the fixture generates a sane default.</p></li>

<li><p>Insert statements are generated with <a href="/sqlc">sqlc</a>.</p></li>
</ul>

<pre><code class="language-sql">-- name: MultiFactorInsert :one
INSERT INTO multi_factor (
    id,
    account_id,
    activated_at,
    expires_at,
    kind,
    name
) VALUES (
    @id,
    @account_id,
    @activated_at,
    @expires_at,
    @kind,
    @name
) RETURNING *;
</code></pre>

<ul>
<li><p>We use of a lot of custom pointer helpers like <code>ptrutil.TimeSQLNull</code> (changes a pointer to a <code>sql.NullTime</code>) and <code>ptrutil.ValOrDefault</code>. Each one of these changes a ~4 line local variable declaration and <code>if</code> block to one LOC that it&rsquo;s inlined into the insert. True Go dogmatists won&rsquo;t like this, but it saves dozens of lines per test fixture, and given hundreds of test fixtures, this adds up to thousands of lines saved overall.</p></li>

<li><p>Each test case gets its own lazily marshaled monotonic ULID generated based on <code>t</code>. Separate generators guarantee monotonicity even if some test cases rewind their generators to generate ULIDs at particular times.</p></li>
</ul>

<h2 id="var-blocks" class="link"><a href="#var-blocks">Organizing with var blocks</a></h2>

<p>Typically, fixtures are generated together in a <code>var ( ... )</code> block, keeping tests looking nice and tidy:</p>

<pre><code class="language-go">t.Run(&quot;SetNameSSOJoinSCIMError&quot;, func(t *testing.T) {
    t.Parallel()

    bundle, ctx := setup(t)

    var (
        org  = dbfactory.Organization(ctx, t, bundle.tx, &amp;dbfactory.OrganizationOpts{SCIMEnabled: true})
        team = dbfactory.Team(ctx, t, bundle.tx, &amp;dbfactory.TeamOpts{OrganizationID: &amp;org.ID})
        _    = dbfactory.AccessGroupAccount_Admin(ctx, t, bundle.tx, team.ID, bundle.account.ID)
    )

    _, err := pservicetest.InvokeHandler(bundle.svc.Update, ctx, &amp;TeamUpdateRequest{
        Name:   ptrutil.Ptr(&quot;new name&quot;),
        TeamID: eid.EID(team.ID),
    })
    prequire.APIErrorWithMessage(t, &amp;apierror.BadRequestError{}, fmt.Sprintf(errMessageTeamUpdateSCIM, &quot;name&quot;), err)
})
</code></pre>

<h2 id="standardize-conventions" class="link"><a href="#standardize-conventions">Standardize conventions, even the small ones</a></h2>

<p>We have a few helpers that are used in almost every test fixture. These are so trivial that they almost don&rsquo;t need to be extracted into their own functions, but we&rsquo;ve done so to prevent implementations from drifting and keep code maximally succinct.</p>

<pre><code class="language-go">// Formats a number like &quot;000007&quot;. Typically used in conjunction
// with nextNumSeq to make identifiers prettier and so they align
// better.
func formatNumSeq(num int64) string {
    return fmt.Sprintf(&quot;%06d&quot;, num)
}

var numSeq int64

// Gets a unique number that can be used in names, etc. and which
// is more friendly to look at than a UUID.
func nextNumSeq() int64 {
    return atomic.AddInt64(&amp;numSeq, 1)
}

func validateOpts(t *testing.T, opts any) {
    t.Helper()

    err := validate.Struct(opts)
    require.NoError(t, err)
}
</code></pre>
]]></content>
    <published>2025-03-20T08:56:52-07:00</published>
    <updated>2025-03-20T08:56:52-07:00</updated>
    <link href="https://brandur.org/fragments/go-data-fixtures"></link>
    <id>tag:brandur.org,2025-03-20:fragments/go-data-fixtures</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Profiling production for memory overruns + canonical log stats</title>
    <summary>Using Go&amp;rsquo;s &lt;code&gt;runtime.MemStats&lt;/code&gt; and canonical log lines to isolate huge memory allocations to a specific endpoint.</summary>
    <content type="html"><![CDATA[<p>You&rsquo;re only lucky for so long. After four years of running our Go API in production with no memory trouble whatsoever, last week we started seeing instantaneous bursts of ~1.5 GB suddenly allocated, enough to cause Heroku to kill the dyno for being &ldquo;vastly over quota&rdquo; (our steady state memory use sits around ~50 MB, so we run on 512 MB dynos).</p>

<p>This was of course, concerning. We were only experiencing a few of these a day, but with no idea what was causing them, and having appeared very suddenly, we had to assume that they might get more frequent. Not only is the API suddenly being taken offline at any moment is a bad place to be UX-wise, and even with our careful use of transactions, makes resource leaks between components possible.</p>

<h2 id="alloc-delta" class="link"><a href="#alloc-delta">Alloc delta</a></h2>

<p>To localize the problem, I used Go&rsquo;s <a href="https://pkg.go.dev/runtime#MemStats"><code>runtime.MemStats</code></a> in conjunction with our <a href="/nanoglyphs/025-logs">canonical API lines</a>, making a new <code>total_alloc_delta</code> property available to see how many allocations took place during the period of an API request:</p>

<pre><code class="language-go">func (m *CanonicalAPILineMiddleware) Wrapper(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var (
            memStats      runtime.MemStats
            memStatsBegin = m.TimeNow()
        )
        runtime.ReadMemStats(&amp;memStats)
        var (
            memStatsBeginDuration = m.TimeNow().Sub(memStatsBegin)

            // TotalAlloc doesn't decrement on heap frees, so it gives
            // us useful info even if the GC runs during the request.
            totalAllocBegin = memStats.TotalAlloc
        )

        // API request served here
        next.ServeHTTP(w, r)
    
        // Middleware continues ...
        memStatsEnd := m.TimeNow()
        // Since we're only interested in one field, reuse the same
        // struct so we don't need to allocate a second one.
        runtime.ReadMemStats(&amp;memStats)
        var (
            memStatsEndDuration = m.TimeNow().Sub(memStatsEnd)
            totalAllocDelta     = memStats.TotalAlloc - totalAllocBegin
        )
        
        logData := &amp;CanonicalAPILineData{
            ID:                   m.ULID.New(),
            HTTPMethod:           r.Method,
            HTTPPath:             r.URL.Path,
            ...
            ReadMemStatsDuration: timeutil.PrettyDuration(memStatsBeginDuration + memStatsEndDuration),
            TotalAllocDelta:      totalAllocDelta,
        }

        plog.Logger(ctx).WithFields(structToFields(logData)).
            Infof(
                &quot;canonical_api_line %s %s -&gt; %v %s(%s)&quot;,
                r.Method,
                routeOrPath,
                logData.Status,
                idempotencyReplayStr,
                duration,
            )
</code></pre>

<p><code>MemStats</code> provides a large bucket of properties to pick from, but <code>TotalAlloc</code>&rsquo;s a useful one because it represents bytes allocated to the heap, but unlike similar stats like <code>HeapAlloc</code>, it&rsquo;s monotonically increasing. It&rsquo;s not decremented as objects are freed:</p>

<pre><code class="language-go">// TotalAlloc is cumulative bytes allocated for heap objects.
//
// TotalAlloc increases as heap objects are allocated, but
// unlike Alloc and HeapAlloc, it does not decrease when
// objects are freed.
TotalAlloc uint64
</code></pre>

<p>This is good because it means that all API requests will end up with the same memory heuristic, and made roughly comparable. Garbage collection may or may not occur during a request. Using <code>TotalAlloc</code> makes it irrelevant whether it did or not.</p>

<p>With that deployed, I can search logs for outliers (<code>:&gt;500000000</code> means greater than 5 MB):</p>

<pre><code class="language-txt">source:platform app:app[web] canonical_api_line (-http_route:/health-checks/{name})
    total_alloc_delta:&gt;500000000
</code></pre>

<p>And voila, we turn up the bad ones. Here, an API request that spiked memory a full 5 GB!</p>

<pre><code class="language-txt">Jan 29 10:18:33 platform app[web] info canonical_api_line
    POST /queries -&gt; 503 (2.53252138s)
total_alloc_delta=5008335944
</code></pre>

<h2 id="parallel-allocations" class="link"><a href="#parallel-allocations">Parallel allocations</a></h2>

<p>The use of <code>TotalAlloc</code> is imperfect because it not only tracks allocations of the current API request, but allocations across the current API request <em>and</em> all parallel requests.</p>

<p>We can see this effect through false positives:</p>

<pre><code class="language-txt">Feb 1 23:07:18 platform app[web] info canonical_api_line
    GET /clusters/{cluster_id}/databases -&gt; 504 (2m57.322010348s)
total_alloc_delta=743772480
</code></pre>

<p>It looks like this API request allocated 744 MB, but what actually happened is that it was a bad timeout that executed for a full three minutes <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup>. During that time, other API requests served in the interim allocated the majority of that memory. It <em>didn&rsquo;t</em> crash our 512 MB dyno because multiple GCs also occurred during that time.</p>

<h2 id="pprof-to-s3" class="link"><a href="#pprof-to-s3">Pprof to S3</a></h2>

<p>Getting our memory overruns localized to a particular endpoint was good, but even having done that, I&rsquo;d need a little more help to figure out where the rogue memory was going. To that end, I put in one more clause in the middleware so that in case of a huge overrun, the process dumps a <a href="https://github.com/google/pprof">pprof</a> heap profile to S3:</p>

<pre><code class="language-go">    ...

    // If we used a particularly huge amount of memory during the
    // request, upload a profile to S3 for analysis. Buckets have a
    // configured life cycle so objects will expire out after some
    // time.
    if err := m.maybeUploadPprof(ctx, logData.RequestID, totalAllocDelta); err != nil {
        plog.Logger(ctx).Errorf(m.Name+&quot;: Error uploading pprof profile: %s&quot;, err)
}

...

const pprofTotalAllocDeltaThreshold = 1_000_000_000

func (m *CanonicalAPILineMiddleware) maybeUploadPprof(ctx context.Context, requestID uuid.UUID, totalAllocDelta uint64) error {
    if !m.pprofEnable || totalAllocDelta &lt; m.pprofTotalAllocDeltaThreshold {
        return nil
    }

    profKey := fmt.Sprintf(&quot;%s/pprof/%s.prof&quot;, m.EnvName, requestID)

    var buf bytes.Buffer
    if err := pprof.WriteHeapProfile(&amp;buf); err != nil {
        return xerrors.Errorf(&quot;error writing heap profile: %w&quot;, err)
    }

    if _, err := m.aws.S3_PutObject(ctx, &amp;s3.PutObjectInput{
        Body:   &amp;buf,
        Bucket: ptrutil.Ptr(awsclient.S3Bucket),
        Key:    &amp;profKey,
    }); err != nil {
        return xerrors.Errorf(&quot;error putting heap profile to S3 at path %q: %w&quot;, profKey, err)
    }

    plog.Logger(ctx).Infof(m.Name+&quot;: pprof_profile_generated_line: TotalAlloc delta %d exceeded %d; generated pprof profile to S3 key %q&quot;,
        totalAllocDelta, m.pprofTotalAllocDeltaThreshold, profKey)

    return nil
}
</code></pre>

<p>Our memory problem ended up being a queries endpoint that was overly willing to read giant result sets into memory, then serialize the whole thing into a big JSON buffer for response, which was also pretty indented (and in Go&rsquo;s <code>encoding/json</code>, indenting a JSON response requires a <em>second</em> giant buffer 2x the size of the first one). I fixed it by reducing the maximum number of rows we were willing to read into the response.</p>

<p>I&rsquo;m not expecting to run into new memory overruns or leaks anytime soon, but I left the pprof code in place for the time being. It only does work in case of huge memory increases so there&rsquo;s no performance penalty most of the time, and it might come in handy again.</p>

<h2 id="stop-the-world" class="link"><a href="#stop-the-world">Stop the world</a></h2>

<p>A token glance at the implementation of <code>runtime.ReadMemStats</code> looks a little concerning:</p>

<pre><code class="language-go">// ReadMemStats populates m with memory allocator statistics.
//
// The returned memory allocator statistics are up to date as of the
// call to ReadMemStats. This is in contrast with a heap profile,
// which is a snapshot as of the most recently completed garbage
// collection cycle.
func ReadMemStats(m *MemStats) {
    _ = m.Alloc // nil check test before we switch stacks, see issue 61158
    stw := stopTheWorld(stwReadMemStats)

    systemstack(func() {
        readmemstats_m(m)
    })

    startTheWorld(stw)
}
</code></pre>

<p>To produce accurate stats, the runtime needs to &ldquo;stop the world&rdquo;, meaning that all active goroutines are paused, a sample taken, and resumed.</p>

<p>Intuitively, that seems like it could be pretty slow, and some initial googling seemed to confirm that. However, I later found a <a href="https://go-review.googlesource.com/c/go/+/34937">patch from 2017</a> that&rsquo;d improved the situation considerably by doing cumulative tracking of relevant stats so only a very brief stop the world was required. It indicated a reduction in timing down to 25µs, even at 100 concurrent goroutines.</p>

<p>I added a separate log stat to see how long my two <code>ReadStatMems</code> calls were taking, and found they were averaging ~100µs for both:</p>

<pre><code class="language-txt">read_mem_stats_duration=0.000098s
read_mem_stats_duration=0.000110s
read_mem_stats_duration=0.000113s
read_mem_stats_duration=0.000126s
read_mem_stats_duration=0.000123s
read_mem_stats_duration=0.000084s
read_mem_stats_duration=0.000091s
read_mem_stats_duration=0.000092s
read_mem_stats_duration=0.000090s
read_mem_stats_duration=0.000083s
</code></pre>

<p>That&rsquo;s 50µs per invocation instead of 25µs, but given that a single DB query takes an order or two of magnitude longer at 1-10ms, a little delay to get memory stats is acceptable. If our stack was hyper performance sensitive or saturated with huge request volume, I&rsquo;d take it out.</p>


]]></content>
    <published>2025-02-02T18:02:27-07:00</published>
    <updated>2025-02-02T18:02:27-07:00</updated>
    <link href="https://brandur.org/fragments/profiling-production"></link>
    <id>tag:brandur.org,2025-02-02:fragments/profiling-production</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Go&#39;s bytes.Buffer vs. strings.Builder</title>
    <summary>Taking five minutes to write a benchmark so I know which of these I should be reaching for first.</summary>
    <content type="html"><![CDATA[<p>I was writing some Go code today that generated other Go code. Writing it line by line, mostly in a loop, but with pre- and post-matter.</p>

<p>My usual go to for this type of thing is <a href="https://pkg.go.dev/bytes#Buffer"><code>bytes.Buffer</code></a>, but after I&rsquo;d finished the implementation, given that I was working entirely with strings, I started to wonder if I should&rsquo;ve used <a href="https://pkg.go.dev/strings#Builder"><code>strings.Builder</code></a> instead.</p>

<p>I realized that I had no idea whether one was faster than the other, so I wrote a quick benchmark to check:</p>

<pre><code class="language-go">package main

import (
    &quot;bytes&quot;
    &quot;strings&quot;
    &quot;testing&quot;
)

var fragments = []string{
    &quot;This&quot;,
    &quot;is a series of&quot;,
    &quot;string fragments&quot;,
    &quot;that will be concatenated together&quot;,
    &quot;into a single larger string&quot;,
    &quot;so that we can&quot;,
    &quot;determine which of Go's various&quot;,
    &quot;tools for doing this&quot;,
    &quot;is most efficient.&quot;,
    &quot;I found a few articles&quot;,
    &quot;online&quot;,
    &quot;but most were poorly cited&quot;,
    &quot;or&quot;,
    &quot;behind a Medium login wall&quot;,
    &quot;or otherwise&quot;,
    &quot;not of admirable quality.&quot;,
}

func BenchmarkBytesBuffer(b *testing.B) {
    for range b.N {
        var buf bytes.Buffer

        for _, fragment := range fragments {
            buf.WriteString(fragment)
            buf.WriteString(&quot; &quot;)
        }

        _ = buf.String()
    }
}

func BenchmarkConcatenateStrings(b *testing.B) {
    for range b.N {
        var str string

        for _, fragment := range fragments {
            str += fragment
            str += &quot; &quot;
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for range b.N {
        var sb strings.Builder

        for _, fragment := range fragments {
            sb.WriteString(fragment)
            sb.WriteString(&quot; &quot;)
        }

        _ = sb.String()
    }
}
</code></pre>

<pre><code class="language-sh">$ go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: github.com/brandur/go-builder-vs-buffer
cpu: Apple M4
BenchmarkBytesBuffer-10           5013081    217.3 ns/op    1280 B/op    5 allocs/op
BenchmarkConcatenateStrings-10    1603748    753.5 ns/op    5557 B/op    31 allocs/op
BenchmarkStringBuilder-10         6916813    146.9 ns/op    752 B/op     6 allocs/op
PASS
ok      github.com/brandur/go-builder-vs-buffer 4.724s

</code></pre>

<p>So there you have it. At least when it comes to concatenating only strings at relatively modest sizes, <code>strings.Builder</code> is about 33% faster, and 80% faster than <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup> than concatenating strings. Given that the DX is identical between the two, I&rsquo;ll make it my new default go to.</p>


]]></content>
    <published>2025-01-02T22:34:32-07:00</published>
    <updated>2025-01-02T22:34:32-07:00</updated>
    <link href="https://brandur.org/fragments/bytes-buffer-vs-strings-builder"></link>
    <id>tag:brandur.org,2025-01-02:fragments/bytes-buffer-vs-strings-builder</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Postgres UUIDv7 + per-backend monotonicity</title>
    <summary>How Postgres&amp;rsquo; v7 UUIDs are made monotonic, and why that&amp;rsquo;s a great feature.</summary>
    <content type="html"><![CDATA[<p>An implementation for <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=78c5e141e9c139fc2ff36a220334e4aa25e1b0eb">UUIDv7 was committed to Postgres</a> earlier this month. These have all the benefits of a v4 (random) UUID, but are generated with a more deterministic order using the current time, and perform considerably better on inserts using ordered structures like B-trees.</p>

<p>A nice surprise is that the random portion of the UUIDs will be monotonic within each Postgres backend:</p>

<blockquote>
<p>In our implementation, the 12-bit sub-millisecond timestamp fraction
is stored immediately after the timestamp, in the space referred to as
&ldquo;rand_a&rdquo; in the RFC. This ensures additional monotonicity within a
millisecond. The rand_a bits also function as a counter. We select a
sub-millisecond timestamp so that it monotonically increases for
generated UUIDs within the same backend, even when the system clock
goes backward or when generating UUIDs at very high
frequency. Therefore, the monotonicity of generated UUIDs is ensured
within the same backend.</p>
</blockquote>

<p>This is a hugely valuable feature in practice, especially in testing. Say you want to generate five objects for testing an API list endpoint. It&rsquo;s possible they&rsquo;re generated in-order by virtue of being across different milliseconds or by getting lucky, but probability is against you, and the likelihood is that some will be out of order. A test case has to generate the five objects, then do an initial sort before making use of them. That&rsquo;s not the end of the world, but it&rsquo;s more test code and adds noise.</p>

<pre><code class="language-ruby">test_accounts = 5.times.map { TestFactory.account }

# maybe IDs were in order, but maybe not, so do an initial sort
test_accounts.sort_by! { |a| a.id }

# API endpoint will return accounts ordered by ID
resp = make_api_request :get, &quot;/accounts&quot;
expect(resp.map { _1[&quot;id&quot;] }).to eq(test_accounts.map(&amp;:id))
</code></pre>

<p>With Postgres ensuring monotonicity for UUIDv7s, the five generated objects get five in-order IDs, making the test safer <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup> and faster to write. Montonicity isn&rsquo;t guaranteed across backends, but that&rsquo;s okay in well written test suites. Patterns like <a href="/fragments/go-test-tx-using-t-cleanup">test transactions</a> will guarantee that each test case speaks to exactly one backend.</p>

<h2 id="12-bits-more-clock" class="link"><a href="#12-bits-more-clock">12 bits more clock</a></h2>

<p>My grasp on monotonicity has always been tenuous at best, so I was curious how it was implemented here. I looked at the patch, and its approach was more obvious than I expected:</p>

<pre><code class="language-c">/*
 * Generate UUID version 7 per RFC 9562, with the given timestamp.
 *
 * UUID version 7 consists of a Unix timestamp in milliseconds (48
 * bits) and 74 random bits, excluding the required version and
 * variant bits. To ensure monotonicity in scenarios of high-
 * frequency UUID generation, we employ the method &quot;Replace
 * LeftmostRandom Bits with Increased Clock Precision (Method 3)&quot;,
 * described in the RFC. This method utilizes 12 bits from the
 * &quot;rand_a&quot; bits to store a 1/4096 (or 2^12) fraction of sub-
 * millisecond precision.
 *
 * ns is a number of nanoseconds since start of the UNIX epoch.
 * This value is used for time-dependent bits of UUID.
 */
static pg_uuid_t* generate_uuidv7(int64 ns) {

...

/*
 * sub-millisecond timestamp fraction (SUBMS_BITS bits, not
 * SUBMS_MINIMAL_STEP_BITS)
 */
increased_clock_precision = ((ns % NS_PER_MS) * (1 &lt;&lt; SUBMS_BITS)) / NS_PER_MS;

/* Fill the increased clock precision to &quot;rand_a&quot; bits */
uuid-&gt;data[6] = (unsigned char) (increased_clock_precision &gt;&gt; 8);
uuid-&gt;data[7] = (unsigned char) (increased_clock_precision);

/* fill everything after the increased clock precision with random bytes */
if (!pg_strong_random(&amp;uuid-&gt;data[8], UUID_LEN - 8))
    ereport(ERROR,
            (errcode(ERRCODE_INTERNAL_ERROR),
            errmsg(&quot;could not generate random values&quot;)));
</code></pre>

<p>UUIDv7 dictates an initial 48 bits that encodes a timestamp down to millisecond precision. A millisecond is a short amount of time for a human, but quite long for a computer, and many UUIDs could easily be generated with the space of a single ms.</p>

<pre><code> 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      48 bits unix_ts_ms                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   48 bits unix_ts_ms (cont)   |  ver  |    12 bits rand_a     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                    62 bits rand_b                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     62 bits rand_b (cont)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>

<p>The Postgres patch solves the problem by repurposing 12 bits of the UUID&rsquo;s random component to increase the precision of the timestamp down to nanosecond granularity (filling <code>rand_a</code> above), which in practice is too precise to contain two UUIDv7s generated in the same process. It makes a repeated UUID <em>between</em> processes more likely, but there&rsquo;s still 62 bits of randomness left to make use of, so collisions remain vastly unlikely.</p>

<h2 id="wait" class="link"><a href="#wait">The wait is on</a></h2>

<p>UUIDv7s are going to make a great core addition to Postgres, and I can&rsquo;t wait to start using them. Quite unfortunately, their commit was delayed past the freeze for Postgres 17, so they won&rsquo;t make it into an official version until Postgres 18 is cut in late 2025. So now, we wait.</p>


]]></content>
    <published>2024-12-31T15:32:43-07:00</published>
    <updated>2024-12-31T15:32:43-07:00</updated>
    <link href="https://brandur.org/fragments/uuid-v7-monotonicity"></link>
    <id>tag:brandur.org,2024-12-31:fragments/uuid-v7-monotonicity</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Stripe V2</title>
    <summary>Stripe&amp;rsquo;s API got a new major version, and no one noticed.</summary>
    <content type="html"><![CDATA[<p>I happened to notice by way of a Slack bot today that Stripe released a <a href="https://docs.stripe.com/api-v2-overview">V2 version of their API</a>. I thought this must&rsquo;ve been a soft launch right before the holidays, surely to be followed up by a more formal blog post, but the Way Back Machine clocked the page in <a href="https://web.archive.org/web/20241004013621/https://docs.stripe.com/api-v2-overview">early October</a>, making it three months old. It&rsquo;s been there all along, I just hadn&rsquo;t seen it before.</p>

<p>The V1 and V2 APIs are separate namespaces and what&rsquo;s available in V2 is currently very minimal (only events and event destinations), so integrations will still use V1 for almost everything, but the overview page tells us about its aspirational design intentions.</p>

<h2 id="json-hateoas" class="link"><a href="#json-hateoas">JSON, with a sprinkling of HATEOAS</a></h2>

<p>A few highlights:</p>

<ul>
<li><p>By far the best and biggest change is that request bodies are sent as JSON instead of <code>application/x-www-form-urlencoded</code>. Form encoding isn&rsquo;t the worst thing in the world, but it falls flat on its face when encoding complex data types like arrays and maps (or worse, <em>nested</em> arrays and maps). It&rsquo;s also just weird and out of place in 2024. This change should&rsquo;ve happened ten years ago.</p></li>

<li><p>Pagination has picked up a hypermedia-esque veneer (see <a href="https://en.wikipedia.org/wiki/HATEOAS">HATEAOS</a>), returning a <code>next_page_url</code> that&rsquo;s requested directly instead of a cursor and having the caller build the next URL themselves.</p></li>

<li><p>The new API is trying to move away from a model where sub-objects in an API resource are expanded by default, to one where they need to be requested with an <code>include</code> parameter. We had plenty of discussions about this before I left. The purpose of the change is to make API requests faster (Stripe&rsquo;s API is quite slow) by rendering less for most requests. I counted only two places where this is actually used so far though, so time will tell whether the gambit actually succeeds or not.</p></li>

<li><p>Endpoints will try for &ldquo;real&rdquo; idempotency where callers can converge failed operations to either success or definitive failure by calling them again:</p>

<blockquote>
<ul>
<li>When you provide the same idempotency key for two requests:

<ul>
<li>API v1 always returns the previously-saved response of the first API request, even if it was an error.</li>
<li>API v2 attempts to retry any failed requests without producing side effects (any extraneous change or observable behavior that occurs as a result of an API call) and provide an updated response.</li>
</ul></li>
</ul>
</blockquote>

<p>Previously (and still for most endpoints), failures from an intermittent blip or bug were a big problem. The idempotency layer dumbly returned whatever canned response had been recorded on the initial go around (including internal server errors), so users wouldn&rsquo;t get closure on what exactly happened. Their best hope would that be a Stripe engineer would eventually repair their charge manually at some later time, and send a webhook about it.</p></li>
</ul>

<h2 id="rest-ish" class="link"><a href="#rest-ish">REST-ish v4-ever</a></h2>

<p>Lots of positive progress there, but a new API version also presents an opportunity to clear out blemishes, and I expected to see more of that. A few points that are less good:</p>

<ul>
<li><p>I was hoping they&rsquo;d fix their verbs to play more nicely with modern REST conventions. Instead of using <code>POST</code> everywhere, use <code>POST</code> for endpoints that are knowingly not idempotent (without an idempotency key), <code>PUT</code> for mutation endpoints that are, and <code>PATCH</code> for mutation endpoints that aren&rsquo;t. I admit it&rsquo;s pedantic, but it&rsquo;s so absolutely trivial to implement, and the use of a good verb signals more information than a reader would otherwise have with a cursory glance at API structure.</p></li>

<li><p>They&rsquo;re still doing the RPC-style calls like:</p>

<pre><code>POST /v2/core/event_destinations/:id/enable
</code></pre>
<p>Also pedantic, but <code>enable</code> here should theoretically be reserved for a nested resource. I think it&rsquo;s cleaner to model actions as IDs under a shared &ldquo;actions&rdquo; subresource:</p>

<pre><code>POST /v2/core/event_destinations/:id/actions/enable
</code></pre></li>
</ul>

<h2 id="nouveau-dx" class="link"><a href="#nouveau-dx">Nouveau DX</a></h2>

<p>Frankly, I was a bit shocked by how little attention this got. There was a time not too long ago when Stripe cutting a new API version would&rsquo;ve been a major event in the tech world, but in three months I didn&rsquo;t come across a single person who mentioned it.</p>

<p>A major part of this is that Stripe is no longer a great technical leader in the same sense that it used to be. But also, as <a href="https://x.com/tweetsbycolin/status/1873241754784411656">Colin points out</a>:</p>

<blockquote>
<p>This is an undeniable sign that &ldquo;a great REST API&rdquo; is no longer the benchmark for great DX</p>
</blockquote>

<p>That&rsquo;s got to be true too. Few of us want to be making manual HTTP calls out to APIs anymore. These days a great SDK, not a great API, is a hallmark, and maybe even a necessity, of a world class development experience.</p>
]]></content>
    <published>2024-12-28T23:56:24-07:00</published>
    <updated>2024-12-28T23:56:24-07:00</updated>
    <link href="https://brandur.org/fragments/stripe-v2"></link>
    <id>tag:brandur.org,2024-12-28:fragments/stripe-v2</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Go&#39;s maximum time.Duration</title>
    <summary>Avoiding overflows with Go&amp;rsquo;s &lt;code&gt;time.Duration&lt;/code&gt; in the presence of exponential algorithms.</summary>
    <content type="html"><![CDATA[<p>While working on a River bug related to retry policy, I came across a case where it was actually plausible to overflow Go&rsquo;s built-in <code>time.Duration</code> and wrap back around to negative number.</p>

<p>A duration has a much simpler representation than a timestamp. It&rsquo;s an <code>int64</code> counted in nanoseconds:</p>

<pre><code class="language-go">// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64
</code></pre>

<p>As the comment states, the maximum duration is about 290 years. More precisely, 292 (non-leap) years, 171 days, and 23 hours:</p>

<pre><code class="language-go">func main() {
    const (
        maxDuration time.Duration = 1&lt;&lt;63 - 1

        day  = 24 * time.Hour
        year = 365 * day
    )

    var (
        years        = maxDuration / year
        withoutYears = maxDuration % year

        days        = withoutYears / day
        withoutDays = withoutYears % day
    )

    fmt.Printf(&quot;max duration: %dy%dd%s\n&quot;, years, days, withoutDays)
}
</code></pre>

<pre><code class="language-sh">$ go run main.go
max duration: 292y171d23h47m16.854775807s
</code></pre>

<p>292 years is a long time, and it&rsquo;s not likely most programs will need more than that, but our retry algorithm is exponential, and crosses that threshold after 310 retries.</p>

<h2 id="compile-v-runtime-overflow" class="link"><a href="#compile-v-runtime-overflow">Compile v. runtime overflow</a></h2>

<p>When performing a direct calculation on a constant, the compiler will detect the overflow:</p>

<pre><code class="language-go">func main() {
    const maxDuration time.Duration = 1&lt;&lt;63 - 1
    var maxDurationSeconds = float64(maxDuration / time.Second)

    notOverflowed := time.Duration(maxDurationSeconds) * time.Second
    fmt.Printf(&quot;not overflowed: %+v\n&quot;, notOverflowed)

    overflowed := time.Duration(int64(maxDuration)+1) * time.Second
    fmt.Printf(&quot;overflowed: %+v\n&quot;, overflowed)
}
</code></pre>

<pre><code class="language-sh">$ go run main.go
./main.go:15:30: int64(maxDuration) + 1 (constant 9223372036854775808 of type int64) overflows int64
</code></pre>

<p>But performing the same operation on a variable will happily wrap around:</p>

<pre><code class="language-go">overflowed := time.Duration(maxDurationSeconds+1) * time.Second
fmt.Printf(&quot;overflowed: %+v\n&quot;, overflowed)
</code></pre>

<pre><code class="language-sh">$ go run main.go
not overflowed: 2562047h47m16s
overflowed: -2562047h47m16.709551616s
</code></pre>

<h2 id="well-defined" class="link"><a href="#well-defined">Little practical use, but well defined</a></h2>

<p>I <a href="https://github.com/riverqueue/river/pull/698">fixed River&rsquo;s back offs at large attempt counts</a> by using Go 1.21&rsquo;s <code>min</code> function combined with the maximum known number of seconds that&rsquo;ll fit in a <code>time.Duration</code>:</p>

<pre><code class="language-go">// The maximum value of a duration before it overflows. About 292 years.
const maxDuration time.Duration = 1&lt;&lt;63 - 1

// Same as the above, but changed to a float represented in seconds.
var maxDurationSeconds = maxDuration.Seconds()

func (p *DefaultClientRetryPolicy) NextRetry(job *rivertype.JobRow) time.Time {
    return time.Now().Add(timeutil.SecondsAsDuration(
        p.retrySeconds(len(job.Errors) + 1),
    ))
}

func (p *DefaultClientRetryPolicy) retrySeconds(attempt int) float64 {
    retrySeconds := math.Pow(float64(attempt), 4)
    return min(retrySeconds, maxDurationSeconds)
}
</code></pre>

<p>After hitting retry attempt 310, the algorithm backs off 292 years at a time. This behavior will never be of any real use to anybody, but I changed it to be <em>well defined</em> behavior of no real use to anybody, with no risk of odd bugs that might otherwise result from an overflow.</p>
]]></content>
    <published>2024-12-21T10:30:01-07:00</published>
    <updated>2024-12-21T10:30:01-07:00</updated>
    <link href="https://brandur.org/fragments/go-max-time-duration"></link>
    <id>tag:brandur.org,2024-12-21:fragments/go-max-time-duration</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>ERROR: invalid byte sequence for encoding UTF8: 0x00 (and what to do about it)</title>
    <summary>Handling a common programming language/database asymmetry around tolerance of zero bytes.</summary>
    <content type="html"><![CDATA[<p>One of the oldest errors I ever remember seeing in an error tracker:</p>

<blockquote>
<p>ERROR: invalid byte sequence for encoding &ldquo;UTF8&rdquo;: <code>0x00</code></p>
</blockquote>

<p>Through my time at Heroku it was like a distant friend. Not one that you&rsquo;d see every day, but one who&rsquo;d appear to be surprise you a few dozen times a year. Since it didn&rsquo;t seem to be causing any major fallout and I never heard a user complain about it, I&rsquo;m somewhat embarrassed to say that in four years neither myself nor anyone else ever bothered to look into it.</p>

<p>These days, on a Go stack and with much better control and insight into any changes we make, we&rsquo;re pretty aggressive about trying to prune Sentry errors down to zero. Over a few months I&rsquo;d see the <code>0x00</code> error come and go, and finally decided to look into it.</p>

<p>The problem comes from Postgres raising an error when a caller tries to insert a text/varchar value containing a value of <code>0x00</code>, or zero byte. The same value that&rsquo;s used to terminate a string in plain old C. Postgres <a href="https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE">explicitly disallows it</a>:</p>

<blockquote>
<p>The character with the code zero cannot be in a string constant.</p>
</blockquote>

<p>The tricky part is that although Postgres won&rsquo;t take a zero byte, almost every programming language ever created <em>will</em>, thereby creating a natural asymmetry between database and language stack.</p>

<p>As far as I know, there aren&rsquo;t any legitimate uses for sending a zero byte to an API or web app. Looking back through our logs, the main places I&rsquo;ve seen it are from bots out on the internet, presumably using common attack patterns to probe for weaknesses, or from pentest teams that we paid to do the same.</p>

<h2 id="edges" class="link"><a href="#edges">Validating at the edges</a></h2>

<p>We&rsquo;re using the <a href="https://github.com/go-playground/validator">validate framework for Go</a> to check that API inputs are sound, like that they&rsquo;re present, below a max length, or within bounds. In a language known for its verbosity, validate annotations are succinct and quick to write.</p>

<p>The custom validations <code>apistring200</code>, <code>apistrong2000</code>, <code>apistring20000</code>, etc. are assigned to API string parameters in <a href="/text#varchars">order of magnitude tiers</a>. Their implementation denies <code>\x00</code>s that come in with request payloads:</p>

<pre><code class="language-go">// API strings are meant to provide a reasonable default validation
// for strings that come in via the API that aren't already
// validated more strictly. The main idea is to make sure that
// we're not getting long, unbounded input that'll either store a
// very invalid value to the database or be rejected by a DB-level
// constraint (which would bubble up as a 500 with little context).
//
// They also validate that strings contain no invalid unicode
// sequences, and that no `\x00` zero bytes are present, both of
// which Postgres will reject.
must(registerAPIString(&quot;apistring200&quot;, 200))
must(registerAPIString(&quot;apistring2000&quot;, 2_000))
must(registerAPIString(&quot;apistring20000&quot;, 20_000))
must(registerAPIString(&quot;apistring200000&quot;, 200_000))

const (
    apiStringErrorMessage = &quot;`{0}` should be a non-empty string with a maximum length of %d characters, and contain no invalid unicode sequences or zero bytes&quot;
)

func registerAPIString(tag string, maxLength int) error {
    if err := validate.RegisterValidation(tag, func(fl validator.FieldLevel) bool {
        val := fl.Field().String()

        if len(val) == 0 || len(val) &gt; maxLength {
            return false
        }

        if !utf8.ValidString(val) {
            return false
        }

        // A zero (0x00) rune is valid UTF-8 and won't be caught
        // by the unicode check above, but Postgres will refuse
        // to insert it.
        if strings.Contains(val, &quot;\x00&quot;) {
            return false
        }

        return true
    }); err != nil {
        return err
    }

    return registerTranslation(tag, fmt.Sprintf(apiStringErrorMessage, maxLength))
}
</code></pre>

<p>Notably, it also denies invalid UTF-8 byte sequences (<code>\x00</code> is not desirable, but it is valid UTF-8), another common malformed input that internet bots like to send, and which will cause its own Postgres error.</p>

<p>Struct fields are tagged with validations, making use easy and concise:</p>

<pre><code class="language-go">// Request for creating a new account.
type AccountCreateRequest struct {
    // Full name for the new account.
    Name *string `json:&quot;name&quot; validate:&quot;apistring200&quot;`
    
    ...
</code></pre>

<h2 id="raw-request-properties" class="link"><a href="#raw-request-properties">Storing raw request properties</a></h2>

<p>That takes care of input forms, but another place we&rsquo;d see the problem is when trying to insert <a href="/canonical-log-lines">canonical API lines</a> to the database for operational visibility. Even where we denied a request with invalid input with a 400, we record a canonical line for it, invalid input and all.</p>

<p>For this case, we take anything invalid in the input and replace it with a placeholder token that&rsquo;s safely storable to Postgres:</p>

<pre><code class="language-go">// TrimInvalidUTF8 replaces any invalid UTF-8 or \x00 bytes with
// symbolic stand-in tokens. This lets strings that contain invalid
// UTF-8 be stored to Postgres, which normally won't tolerate
// invalid UTF-8 in string-like fields.
func TrimInvalidUTF8(s string) string {
    if !utf8.ValidString(s) {
        s = strings.ToValidUTF8(s, &quot;[invalid UTF-8]&quot;)
    }

    // A zero (0x00) rune is valid UTF-8 and won't be caught by the
    // check above, but Postgres will refuse to insert it. Replace
    // all instances with a marker that Postgres can tolerate and
    // which is indicative of what happened. This should only ever
    // happen because of random probing from malicious internet
    // actors sending garbage into HTTP paths and what not.
    if strings.Contains(s, &quot;\x00&quot;) {
        s = strings.ReplaceAll(s, &quot;\x00&quot;, &quot;[0x00 UTF-8 rune]&quot;)
    }

    return s
}
</code></pre>

<p>This is combined with another helper to that samples inputs longer than we&rsquo;re willing to store:</p>

<pre><code class="language-go">// Returns a string that's been truncated the given max length and
// stripped of any invalid UTF-8 that Postgres might balk at.
// Returns an empty string on `nil` for purposes of the batch
// insert will treat empty strings as NULL.
validTruncatedStringOrEmpty := func(sPtr *string, maxLength int) string {
    if sPtr == nil {
        return &quot;&quot;
    }

    return stringutil.SampleLongN(stringutil.TrimInvalidUTF8(*sPtr), maxLength)
}
</code></pre>

<p>When inserting a canonical line for a request, inputs are sanitized and truncated. This happens for obvious fields where an invalid input can be sent like a query string or form body, but for less obvious ones as well. Invalid input can come in almost anywhere, including headers like <code>Content-Type</code> or <code>User-Agent</code>:</p>

<pre><code class="language-go">insertParams.ContentType[i] =
    validTruncatedStringOrEmpty(logData.ContentType, 200)
insertParams.HTTPPath[i] =
    validTruncatedStringOrEmpty(&amp;logData.HTTPPath, 200)
insertParams.QueryString[i] =
    validTruncatedStringOrEmpty(logData.QueryString, 2000)
insertParams.UserAgent[i] =
    validTruncatedStringOrEmpty(logData.UserAgent, 200)
</code></pre>

<h2 id="one-down" class="link"><a href="#one-down">0x01 down</a></h2>

<p>This is one of those little housekeeping tasks that may not be that important, but is quite gratifying. With the steps above we&rsquo;ve eradicated &ldquo;invalid byte sequence&rdquo; errors, taking us a step closer to our target steady state of zero Sentry issues.</p>
]]></content>
    <published>2024-12-19T14:58:05-07:00</published>
    <updated>2024-12-19T14:58:05-07:00</updated>
    <link href="https://brandur.org/fragments/invalid-byte-sequence"></link>
    <id>tag:brandur.org,2024-12-19:fragments/invalid-byte-sequence</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>The parallel test bundle, a convention for Go testing</title>
    <summary>A Go convention that we&amp;rsquo;ve found effective for making subtests parallel-safe, keeping them DRY, and keeping code readable.</summary>
    <content type="html"><![CDATA[<p>A year ago we went through of process of getting every test case in our project tagged with <a href="/t-parallel"><code>t.Parallel</code> and ratcheted with <code>paralleltest</code></a>. I was initially skeptical about this being worth the effort because testing across Go packages was already happening in parallel, but it turned out to be a major boon for running large packages individually where we reduced test time by 30%+. We did one more step from there to tag every <em>subtest</em> with <code>t.Parallel</code> too. The gains from that weren&rsquo;t as big, but it helps when running tests with many subtests one off, and isn&rsquo;t much effort to sustain now that it&rsquo;s in place.</p>

<p>We&rsquo;re running close to 5,000 tests at this point. Large scale code refactoring tools aren&rsquo;t widespread in Go, so I did most of the refactoring with some <em>very</em> gnarly multi-line regexes, and even with those, the only reason that it was possible was that we&rsquo;re obsessive with keeping strong code convention. Most test cases were structured with an identical layout, which might&rsquo;ve seemed like unnecessary pedantry when it was first going in, but later paid off in reams as I refactored thousands of tests in hours instead of weeks.</p>

<p>Let me showcase a test convention that we&rsquo;ve found to be useful for making subtests parallel-safe, keeping them DRY (unlike many languages, Go doesn&rsquo;t have built-in facilities for setup/teardown blocks in tests), and keeping code readable. I try to be honest in the assessment of programming conventions and am not always certain about new ones, but we&rsquo;ve been using the parallel test bundle for months and I&rsquo;d rate it a <sup>10</sup>&frasl;<sub>10</sub> strong recommendation. Better yet, it&rsquo;s all just plain Go code and doesn&rsquo;t require the adoption of anything weird/novel.</p>

<h2 id="bundle-struct" class="link"><a href="#bundle-struct">The test bundle struct</a></h2>

<p>The test bundle itself is simple struct containing the object under test and useful fixtures to have available across subtests:</p>

<pre><code class="language-go">type testBundle struct {
    account *dbsqlc.Account
    svc     *playgroundTutorialService
    team    *dbsqlc.Team
    tx      db.Tx
}
</code></pre>

<h2 id="setup-function" class="link"><a href="#setup-function">The setup function</a></h2>

<p>It&rsquo;s paired with a <code>setup</code> helper function that returns a bundle:</p>

<pre><code class="language-go">setup := func(t *testing.T) (*testBundle, context.Context) {
    t.Helper()

    // These two vars are standard across almost every test case.
    var (
        ctx = ptesting.Context(t)
        tx  = ptesting.TestTx(ctx, t)
    )

    // Group of data fixtures.
    var (
        team    = dbfactory.Team(ctx, t, tx, &amp;dbfactory.TeamOpts{})
        account = dbfactory.Account(ctx, t, tx, &amp;dbfactory.AccountOpts{})
        _       = dbfactory.AccessGroupAccount_Admin(ctx, t, tx, team.ID, account.ID)
    )
    ctx = authntest.Account(account).Context(ctx)

    return &amp;testBundle{
        account: account,
        svc:     pservicetest.InitAndStart(ctx, t, NewPlaygroundTutorialService(), tx.Begin, nil),
        team:    team,
        tx:      tx,
    }, ctx
}
</code></pre>

<p>Along with a test bundle, the function also returns a context <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup>, which is useful for seeding context with a context logger that makes sure all <a href="/t-parallel#logging">logging output is collated with the test</a> being run instead of <code>stdout</code> where its output would be interleaved with that of other tests running parallel. Tests that don&rsquo;t need a context omit the second return value.</p>

<h2 id="subtests" class="link"><a href="#subtests">Subtest invocations</a></h2>

<p>Each subtest marks itself as parallel, and calls <code>setup</code> to procure a test bundle:</p>

<pre><code class="language-go">t.Run(&quot;AllProperties&quot;, func(t *testing.T) {
    t.Parallel()

    bundle, ctx := setup(t)
    
    ...
</code></pre>

<p>Each instance of a test bundle is fully insulated from every other instance, ensuring that no side effects from a test can leak into any other. Every test case uses a test transaction so that it&rsquo;s got its own private snapshot into the database for purposes of raising fixtures or querying.</p>

<p>We tend to put test bundles in every test case, even where the bundle contains only a single field. This is a courtesy to a future developer who might need to augment the test and where a preexisting test bundle makes that faster to do. It also keeps convention strong in case we need to do another broad refactor down the line.</p>

<h2 id="complete-example" class="link"><a href="#complete-example">Complete example</a></h2>

<p>Here&rsquo;s a full code sample with all the steps together:</p>

<pre><code class="language-go">func TestPlaygroundTutorialServiceCreate(t *testing.T) {
   t.Parallel()

   type testBundle struct {
      account *dbsqlc.Account
      svc     *playgroundTutorialService
      team    *dbsqlc.Team
      tx      db.Txer
   }

   setup := func(t *testing.T) (*testBundle, context.Context) {
      t.Helper()

      var (
         ctx = ptesting.Context(t)
         tx  = ptesting.TestTx(ctx, t)
      )

      var (
         team    = dbfactory.Team(ctx, t, tx, &amp;dbfactory.TeamOpts{})
         account = dbfactory.Account(ctx, t, tx, &amp;dbfactory.AccountOpts{})
         _       = dbfactory.AccessGroupAccount_Admin(ctx, t, tx, team.ID, account.ID)
      )
      ctx = authntest.Account(account).Context(ctx)

      return &amp;testBundle{
         account: account,
         svc:     pservicetest.InitAndStart(ctx, t, NewPlaygroundTutorialService(), tx.Begin, nil),
         team:    team,
         tx:      tx,
      }, ctx
   }

   t.Run(&quot;AllProperties&quot;, func(t *testing.T) {
      t.Parallel()

      bundle, ctx := setup(t)

      resp, err := pservicetest.InvokeHandler(bundle.svc.Create, ctx, &amp;PlaygroundTutorialCreateRequest{
         BootstrapSQL: ptrutil.Ptr(`SELECT unnest(array[1,2,3]);`),
         Name:         &quot;My playground tutorial&quot;,
         Content:      &quot;# My tutorial\n\nThis is my SQL tutorial, created by **me**.&quot;,
         IsPinned:     true,
         IsPublic:     true,
         TeamID:       eid.EID(bundle.team.ID),
         Weight:       ptrutil.Ptr(int32(100)),
      })
      require.NoError(t, err)
      prequire.PartialEqual(t, &amp;apiresourcekind.PlaygroundTutorial{
         BootstrapSQL: ptrutil.Ptr(`SELECT unnest(array[1,2,3]);`),
         Content:      &quot;# My tutorial\n\nThis is my SQL tutorial, created by **me**.&quot;,
         IsPinned:     true,
         IsPublic:     true,
         Name:         &quot;My playground tutorial&quot;,
         TeamID:       eid.EID(bundle.team.ID),
         Weight:       ptrutil.Ptr(int32(100)),
      }, resp)

      _, err = dbsqlc.New().PlaygroundTutorialGetByID(ctx, bundle.tx, uuid.UUID(resp.ID))
      require.NoError(t, err)

      prequire.EventForActor(ctx, t, bundle.tx, &quot;playground_tutorial.created&quot;, bundle.account.ID)
   })
}
</code></pre>

<p>See also the <a href="/fragments/partial-equal"><code>PartialEqual</code> helper</a> which I wasn&rsquo;t completely sure about when I first put it in, but am now fully bought into now because it&rsquo;s shown itself to be so effective at keeping many consecutive assertions very tidy.</p>


]]></content>
    <published>2024-10-27T16:06:21-07:00</published>
    <updated>2024-10-27T16:06:21-07:00</updated>
    <link href="https://brandur.org/fragments/parallel-test-bundle"></link>
    <id>tag:brandur.org,2024-10-27:fragments/parallel-test-bundle</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Rails World 2024</title>
    <summary>A few reflections on this year&amp;rsquo;s event. (It was great.)</summary>
    <content type="html"><![CDATA[<p>I attended Rails World again this year, this time in Toronto. A quick recap while it&rsquo;s still fresh.</p>

<p>What a great event. Both this year and last the organizers went out of their way to pick some of the most incredible venues I&rsquo;ve ever seen. Many places are adequate to the task of containing a conference for a few days, but few make your mouth go wide with a &ldquo;wow&rdquo; as you walk into the place.</p>

<p>This year&rsquo;s was held at Evergeen Brick Works, an old factory that lapsed into a state of disrepair for many years, and later converted to event venue. Its renovators decided to keep some aspects of the previous abandoned wreck. Its roof that&rsquo;d fallen in wasn&rsquo;t replaced, leaving the evergreens that&rsquo;d grown in the interim stretching through up into the sky (unclear what would&rsquo;ve happened if it&rsquo;d rained). Derelict machinery and the more tasteful graffiti had been left in place to add to the character. Meanwhile, ultra-modern acoustics and AV equipment made for excellent talks, and clashed nicely with the exposed brick.</p>

<p><img src="/photographs/fragments/rails-world-2024/evergreen.jpg" srcset="/photographs/fragments/rails-world-2024/evergreen@2x.jpg 2x, /photographs/fragments/rails-world-2024/evergreen.jpg 1x" loading="lazy" class="rounded-md"></p>

<p><img src="/photographs/fragments/rails-world-2024/graffiti.jpg" srcset="/photographs/fragments/rails-world-2024/graffiti@2x.jpg 2x, /photographs/fragments/rails-world-2024/graffiti.jpg 1x" loading="lazy" class="rounded-md"></p>

<p>Attention was paid to every detail. Quality drinks and delicious snacks were always on offer between sessions, and three food trucks operated all day outside (and good choices too: pizza served out of a decommissioned fire truck, beaver tails, and poutine, only Canada&rsquo;s best! <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup>). One of my favorite details that was a holdover from the conference&rsquo;s first year is that all breakfast and lunch food is edible standing up, and served out of the same area that made up the convention floor. With few tables available, people mingle organically while eating, preventing a common conference lunch problem of groups self-siloing at tables where they stay immobile for 30+ minutes and meet few new people, if any. Organizers responded dynamically to fix problems as they arose. For example, lunch lines were too long on the first day, so by day two there were double the number of food stations. Pair programming sessions were available all day through Test Double.</p>

<p>This was all a nice change after attending RailsConf a few years back. There you couldn&rsquo;t even get coffee outside a tight 30 minute availability window in the morning. This was understandable because money was tight. Ruby Central was spending it on more important things, like paying out $500k cancellation penalties to send a political &ldquo;fuck you&rdquo; to the entire state of Texas, which happily took their money and proceeded to not notice at all. (It may not be a big surprise to hear that 2025 will be the last year of RailsConf.)</p>

<p>DHH is <a href="https://world.hey.com/dhh/wonderful-rails-world-vibes-7a6141d2">pretty transparent on numbers</a>, and was up front that Rails World operates at a loss that&rsquo;s backstopped by the large companies that form Rails Foundation:</p>

<blockquote>
<p>Rails Foundation, the founding core members listed above, as well as the contributing members [&hellip;], were willing to happily underwrite a loss of over $100,000 on the conference itself.</p>
</blockquote>

<p>I love it. This is one of the best ways for companies getting good leverage out of Ruby/Rails to give back to the community. We&rsquo;re not contributing anywhere near what a colossus like Shopify is, but it felt great to have Crunchy sponsoring the event.</p>

<p><img src="/photographs/fragments/rails-world-2024/rails-8.jpg" srcset="/photographs/fragments/rails-world-2024/rails-8@2x.jpg 2x, /photographs/fragments/rails-world-2024/rails-8.jpg 1x" loading="lazy" class="rounded-md"></p>

<h2 id="tech-highlights" class="link"><a href="#tech-highlights">Tech highlights</a></h2>

<p>I spent most of the conference at our booth, so I mostly only got a chance to catch <a href="https://www.youtube.com/watch?v=-cEn_83zRFw">the keynotes</a>, but that was enough to catch the broad themes. A few notable highlights.</p>

<h3 id="solid-cache" class="link"><a href="#solid-cache">Solid Cache</a></h3>

<p>Like last year, David touched upon Solid Cache. This is such a great concept: caches traditionally always needed to be memory bound using a component like memcached or Redis because memory was fast and disks were slow. Now, memory is still fast, but with modern SSDs, disk is <em>also</em> fast, and available in much larger denominations. 37 Signal&rsquo;s products like Hey put their cache in MySQL, where they run it on a 30 TB disk with 60 days retention, and which has a 96% cache hit rate. This especially improves cache hits for the long tail of older keys that would&rsquo;ve been long since the evicted given a less spacious in-memory data set.</p>

<p>Solid Cache also dovetails well with the <a href="/fragments/single-dependency-stacks">single dependency stack</a>. Three years later we still run one and exactly one persistence component: Postgres. It&rsquo;s amazing just how plausible this is even for a mature stack, and it makes you realize that even the most fundamental belief systems of the programming world should be reevaluated every once in a while.</p>

<p>37 Signals stubbornly cargo cults Oracle products, but as Andrew covers, <a href="https://andyatkinson.com/solid-cache-rails-postgresql">Solid Cache can be made workable on Postgres too</a>. Although let me caveat that to say I&rsquo;ve never done it, and suspect that there might be issues with long-lived deletion expiration queries at the scale of 30 TB of data since Postgres isn&rsquo;t particularly good at efficiently deleting rows (a big reason that recent partitioning improvements are so important).</p>

<h3 id="server-phobia" class="link"><a href="#server-phobia">Server-phobia</a></h3>

<p>For the last few months David&rsquo;s been on an anti-cloud mission. One of keynote slides highlights the size, capacity and cost of a Performance M dyno (1 core/2 threads w/ 2.5GB for $250/mo.), with the next showing a rough equivalent on Hetzner (48 cores/96 threads w/ 256GB for $220/mo.), the clear message being that the Hetzner box is 50-100x more capable, and also cheaper. A big new piece of Rails is <a href="https://kamal-deploy.org/">Kamal</a>, a system that&rsquo;s meant to make deployment to raw metal as simple as it is on Heroku. Kamal bundles the new <a href="https://github.com/basecamp/kamal-proxy">Kamal Proxy</a>, a reverse proxy that coordinates deploys, terminates TLS, and handles graceful restarts.</p>

<p><img src="/photographs/fragments/rails-world-2024/performance-m.jpg" srcset="/photographs/fragments/rails-world-2024/performance-m@2x.jpg 2x, /photographs/fragments/rails-world-2024/performance-m.jpg 1x" loading="lazy" class="rounded-md"></p>

<p><img src="/photographs/fragments/rails-world-2024/hetzner.jpg" srcset="/photographs/fragments/rails-world-2024/hetzner@2x.jpg 2x, /photographs/fragments/rails-world-2024/hetzner.jpg 1x" loading="lazy" class="rounded-md"></p>

<p>He&rsquo;s got a point with this one. For a long time servers represented a huge capital investment and distraction from building an actual product, and in that context AWS and its ancillaries are an attractive idea. But as anyone who&rsquo;s used a lot of AWS could tell you, it may be cheap in the beginning, but it&rsquo;s only a matter of time until that inverts, and AWS bills become a recurring nightmare.</p>

<p>That said, if I were trying to send this message I&rsquo;d be careful to make it clear that this is a trade off. You&rsquo;re unquestionably going to save money on hardware, but you&rsquo;ll spend more time on management. Someone&rsquo;s also going to be the one carrying the pager for all these boxes, and presumably that&rsquo;s not the 37 Signals CEO or any of its executive team.</p>

<h3 id="rails-8-1" class="link"><a href="#rails-8-1">Rails 8.1: Et tu search?</a></h3>

<p>Rails 8 was released that day, and he closed the keynote by touching on some expected features for its next major release, 8.1. Next in its sights is the beast that no sane person wants to run: ElasticSearch, with the promise of bringing a sophisticated search engine into Rails itself. Also up for inclusion is &ldquo;House (MD)&rdquo;, which would make Markdown a more native piece of the Rails stack.</p>

<pre><code class="language-ruby"># search on any field
Post.search &quot;announcement&quot;

# by specific fields
Post.search title: &quot;announcement&quot;, content: &quot;solid search&quot;
</code></pre>

<hr />

<p><img src="/photographs/fragments/rails-world-2024/conference-hall.jpg" srcset="/photographs/fragments/rails-world-2024/conference-hall@2x.jpg 2x, /photographs/fragments/rails-world-2024/conference-hall.jpg 1x" loading="lazy" class="rounded-md"></p>

<hr />

<h2 id="20-min" class="link"><a href="#20-min">Twenty min</a></h2>

<p>Rails World was bigger this year than last, but it&rsquo;s far from a huge conference, as shown by the competitive ticketing process, where tickets were gone 20 minutes after going on sale.</p>

<p>Hard-to-get tickets are bad, but a positive side effect is that everyone at Rails World <em>really wanted</em> to be at Rails World. You don&rsquo;t get there by accident. The result is that every single person you spoke to had something interesting to say. In one case I&rsquo;d randomly started talking to a couple Dutch guys staying at the same hotel I was, and 15 minutes later we were talking about the trade offs of Aurora versus vanilla Postgres. This will sound self-serving, but I met quite a few people that were already familiar with this website, and they&rsquo;d ask <em>me</em> about topics I&rsquo;d written about recently like <a href="https://www.crunchydata.com/blog/real-world-performance-gains-with-postgres-17-btree-bulk-scans">Postgres 17 bulk B-tree lookups</a> or <a href="/fragments/secure-bytes-without-pgcrypto">generating a couple secure bytes with <code>gen_random_uuid()</code></a>.</p>

<p>I love it. The passion and expertise is the closest I&rsquo;ve experienced at any event to what we used to get in the halcyon days of the early 2010s, before tech was so obviously the most important industry in the world, and became ludicrously financialized as every venture firm and Stanford graduate jumped to get a piece of it.</p>

<hr />

<h2 id="toronto" class="link"><a href="#toronto">Unpopular opinion: Toronto</a></h2>

<p>Going on its second year now, there&rsquo;s a traditional announcement in the closing keynote of where the next Rails World will be held. In 2025, it&rsquo;ll be back in Amsterdam, and I admit to breathing a sigh of relief (assuming I can even get in).</p>

<p>The Evergreen Brickworks venue is gorgeous, Shopify&rsquo;s Toronto office is fabulous, and I had a good time visiting the city. But. Toronto&rsquo;s downtown is enormous, and it&rsquo;s the kind of place where every street, at every hour day or night, is characterized by the constant roar of total, all-encompassing, gridlock traffic. And like anywhere, when traffic is bad and tempers are heated, roads are never enough space for the pinnacle of human innovation, the automobile, and cars spill over onto every crosswalk and bike lane. With the bike lanes full, bike traffic moves onto the sidewalks, 90%+ of which is also motorized, with few riders even bothering to give lip service to those little foot rest doodads on the bottom of the bike that before the advent of the lithium battery and lightweight motor, used to be for peddling. Stop signs, red lights, and traffic priority all become the loosest of possible suggestions.</p>

<p>I&rsquo;d be exploring an inner city suburb, with leafy canopy and the most gorgeous, stately houses that positively <em>ooze</em> history in all directions. Amazing! Beautiful! Except, these otherwise quiet streets are filled to the brim with hundreds of bumper-to-bumper SUVs (no self-respecting Canadian drives anything smaller than an SUV, and a family of two or more should ideally upgrade to something a little more size appropriate, like an F-350) inching their way onward at a pace only marginally faster than a brisk walk. I&rsquo;d cross a bridge over a deep, forested ravine. Look over the edge, expecting to see a peaceful, bubbling brook far below. What do I see instead? A highway of course, which Torontonians have seen fit to plough through each of the city&rsquo;s precious few parks.</p>

<p>After one of the evening parties I found myself talking to a guy who was professing his undying love for the city of Toronto. Me: what exactly do you like about it? Him: the <em>diversityyyyyy</em> man. Me: &hellip; okay, &hellip; anything else?</p>

<p>Sorry, I can&rsquo;t help myself. But also, Amsterdam is the correct answer.</p>

<hr />

<p>To recap, great event, great people. I hope to see many of you there next year.</p>


]]></content>
    <published>2024-10-06T13:17:03-07:00</published>
    <updated>2024-10-06T13:17:03-07:00</updated>
    <link href="https://brandur.org/fragments/rails-world-2024"></link>
    <id>tag:brandur.org,2024-10-06:fragments/rails-world-2024</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>TIL: Variables in custom VSCode snippets</title>
    <summary>Using built-in variables in VSCode snippets to make publishing to this site incrementally faster.</summary>
    <content type="html"><![CDATA[<p>This blog is entirely driven by Markdown, TOML, and Git. Publishing an <a href="/atoms">atom</a> or <a href="/sequences">sequence</a> involves popping open a TOML file, adding a new item to the top, committing to Git, and pushing to origin to trigger a CI action that deploys the site:</p>

<pre><code class="language-toml">[[atoms]]
  published_at = 2024-10-04T10:24:22-07:00
  description = &quot;&quot;&quot;\
Hello, world!
&quot;&quot;&quot;
</code></pre>

<p>This generally works quite well, and in this developer&rsquo;s humble opinion, far preferable to something involving a web UI with a little text box, but when I&rsquo;m being honest with myself, I have to admit that the friction to editing is a little too high, and prevents me from publishing posts that I would&rsquo;ve done if I was on a platform <em>with</em> a web UI and a little text box, like Twitter.</p>

<p>I&rsquo;d been using <a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets">VSCode snippets</a> to speed up inserting a new TOML item, but the <code>published_at</code> date wasn&rsquo;t automated, so I&rsquo;d have to jump to a terminal, get a timestamp with <code>date</code>, then jump back and paste it. Not a big deal, but a little slow and mildly annoying.</p>

<p>I went back and RTFMed. It turns out that custom snippets support a number of built-in variables like <code>$TM_FILENAME</code>, <code>$CURRENT_SECONDS_UNIX</code>, or even <code>$UUID</code> for a random V4 UUID.</p>

<p>With a few more variables I got it to insert RFC3339 dates exactly like the ones I&rsquo;d been grabbing from my terminal:</p>

<pre><code class="language-json">{
	&quot;New atom&quot;: {
		&quot;prefix&quot;: &quot;at&quot;,
		&quot;body&quot;: [
			&quot;&quot;,
			&quot;[[atoms]]&quot;,
			&quot;  published_at = $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}T$CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND$CURRENT_TIMEZONE_OFFSET&quot;,
			&quot;  description = \&quot;\&quot;\&quot;\\&quot;,
			&quot;$1&quot;,
			&quot;\&quot;\&quot;\&quot;&quot;,
			&quot;&quot;
		],
		&quot;description&quot;: &quot;New atom&quot;
	}
}
</code></pre>

<p>There&rsquo;s quite a few other useful built-ins (e.g. currently selected text, contents of clipboard, start comment), and <a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets#_transform-examples">transformations with regex</a> are supported.</p>

<p>I also took the time to get the whitespace around the inserted block exactly right, so no extra time is needed to correct it after insertion. All in all I probably saved myself about ten seconds for each snippet use, but it&rsquo;s enough of a gain to make myself marginally more likely to do it.</p>

<p>Next up (hopefully): a mobile publishing workflow, something that&rsquo;s been sorely missing for years.</p>
]]></content>
    <published>2024-10-04T11:18:21-07:00</published>
    <updated>2024-10-04T11:18:21-07:00</updated>
    <link href="https://brandur.org/fragments/vscode-snippets"></link>
    <id>tag:brandur.org,2024-10-04:fragments/vscode-snippets</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>A few secure, random bytes without `pgcrypto`</title>
    <summary>Avoiding the &lt;code&gt;pgcrypto&lt;/code&gt; extension and its OpenSSL dependency by generating cryptographically secure randomness through &lt;code&gt;gen_random_uuid()&lt;/code&gt;.</summary>
    <content type="html"><![CDATA[<p>In Postgres it&rsquo;s common to see the SQL <code>random()</code> function used to generate a random number, but it&rsquo;s a pseudo-random number generator, and not suitable for cases where real randomness is required critical. Postgres also provides a way of getting secure random numbers as well, but only through the use of the <code>pgcrypto</code> extension, which makes <code>gen_random_bytes</code> available.</p>

<p>Pulling <code>pgcrypto</code> into your database is probably fine&mdash;at least it&rsquo;s a core extension that&rsquo;s distributed with Postgres itself&mdash;but while testing the RC version of <a href="https://www.crunchydata.com/blog/real-world-performance-gains-with-postgres-17-btree-bulk-scans">Postgres 17</a> last week, I found that it was surprisingly difficult to build Postgres against OpenSSL, which is required to build <code>pgcrypto</code>, thereby making <code>pgcrypto</code> itself hard to build.</p>

<p>I&rsquo;m broadly against the use of Postgres extensions because they make upgrades harder and projects less portable <sup id="footnote-1-source"><a href="#footnote-1">1</a></sup>, so we have a minimal posture when it comes to them, depending only on <code>btree_gist</code> and <code>pgcrypto</code>. Like <code>pgcrypto</code>, <code>btree_gist</code> is also distributed with Postgres, but unlike <code>pgcrypto</code>, doesn&rsquo;t have an OpenSSL dependency, making it trivial to build.</p>

<p>Rather than wasting more time trying to get OpenSSL configured, I did a quick code audit to find out where we were using <code>pgcrypto</code>, and found that we were using it in exactly one place to generate random bytes for use in <a href="/nanoglyphs/026-ids">a ULID</a>:</p>

<pre><code class="language-sql">-- 10 entropy bytes
ulid = timestamp || gen_random_bytes(10);
</code></pre>

<p>Needing a whole extension for generating a few random bytes seems like a waste, but unfortunately Postgres doesn&rsquo;t offer a built-in way to get cryptographically secure random bytes in any other way &hellip; or does it?</p>

<h2 id="secure-bytes" class="link"><a href="#secure-bytes">Secure bytes, just not for you</a></h2>

<p>Internally, Postgres has a module called <code>pg_strong_random.c</code> that exports a <code>pg_strong_random()</code> function that will use OpenSSL if available, but can fall back to <code>/dev/urandom</code> in case it&rsquo;s not, which is perfectly fine for our purposes:</p>

<pre><code class="language-c">/*
 * pg_strong_random &amp; pg_strong_random_init
 *
 * Generate requested number of random bytes. The returned bytes are
 * cryptographically secure, suitable for use e.g. in authentication.
 *
 * Before pg_strong_random is called in any process, the generator must first
 * be initialized by calling pg_strong_random_init().
 *
 * We rely on system facilities for actually generating the numbers.
 * We support a number of sources:
 *
 * 1. OpenSSL's RAND_bytes()
 * 2. Windows' CryptGenRandom() function
 * 3. /dev/urandom
 *
 * Returns true on success, and false if none of the sources
 * were available. NB: It is important to check the return value!
 * Proceeding with key generation when no random data was available
 * would lead to predictable keys and security issues.
 */
</code></pre>

<p>So secure randomness is available without needing to dip into OpenSSL or <code>pgcrypto</code>. Postgres just doesn&rsquo;t make it available to you.</p>

<h2 id="roundabout-randomness" class="link"><a href="#roundabout-randomness">Roundabout randomness</a></h2>
 

<p>Luckily, there&rsquo;s a workaround. <code>pg_strong_random()</code> is called through another function that&rsquo;s exported to userspace, Postgres 13&rsquo;s <code>gen_random_uuid()</code> which generates a V4 UUID that&rsquo;s secure, random data with the exception of six variant/version bits in the middle:</p>

<pre><code class="language-c">Datum
gen_random_uuid(PG_FUNCTION_ARGS)
{
    pg_uuid_t  *uuid = palloc(UUID_LEN);

    if (!pg_strong_random(uuid, UUID_LEN))
        ereport(ERROR,
                (errcode(ERRCODE_INTERNAL_ERROR),
                 errmsg(&quot;could not generate random values&quot;)));

    /*
     * Set magic numbers for a &quot;version 4&quot; (pseudorandom) UUID, see
     * http://tools.ietf.org/html/rfc4122#section-4.4
     */
    uuid-&gt;data[6] = (uuid-&gt;data[6] &amp; 0x0f) | 0x40;    /* time_hi_and_version */
    uuid-&gt;data[8] = (uuid-&gt;data[8] &amp; 0x3f) | 0x80;    /* clock_seq_hi_and_reserved */

    PG_RETURN_UUID_P(uuid);
}
</code></pre>

<p>Given our use of <code>pgcrypto</code> is so limited, and we only need ten random bytes at a time for a ULID, I changed our <code>gen_ulid()</code> implementation to find ten bytes of randomness by pulling five bytes off the front and back of a V6 UUID:</p>

<pre><code class="language-sql">-- 10 entropy bytes
--
-- We extract these by generating a random UUID and extracting
-- the first five bytes and last bytes out of it (thus avoiding
-- versioning bits in the middle). This is a roundabout way of
-- doing this, but is done to avoid a dependency on the pgcrypto
-- extension just to get `gen_random_bytes()`.
--
-- `uuid_send()` changes `uuid` to `bytea`.
random_uuid = uuid_send(gen_random_uuid());
ulid = timestamp ||
    substring(random_uuid FROM 1 FOR 5) ||
    substring(random_uuid FROM 12 FOR 5);
</code></pre>

<p>Which then lets us rid ourselves of <code>pgcrypto</code>, along with OpenSSL:</p>

<pre><code class="language-sql">DROP EXTENSION pgcrypto;
</code></pre>

<p>Making tests against a locally built version of Postgres considerably easier.</p>

<p>I&rsquo;m hoping we can ditch this hack as soon as V7 UUIDs land in core (they didn&rsquo;t make Postgres 17, which is very sad), but in the mean time, this trick might be useful to someone else.</p>


]]></content>
    <published>2024-09-24T11:38:37-07:00</published>
    <updated>2024-09-24T11:38:37-07:00</updated>
    <link href="https://brandur.org/fragments/secure-bytes-without-pgcrypto"></link>
    <id>tag:brandur.org,2024-09-24:fragments/secure-bytes-without-pgcrypto</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Direnv&#39;s `source_env`, and how to manage project configuration</title>
    <summary>How I accidentally stumbled across the &lt;code&gt;source_env&lt;/code&gt; directive and dramatically improved my configuration methodology overnight.</summary>
    <content type="html"><![CDATA[<p>For years I&rsquo;ve been using <a href="https://direnv.net/">Direnv</a> to manage configuration in projects. It&rsquo;s a small program that loads env vars out of an <code>.envrc</code> file on a directory by directory basis, using a shell hook to load vars as you enter a folder, and unload them as you leave.</p>

<p>A typical <code>.envrc</code>:</p>

<pre><code class="language-sh">export API_URL=&quot;http://localhost:5222&quot;
export DATABASE_URL=&quot;postgres://localhost:5432/project-db&quot;
export ENV_NAME=dev
</code></pre>

<p>The beauty of Direnv is not only that it&rsquo;s 12-factor friendly, but that it&rsquo;s language agnostic, and unlike its language-specific alternatives that hook into program code in various creative ways, Direnv makes configuration available to your main program <em>and</em> anything else you need to run with it.</p>

<p>So configuration is available for your project&rsquo;s core programs:</p>

<pre><code class="language-sh"># gets DATABASE_URL from env
make build/api &amp;&amp; build/api
</code></pre>

<p>And for all adjacent utilities, including ones that you didn&rsquo;t write, and would otherwise have no way of hooking into a bespoke configuration system:</p>

<pre><code class="language-sh"># still works fine!
goose -dir ./migrations/main postgres $DATABASE_URL
</code></pre>

<h2 id="uneven-distribution" class="link"><a href="#uneven-distribution">Uneven distribution</a></h2>

<p>For years I&rsquo;ve recommended in project READMEs to get started by copying an <code>.envrc</code> template and running the program:</p>

<pre><code class="language-sh">cp .envrc.sample .envrc
direnv allow
go test ./...
</code></pre>

<p><code>.envrc.sample</code> is committed to Git while <code>.envrc</code> is not due to the presumption that it may eventually be edited to include user-specific secrets.</p>

<p>That works fine, but has always had the downside in that if configuration changes and <code>.envrc.sample</code> is updated, other developers don&rsquo;t get those changes unless they copy a fresh <code>.envrc.sample</code>, and they almost certainly won&rsquo;t think to do that. This is an advantage that I&rsquo;d thought language-specific configuration systems like <a href="https://www.npmjs.com/package/dotenv0">Dotenv</a> have had over Direnv, where they can often read multiple env files, some of which may contain shared configuration that&rsquo;s versioned with the repo.</p>

<h2 id="section-1" class="link"><a href="#section-1">The missing piece of the puzzle: `source_env`</a></h2>

<p>Well, after being a Direnv user for <em>ten years</em>, yesterday I learnt of the existence of <a href="https://direnv.net/man/direnv-stdlib.1.html"><code>source_env</code></a>, a special directive that can go in an <code>.envrc</code> and which will read out out of another envrc file.</p>

<p>This simplifies the configuration of my projects <em>dramatically</em>. They have an <code>.envrc.sample</code>, but it&rsquo;s stripped down to almost nothing, containing only a <code>source_env</code> statement and room to add customization.</p>

<pre><code class="language-sh"># Common configuration for al developers, committed to Git.
source_env .envrc.local

# Custom env values go here.
</code></pre>

<p>Meanwhile, all default configuration migrates to a <code>.envrc.local</code> (the <code>.local</code> suffix not having any special meaning, but rather just a convention to use):</p>

<pre><code class="language-sh">#
# .envrc.local
#
# Shared env vars commmitted to Git and made available to all
# developers. As # much configuration should go here as possible
# so that new env vars don't break # anyone and everyone gets to
# benefit from improvements, but don't add anything too secret or
# too custom.
#

export API_URL=&quot;http://localhost:5222&quot;
export DATABASE_URL=&quot;postgres://localhost:5432/project-db&quot;
export ENV_NAME=dev
</code></pre>

<p><code>.envrc.local</code> is committed to Git, and when anyone changes configuration, all other developers get the updates the next time they pull from master.</p>

<p>This doesn&rsquo;t account for truly sensitive configuration that shouldn&rsquo;t be stored in a Git repository, but my advice on that: projects should always be able to gracefully degrade so they can run (at least in development mode) with no sensitive secrets at all. And <em>certainly</em> the test suite should be able to. If your project can&rsquo;t do that, something is wrong.</p>

<p>For my money, Direnv + <code>source_env</code> is a perfect dev configuration system, and one that works cleanly in any language ecosystem.</p>
]]></content>
    <published>2024-09-20T04:53:58-07:00</published>
    <updated>2024-09-20T04:53:58-07:00</updated>
    <link href="https://brandur.org/fragments/direnv-source-env"></link>
    <id>tag:brandur.org,2024-09-20:fragments/direnv-source-env</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
  <entry>
    <title>Your Go version CI matrix might be wrong</title>
    <summary>As of Go 1.21, Go fetches toolchains automatically, and it&amp;rsquo;s easy to not be running the version that you thought you were running.</summary>
    <content type="html"><![CDATA[<p>We had an unpleasant surprise this week in <a href="https://github.com/riverqueue/river">River&rsquo;s</a> CI suite. Since the project&rsquo;s inception we <em>thought</em> we were supporting the latest two versions of Go (1.21 and 1.22), but it turns out that we never were.</p>

<p>As per common convention, we had a GitHub Actions CI matrix testing against both versions:</p>

<pre><code class="language-yaml">strategy:
  matrix:
    go-version:
      - &quot;1.21&quot;
      - &quot;1.22&quot;
</code></pre>

<p>That looks kosher, right? Wrong!</p>

<p>Builds were happily passing this whole time, but upon closer inspection of the install step, we see this:</p>

<pre><code class="language-txt">Run actions/setup-go@v5
Setup go version spec 1.21
Found in cache @ /opt/hostedtoolcache/go/1.21.12/x64
Added go to the path
Successfully set up Go version 1.21
go: downloading go1.22.5 (linux/amd64)
</code></pre>

<p>GitHub Actions had been downloading Go 1.21, then immediately upgrading itself to Go 1.22.</p>

<p>Since Go 1.21, Go has had a built in concepts called <a href="https://go.dev/doc/toolchain">toolchains</a>. An installed version of Go contains its own toolchain, but has the capacity to fetch and install other toolchains as well. Usually this is convenient feature because it means you can drop into any Go project and immediately get it running with a single command with no package or version managers in sight, but it has unexpected side effects.</p>

<h2 id="go-mod" class="link"><a href="#go-mod">`go.mod` version and toolchain</a></h2>

<p>Along with toolchains, Go 1.21 also changed its treatment of <code>go</code> directives in <code>go.mod</code> so that instead of an advisory requirement, they&rsquo;re now a mandatory one. Any Go project needs to have its own <code>go</code> directive set to something at least as high as any modules it requires. So if a dependency requires Go 1.22.5, the project itself must be set to at least Go 1.22.5. Most of the time you won&rsquo;t even notice this because getting a new module with <code>go get</code> will handle updating a project&rsquo;s <code>go</code> directive automatically.</p>

<p>Given River is always a dependency, we want to provide as much leeway as possible on the minimum version bound, even while we&rsquo;ll always be using more modern versions of Go. <code>go.mod</code> files support a <code>go</code> directive along with a <code>toolchain</code> to specify a minimum bound along with a preferred toolchain:</p>

<pre><code class="language-txt">go 1.21

toolchain go1.22.5
</code></pre>

<p>Once again though, the presence of <code>toolchain</code> will cause CI jobs to upgrade themselves to 1.22 instead of running on the version of Go they&rsquo;re supposed to be targeting. We need one more magic env var to prevent this:</p>

<pre><code class="language-yaml">env:
  # The special value &quot;local&quot; tells Go to use the bundled Go
  # version rather than trying to fetch one according to a
  # `toolchain` value in `go.mod`. This ensures that we're
  # really running the Go version in the CI matrix rather than
  # one that the Go command has upgraded to automatically.
  GOTOOLCHAIN: local
</code></pre>

<h2 id="go-directives" class="link"><a href="#go-directives">Take care with `go` directives</a></h2>

<p>A learning from this debacle is that Go modules that expect to be dependencies need to be very careful with the <code>go</code> directive in <code>go.mod</code> because it could have considerable downstream impact.</p>

<p>We&rsquo;re setting <code>go 1.21</code> which is the same as <code>go 1.21.0</code>, so any project that requires River will be able to use any patch version of Go 1.21 or 1.22.</p>

<p>Go&rsquo;s incredibly trigger happy when it comes to changing a <code>go.mod'</code>s <code>go</code> version, which it will happily and silently do at any opportunity. I&rsquo;m legitimately amazed that we haven&rsquo;t seen more problems where dependencies accidentally upgrade to a new version of Go and break any downstream projects where that new version isn&rsquo;t yet available. This could even happen where a patch version changes as a brand new Go release comes out, but isn&rsquo;t yet available in everyone&rsquo;s build systems.</p>

<p>River&rsquo;s a multi-module project, and we hadn&rsquo;t even intentionally updated to Go 1.22.5, which spurred the bug report that led to discovery of the issue. I think what happened is that as we added new modules with <code>go mod init</code>, those would get assigned the latest patch release of Go, and then as we we required those from other modules, the new versions would proliferate. We&rsquo;d see the change in diffs being reviewed, but didn&rsquo;t think much of it.</p>

<p>Along with patching all our directives to <code>go 1.21</code> we&rsquo;ll also be adding a CI check that verifies they all match up across modules to avoid any accidental version bumps in the future.</p>
]]></content>
    <published>2024-08-11T11:52:37-07:00</published>
    <updated>2024-08-11T11:52:37-07:00</updated>
    <link href="https://brandur.org/fragments/go-version-matrix"></link>
    <id>tag:brandur.org,2024-08-11:fragments/go-version-matrix</id>
    <author>
      <name>Brandur Leach</name>
      <uri>https://brandur.org</uri>
    </author>
  </entry>
</feed>