<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Race-Conditions on houdeshell.dev</title><link>https://houdeshell.dev/tags/race-conditions/</link><description>Recent content in Race-Conditions on houdeshell.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>© CRH</copyright><lastBuildDate>Sat, 05 Sep 2026 10:00:00 -0400</lastBuildDate><atom:link href="https://houdeshell.dev/tags/race-conditions/index.xml" rel="self" type="application/rss+xml"/><item><title>Software that I &lt;img class='title-icon' src='https://houdeshell.dev/static/images/lego-heart.svg' alt='heart' /></title><link>https://houdeshell.dev/software/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/software/</guid><description>&lt;p>I bounce between Mac, Windows, and Linux daily. Honestly, the tools matter more than the OS at this point. Great software is great software, and I&amp;rsquo;ve been lucky enough to build a career on top of things other people built well.&lt;/p>
&lt;p>This is the stuff I actually reach for. Not a &amp;ldquo;best of&amp;rdquo; list, not sponsored, not comprehensive. Just software that makes me better at what I do. Or at least makes the work more fun.&lt;/p>
&lt;style>
.sw-grid {
display: flex;
flex-direction: column;
margin: 1.6em 0 2.6em;
counter-reset: sw;
border-top: 1px solid var(--border);
}
/* Ledger: name, tag and quip on one row so the whole kit reads as a table.
Widths are sized to the real content — longest name is 18 characters
("JetBrains DataGrip"), longest tag is 16 ("it's complicated") — and every
pixel left over goes to the quip, which is the column worth reading. */
.sw-card {
display: grid;
grid-template-columns: 34px 11.5rem 8rem minmax(0, 1fr);
grid-template-areas: "num title tag desc";
column-gap: 1.4em;
row-gap: 0.3em;
align-items: baseline;
padding: 0.85em 6px 0.85em 4px;
border-bottom: 1px solid var(--border);
counter-increment: sw;
transition: background 0.2s ease, padding-left 0.2s ease;
}
.sw-card::before {
content: counter(sw, decimal-leading-zero);
grid-area: num;
align-self: baseline;
font-family: var(--font-mono);
font-size: 0.72em;
font-weight: 500;
letter-spacing: 0.08em;
color: var(--text-muted);
transition: color 0.2s ease;
}
.sw-card:hover {
background: rgba(251, 60, 0, 0.035);
padding-left: 12px;
}
.sw-card:hover::before {
color: #fb3c00;
}
.sw-card .sw-tag {
grid-area: tag;
align-self: baseline;
font-family: var(--font-mono);
font-size: 0.7em;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-secondary);
background: none;
border: none;
padding: 0;
margin: 0;
transition: color 0.2s ease;
}
.sw-card:hover .sw-tag {
color: #fb3c00;
}
.sw-card h3 {
grid-area: title;
margin: 0 !important;
line-height: 1.35 !important;
font-size: 1em !important;
font-weight: 600 !important;
letter-spacing: -0.005em !important;
border: none !important;
padding: 0 !important;
color: var(--text) !important;
text-decoration: none !important;
}
.sw-card h3::before {
content: none !important;
}
.sw-card h3 a {
color: var(--text);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.sw-card:hover h3 a {
color: #fb3c00;
border-bottom-color: rgba(251, 60, 0, 0.4);
}
.sw-card p {
grid-area: desc;
margin: 0 !important;
font-size: 0.93em;
color: var(--text-secondary);
line-height: 1.6;
}
.sw-card p a {
color: var(--text);
text-decoration: none;
border-bottom: 1px dashed var(--accent-line);
}
.sw-card p a:hover {
color: #fb3c00;
border-bottom-color: #fb3c00;
}
.sw-card code {
background: rgba(0, 0, 0, 0.04) !important;
color: var(--text) !important;
border-color: var(--border) !important;
}
@media (max-width: 1000px) {
.sw-card {
grid-template-columns: 44px 130px 1fr;
grid-template-areas:
"num tag title"
"num tag desc";
column-gap: 1.5em;
padding: 1em 6px 1em 4px;
}
}
@media (max-width: 640px) {
.sw-card {
grid-template-columns: 1fr;
grid-template-areas:
"num"
"tag"
"title"
"desc";
row-gap: 0.35em;
padding: 0.95em 4px;
}
.sw-card:hover {
padding-left: 4px;
}
}
&lt;/style>
&lt;h2 id="software-development" class="md-heading">
Software Development
&lt;a class="md-heading-anchor" href="#software-development" aria-label="Link to Software Development">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="jetbrains-rider" class="md-heading">
&lt;a href="https://www.jetbrains.com/rider/"target="_blank" rel="noopener noreferrer">JetBrains Rider&lt;/a>
&lt;a class="md-heading-anchor" href="#jetbrains-rider" aria-label="Link to JetBrains Rider">#&lt;/a>
&lt;/h3>
&lt;p>Cross-platform .NET IDE. Fast, smart, and doesn&amp;rsquo;t need Visual Studio&amp;rsquo;s weight to get the job done.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="visual-studio" class="md-heading">
&lt;a href="https://visualstudio.microsoft.com/"target="_blank" rel="noopener noreferrer">Visual Studio&lt;/a>
&lt;a class="md-heading-anchor" href="#visual-studio" aria-label="Link to Visual Studio">#&lt;/a>
&lt;/h3>
&lt;p>The OG. Heavy, but when you need the full .NET debugging experience, nothing else comes close.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="vs-code" class="md-heading">
&lt;a href="https://code.visualstudio.com/"target="_blank" rel="noopener noreferrer">VS Code&lt;/a>
&lt;a class="md-heading-anchor" href="#vs-code" aria-label="Link to VS Code">#&lt;/a>
&lt;/h3>
&lt;p>Extension ecosystem is unmatched. Somehow Electron done right.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="neovim" class="md-heading">
&lt;a href="https://neovim.io/"target="_blank" rel="noopener noreferrer">Neovim&lt;/a>
&lt;a class="md-heading-anchor" href="#neovim" aria-label="Link to Neovim">#&lt;/a>
&lt;/h3>
&lt;p>Vim reborn. Lua config, LSP native, and a plugin ecosystem that won&amp;rsquo;t quit. &lt;code>:wq&lt;/code> is a lifestyle.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="git" class="md-heading">
&lt;a href="https://git-scm.com/"target="_blank" rel="noopener noreferrer">Git&lt;/a>
&lt;a class="md-heading-anchor" href="#git" aria-label="Link to Git">#&lt;/a>
&lt;/h3>
&lt;p>The version control system that won. Love it or hate it, you can&amp;rsquo;t ship without it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="lazygit" class="md-heading">
&lt;a href="https://github.com/jesseduffield/lazygit"target="_blank" rel="noopener noreferrer">lazygit&lt;/a>
&lt;a class="md-heading-anchor" href="#lazygit" aria-label="Link to lazygit">#&lt;/a>
&lt;/h3>
&lt;p>Terminal UI for git that makes interactive rebases feel like cheating.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">JSON&lt;/span>
&lt;h3 id="jq" class="md-heading">
&lt;a href="https://jqlang.github.io/jq/"target="_blank" rel="noopener noreferrer">jq&lt;/a>
&lt;a class="md-heading-anchor" href="#jq" aria-label="Link to jq">#&lt;/a>
&lt;/h3>
&lt;p>&lt;code>sed&lt;/code> for JSON. Once you learn the syntax, you&amp;rsquo;ll pipe everything through it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="jetbrains-datagrip" class="md-heading">
&lt;a href="https://www.jetbrains.com/datagrip/"target="_blank" rel="noopener noreferrer">JetBrains DataGrip&lt;/a>
&lt;a class="md-heading-anchor" href="#jetbrains-datagrip" aria-label="Link to JetBrains DataGrip">#&lt;/a>
&lt;/h3>
&lt;p>SQL IDE that actually understands your schema. Autocomplete that works across joins.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="dbeaver" class="md-heading">
&lt;a href="https://dbeaver.io/"target="_blank" rel="noopener noreferrer">DBeaver&lt;/a>
&lt;a class="md-heading-anchor" href="#dbeaver" aria-label="Link to DBeaver">#&lt;/a>
&lt;/h3>
&lt;p>Universal database tool that actually works. Connect to anything, query everything.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="ssms" class="md-heading">
&lt;a href="https://learn.microsoft.com/en-us/sql/ssms/"target="_blank" rel="noopener noreferrer">SSMS&lt;/a>
&lt;a class="md-heading-anchor" href="#ssms" aria-label="Link to SSMS">#&lt;/a>
&lt;/h3>
&lt;p>Microsoft SQL Management Studio. If you&amp;rsquo;re in SQL Server land, you already know.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="postgresql" class="md-heading">
&lt;a href="https://www.postgresql.org/"target="_blank" rel="noopener noreferrer">PostgreSQL&lt;/a>
&lt;a class="md-heading-anchor" href="#postgresql" aria-label="Link to PostgreSQL">#&lt;/a>
&lt;/h3>
&lt;p>The database that keeps getting better. Extensions, JSON support, and rock-solid reliability.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">containers&lt;/span>
&lt;h3 id="docker" class="md-heading">
&lt;a href="https://www.docker.com/"target="_blank" rel="noopener noreferrer">Docker&lt;/a>
&lt;a class="md-heading-anchor" href="#docker" aria-label="Link to Docker">#&lt;/a>
&lt;/h3>
&lt;p>&amp;ldquo;Works on my machine&amp;rdquo; became &amp;ldquo;works on every machine.&amp;rdquo; Changed how we ship software.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="languages" class="md-heading">
Languages
&lt;a class="md-heading-anchor" href="#languages" aria-label="Link to Languages">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">backend&lt;/span>
&lt;h3 id="c" class="md-heading">
&lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/"target="_blank" rel="noopener noreferrer">C#&lt;/a>
&lt;a class="md-heading-anchor" href="#c" aria-label="Link to C#">#&lt;/a>
&lt;/h3>
&lt;p>Pays the bills. &lt;code>Span&amp;lt;T&amp;gt;&lt;/code> and ArrayPool mean the hot paths stay in managed code instead of dropping to C.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">web&lt;/span>
&lt;h3 id="typescript" class="md-heading">
&lt;a href="https://www.typescriptlang.org/"target="_blank" rel="noopener noreferrer">TypeScript&lt;/a>
&lt;a class="md-heading-anchor" href="#typescript" aria-label="Link to TypeScript">#&lt;/a>
&lt;/h3>
&lt;p>JavaScript with adult supervision. The type system is stranger and better than people expect, right up until someone reaches for &lt;code>any&lt;/code>.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">services&lt;/span>
&lt;h3 id="go" class="md-heading">
&lt;a href="https://go.dev/"target="_blank" rel="noopener noreferrer">Go&lt;/a>
&lt;a class="md-heading-anchor" href="#go" aria-label="Link to Go">#&lt;/a>
&lt;/h3>
&lt;p>Boring on purpose, which is the whole point. You&amp;rsquo;ll write &lt;code>if err != nil&lt;/code> ten thousand times and ship something that runs for years.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">systems&lt;/span>
&lt;h3 id="rust" class="md-heading">
&lt;a href="https://www.rust-lang.org/"target="_blank" rel="noopener noreferrer">Rust&lt;/a>
&lt;a class="md-heading-anchor" href="#rust" aria-label="Link to Rust">#&lt;/a>
&lt;/h3>
&lt;p>The borrow checker is a reviewer who never gets tired and is never wrong. Miserable for a week, then that entire class of bug is gone.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">the metal&lt;/span>
&lt;h3 id="c-1" class="md-heading">
&lt;a href="https://en.cppreference.com/w/c"target="_blank" rel="noopener noreferrer">C&lt;/a>
&lt;a class="md-heading-anchor" href="#c-1" aria-label="Link to C">#&lt;/a>
&lt;/h3>
&lt;p>Still the substrate. Every abstraction you like is a polite lie told on top of it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">the metal&lt;/span>
&lt;h3 id="c-2" class="md-heading">
&lt;a href="https://isocpp.org/"target="_blank" rel="noopener noreferrer">C++&lt;/a>
&lt;a class="md-heading-anchor" href="#c-2" aria-label="Link to C&amp;#43;&amp;#43;">#&lt;/a>
&lt;/h3>
&lt;p>Powerful enough to build anything, sharp enough to take a finger off. Worth it when the alternative is worse.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="ai" class="md-heading">
AI
&lt;a class="md-heading-anchor" href="#ai" aria-label="Link to AI">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="claude-code" class="md-heading">
&lt;a href="https://docs.anthropic.com/en/docs/claude-code"target="_blank" rel="noopener noreferrer">Claude Code&lt;/a>
&lt;a class="md-heading-anchor" href="#claude-code" aria-label="Link to Claude Code">#&lt;/a>
&lt;/h3>
&lt;p>AI pair programmer in your terminal. It&amp;rsquo;s writing this page right now.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="vs-code-agents" class="md-heading">
&lt;a href="https://code.visualstudio.com/"target="_blank" rel="noopener noreferrer">VS Code Agents&lt;/a>
&lt;a class="md-heading-anchor" href="#vs-code-agents" aria-label="Link to VS Code Agents">#&lt;/a>
&lt;/h3>
&lt;p>Native agent mode inside the editor. Let the assistant plan, edit files, and run commands without leaving VS Code or babysitting a separate CLI.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="codex" class="md-heading">
&lt;a href="https://github.com/openai/codex"target="_blank" rel="noopener noreferrer">Codex&lt;/a>
&lt;a class="md-heading-anchor" href="#codex" aria-label="Link to Codex">#&lt;/a>
&lt;/h3>
&lt;p>OpenAI&amp;rsquo;s open-source coding agent. Terminal-native, sandboxed by default, and you can swap the model behind it whenever.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">platform&lt;/span>
&lt;h3 id="azure-ai-foundry" class="md-heading">
&lt;a href="https://azure.microsoft.com/en-us/products/ai-foundry"target="_blank" rel="noopener noreferrer">Azure AI Foundry&lt;/a>
&lt;a class="md-heading-anchor" href="#azure-ai-foundry" aria-label="Link to Azure AI Foundry">#&lt;/a>
&lt;/h3>
&lt;p>Microsoft&amp;rsquo;s AI app stack. Build agents, point them at your data, ship them on Azure. The enterprise wrapper for everything you&amp;rsquo;d otherwise glue together yourself.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="infrastructure" class="md-heading">
Infrastructure
&lt;a class="md-heading-anchor" href="#infrastructure" aria-label="Link to Infrastructure">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="kubernetes" class="md-heading">
&lt;a href="https://kubernetes.io/"target="_blank" rel="noopener noreferrer">Kubernetes&lt;/a>
&lt;a class="md-heading-anchor" href="#kubernetes" aria-label="Link to Kubernetes">#&lt;/a>
&lt;/h3>
&lt;p>Container orchestration that makes you mass produce YAML for a living.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="k3s" class="md-heading">
&lt;a href="https://k3s.io/"target="_blank" rel="noopener noreferrer">K3s&lt;/a>
&lt;a class="md-heading-anchor" href="#k3s" aria-label="Link to K3s">#&lt;/a>
&lt;/h3>
&lt;p>All of Kubernetes in a single binary. Perfect for homelab, edge, and production deployments.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IaC&lt;/span>
&lt;h3 id="terraform--opentofu" class="md-heading">
&lt;a href="https://www.terraform.io/"target="_blank" rel="noopener noreferrer">Terraform&lt;/a> / &lt;a href="https://opentofu.org/"target="_blank" rel="noopener noreferrer">OpenTofu&lt;/a>
&lt;a class="md-heading-anchor" href="#terraform--opentofu" aria-label="Link to Terraform / OpenTofu">#&lt;/a>
&lt;/h3>
&lt;p>Infrastructure as code. OpenTofu if you prefer the open-source fork without the license drama.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">networking&lt;/span>
&lt;h3 id="tailscale" class="md-heading">
&lt;a href="https://tailscale.com/"target="_blank" rel="noopener noreferrer">Tailscale&lt;/a>
&lt;a class="md-heading-anchor" href="#tailscale" aria-label="Link to Tailscale">#&lt;/a>
&lt;/h3>
&lt;p>WireGuard-based mesh VPN that just works. Connect everything without opening a single port.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">observability&lt;/span>
&lt;h3 id="grafana" class="md-heading">
&lt;a href="https://grafana.com/"target="_blank" rel="noopener noreferrer">Grafana&lt;/a>
&lt;a class="md-heading-anchor" href="#grafana" aria-label="Link to Grafana">#&lt;/a>
&lt;/h3>
&lt;p>Dashboards for everything. Pairs with Prometheus, Loki, and Tempo for the whole observability stack.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="k9s" class="md-heading">
&lt;a href="https://k9scli.io/"target="_blank" rel="noopener noreferrer">k9s&lt;/a>
&lt;a class="md-heading-anchor" href="#k9s" aria-label="Link to k9s">#&lt;/a>
&lt;/h3>
&lt;p>Terminal UI for Kubernetes. Makes cluster management feel like a video game. Way faster than raw &lt;code>kubectl&lt;/code>.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="lens" class="md-heading">
&lt;a href="https://k8slens.dev/"target="_blank" rel="noopener noreferrer">Lens&lt;/a>
&lt;a class="md-heading-anchor" href="#lens" aria-label="Link to Lens">#&lt;/a>
&lt;/h3>
&lt;p>The Kubernetes IDE. When you want to point and click your way through a cluster without shame.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">it's complicated&lt;/span>
&lt;h3 id="cloudflare" class="md-heading">
&lt;a href="https://www.cloudflare.com/"target="_blank" rel="noopener noreferrer">Cloudflare&lt;/a>
&lt;a class="md-heading-anchor" href="#cloudflare" aria-label="Link to Cloudflare">#&lt;/a>
&lt;/h3>
&lt;p>You love them. You hate them. Your DNS is already there. Their edge network is everywhere, their pricing is unbeatable, and their product sprawl is terrifying. Stockholm syndrome as a service.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="shell--terminal" class="md-heading">
Shell &amp;amp; Terminal
&lt;a class="md-heading-anchor" href="#shell--terminal" aria-label="Link to Shell &amp;amp; Terminal">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="bash" class="md-heading">
&lt;a href="https://www.gnu.org/software/bash/"target="_blank" rel="noopener noreferrer">Bash&lt;/a>
&lt;a class="md-heading-anchor" href="#bash" aria-label="Link to Bash">#&lt;/a>
&lt;/h3>
&lt;p>The shell that&amp;rsquo;s been there since before you were born. Simple, portable, everywhere.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="zsh" class="md-heading">
&lt;a href="https://www.zsh.org/"target="_blank" rel="noopener noreferrer">Zsh&lt;/a>
&lt;a class="md-heading-anchor" href="#zsh" aria-label="Link to Zsh">#&lt;/a>
&lt;/h3>
&lt;p>Bash&amp;rsquo;s cooler sibling. Tab completion, globbing, and plugin support that actually makes the terminal enjoyable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">prompt&lt;/span>
&lt;h3 id="starship" class="md-heading">
&lt;a href="https://github.com/starship/starship"target="_blank" rel="noopener noreferrer">Starship&lt;/a>
&lt;a class="md-heading-anchor" href="#starship" aria-label="Link to Starship">#&lt;/a>
&lt;/h3>
&lt;p>Cross-shell prompt written in Rust. Fast, pretty, and infinitely configurable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">multiplexer&lt;/span>
&lt;h3 id="tmux" class="md-heading">
&lt;a href="https://github.com/tmux/tmux"target="_blank" rel="noopener noreferrer">tmux&lt;/a>
&lt;a class="md-heading-anchor" href="#tmux" aria-label="Link to tmux">#&lt;/a>
&lt;/h3>
&lt;p>Terminal multiplexer. SSH into a box, detach, come back tomorrow. Your session is still there.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">coreutils&lt;/span>
&lt;h3 id="eza" class="md-heading">
&lt;a href="https://github.com/eza-community/eza"target="_blank" rel="noopener noreferrer">eza&lt;/a>
&lt;a class="md-heading-anchor" href="#eza" aria-label="Link to eza">#&lt;/a>
&lt;/h3>
&lt;p>Modern replacement for &lt;code>ls&lt;/code>. Colors, git status, and tree view out of the box. Written in Rust, obviously.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="utilities--apps" class="md-heading">
Utilities / Apps
&lt;a class="md-heading-anchor" href="#utilities--apps" aria-label="Link to Utilities / Apps">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">macOS&lt;/span>
&lt;h3 id="alttab" class="md-heading">
&lt;a href="https://alt-tab-macos.netlify.app/"target="_blank" rel="noopener noreferrer">AltTab&lt;/a>
&lt;a class="md-heading-anchor" href="#alttab" aria-label="Link to AltTab">#&lt;/a>
&lt;/h3>
&lt;p>Why does macOS think a minimized window is an inactive window and not let you switch to it? This fixes that.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>About</title><link>https://houdeshell.dev/about/</link><pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/about/</guid><description>&lt;div class="about-intro">
&lt;p class="about-tagline">&lt;span class="about-tagline-slash">//&lt;/span> bit herder. pointer wrecker. yaml apologist.&lt;/p>
&lt;/div>
&lt;p>I&amp;rsquo;m a bit herder, a pointer wrecker, and HTTP is my best friend. &lt;code>0xA3&lt;/code> &amp;amp; &lt;code>0x7C&lt;/code> are also my friends, but not &lt;code>0x08&lt;/code>.&lt;/p>
&lt;p>I have strong opinions about most of this and I hold every one of them loosely. Show me better data and I&amp;rsquo;ll change my mind on the spot, without being weird about it. Ideas are supposed to adapt. So are the people holding them.&lt;/p>
&lt;p>I spend my days somewhere between writing code and making sure other people can write better code. I love distributed systems, low-level embedded devices, and anything that has a &lt;code>content-type&lt;/code>. I also have an unhealthy relationship with YAML, but that&amp;rsquo;s a Kubernetes problem.&lt;/p>
&lt;h2 id="the-day-job" class="md-heading">
The day job
&lt;a class="md-heading-anchor" href="#the-day-job" aria-label="Link to The day job">#&lt;/a>
&lt;/h2>
&lt;p>During the day, I lead engineering and operations at &lt;a href="https://www.energycap.com"target="_blank" rel="noopener noreferrer">EnergyCAP&lt;/a>, a SaaS platform that helps organizations track utility spend, energy consumption, and sustainability goals across massive building portfolios. We&amp;rsquo;re talking &lt;strong>$100B+ in utility bill spend&lt;/strong> tracked across &lt;strong>350,000+ sites&lt;/strong>. Government, education, healthcare, commercial real estate. If it has a meter, we probably manage it.&lt;/p>
&lt;p>I started here as a developer in 2009 and have been building ever since: through senior roles, into leadership, and eventually into the VP seat where I get to shape both the tech and the teams behind it. Some days that means architecture decisions. Most days it means removing blockers so smart people can do their best work. And yes, sometimes it&amp;rsquo;s just meetings about meetings.&lt;/p>
&lt;p>My philosophy is simple: &lt;strong>technology should serve people&lt;/strong>. Build teams where people feel safe to be wrong, give them the tools to be great, and get out of the way.&lt;/p>
&lt;h3 id="things-i-geek-out-about" class="md-heading">
Things I geek out about
&lt;a class="md-heading-anchor" href="#things-i-geek-out-about" aria-label="Link to Things I geek out about">#&lt;/a>
&lt;/h3>
&lt;p>Distributed systems, container orchestration, database performance tuning, developer tooling, process dumps, leadership that doesn&amp;rsquo;t suck, making engineers&amp;rsquo; lives better, and the eternal quest for a perfect terminal setup.&lt;/p>
&lt;h2 id="i-also-talk-at-things" class="md-heading">
I also talk at things
&lt;a class="md-heading-anchor" href="#i-also-talk-at-things" aria-label="Link to I also talk at things">#&lt;/a>
&lt;/h2>
&lt;p>I like getting on stage and nerding out about the things I&amp;rsquo;ve learned the hard way. My talks tend to fall into a few buckets, and yes, I have strong opinions about all of them.&lt;/p>
&lt;h3 id="ai--the-developer-experience" class="md-heading">
AI &amp;amp; the developer experience
&lt;a class="md-heading-anchor" href="#ai--the-developer-experience" aria-label="Link to AI &amp;amp; the developer experience">#&lt;/a>
&lt;/h3>
&lt;p>The AI wave hit and I leaned in. I&amp;rsquo;ve talked about using LLMs as debugging partners, building RAG pipelines that actually know your business context, the ethical lines we should be drawing as we hand more work to machines, and what happens to the junior-to-senior pipeline when the tools eat the entry-level work.&lt;/p>
&lt;ul class="talk-list">
&lt;li>We Broke the Way Junior Engineers Become Senior Engineers&lt;/li>
&lt;li>My Rubber Duck is a Large Language Model&lt;/li>
&lt;li>Unleashing the Power of the AI Wizards: Retrieval-Augmented Generation Spells&lt;/li>
&lt;/ul>
&lt;h3 id="kubernetes--cloud-native" class="md-heading">
Kubernetes &amp;amp; cloud native
&lt;a class="md-heading-anchor" href="#kubernetes--cloud-native" aria-label="Link to Kubernetes &amp;amp; cloud native">#&lt;/a>
&lt;/h3>
&lt;p>I was late to the Kubernetes party, and I&amp;rsquo;ll tell you all about it. From workshops where we break clusters on purpose, to navigating the 900+ services in the CNCF landscape, to making the case that cloud-native principles work even when you&amp;rsquo;re not in the cloud.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Kubernetes: The Hard Parts, Learned the Easy Way (workshop)&lt;/li>
&lt;li>Your Pod Is Pending and Nobody Knows Why (workshop)&lt;/li>
&lt;li>Kubernetes Chronicles: Late to the Party, Big on Adventure!&lt;/li>
&lt;li>K3s: Half the Size, Twice as Awesome (workshop)&lt;/li>
&lt;li>Orchestrating Machine Learning Workloads with Kubernetes&lt;/li>
&lt;li>Exploring the Cloud Native Landscape&lt;/li>
&lt;li>Cloud Native is Only for the Cloud, Right?&lt;/li>
&lt;li>Nomad: Orchestration Doesn't Start with a K&lt;/li>
&lt;/ul>
&lt;h3 id="performance--databases" class="md-heading">
Performance &amp;amp; databases
&lt;a class="md-heading-anchor" href="#performance--databases" aria-label="Link to Performance &amp;amp; databases">#&lt;/a>
&lt;/h3>
&lt;p>I&amp;rsquo;ve spent an unreasonable amount of time staring at query plans and disassembly. These talks cover squeezing performance out of SQL Server, using DMVs like cheat codes, chasing allocations below the level of Big-O, and that one time we took a 10-hour process down to 10 minutes.&lt;/p>
&lt;ul class="talk-list">
&lt;li>The Allocation Is the Problem&lt;/li>
&lt;li>Your Benchmark Is Lying to You&lt;/li>
&lt;li>ReArchitecting Data: 10 Hours to 10 Minutes&lt;/li>
&lt;li>Mastering SQL Server Performance Optimization (workshop)&lt;/li>
&lt;li>SQL Server DMVs That Give Me Superpowers&lt;/li>
&lt;li>Achieving Continuous High Performance with Query Store&lt;/li>
&lt;li>Practical High Performance: C# Edition&lt;/li>
&lt;li>Intrinsics in .NET: Start Somewhere&lt;/li>
&lt;/ul>
&lt;h3 id="production-war-stories--leadership" class="md-heading">
Production war stories &amp;amp; leadership
&lt;a class="md-heading-anchor" href="#production-war-stories--leadership" aria-label="Link to Production war stories &amp;amp; leadership">#&lt;/a>
&lt;/h3>
&lt;p>Production breaks. Systems fail. The interesting part is what you do next, and how often the retry you added to be safe is the thing that took you down. I also talk about the human side: what I&amp;rsquo;ve learned (and gotten wrong) leading engineering teams.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Your Retry Is the Outage&lt;/li>
&lt;li>The Postmortem Is Not the Point&lt;/li>
&lt;li>The Corpse Knows: Debugging Production with Process Dumps&lt;/li>
&lt;li>Bug Squashing with Process Dumps&lt;/li>
&lt;li>Recovery by Design: A Postmortem Adventure&lt;/li>
&lt;li>Joining the Cloud: Our Journey&lt;/li>
&lt;li>TIL as a CTO&lt;/li>
&lt;/ul>
&lt;h3 id="the-wildcard" class="md-heading">
The wildcard
&lt;a class="md-heading-anchor" href="#the-wildcard" aria-label="Link to The wildcard">#&lt;/a>
&lt;/h3>
&lt;p>Sometimes you just want to build something fun with your hands.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Paper Circuits: Origami for a New Generation&lt;/li>
&lt;/ul>
&lt;h2 id="lets-talk" class="md-heading">
Let&amp;rsquo;s talk
&lt;a class="md-heading-anchor" href="#lets-talk" aria-label="Link to Let’s talk">#&lt;/a>
&lt;/h2>
&lt;p>I drink a bit too much coffee while rambling on about technology. If you&amp;rsquo;re willing to have a conversation, I&amp;rsquo;m ready to buy you a cup.&lt;/p>
&lt;dl class="contact-list">
&lt;dt>email&lt;/dt>&lt;dd>&lt;a href="mailto:chris@houdeshell.dev">chris@houdeshell.dev&lt;/a>&lt;/dd>
&lt;dt>github&lt;/dt>&lt;dd>&lt;a href="https://github.com/choudeshell">choudeshell&lt;/a>&lt;/dd>
&lt;dt>linkedin&lt;/dt>&lt;dd>&lt;a href="https://www.linkedin.com/in/choudeshell/">choudeshell&lt;/a>&lt;/dd>
&lt;dt>bluesky&lt;/dt>&lt;dd>&lt;a href="https://bsky.app/profile/choudeshell.bsky.social">@choudeshell.bsky.social&lt;/a>&lt;/dd>
&lt;/dl></description></item><item><title>Now</title><link>https://houdeshell.dev/now/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/now/</guid><description/></item><item><title>The Continuation That Never Woke Up</title><link>https://houdeshell.dev/post/2026-09-05_continuation-that-never-woke-up/</link><pubDate>Sat, 05 Sep 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-09-05_continuation-that-never-woke-up/</guid><description>&lt;p>An order got stuck halfway through post processing. Somebody re-ran it. It went through fine. Ticket closed.&lt;/p>
&lt;p>A few weeks later it happened again, and we did the same thing, because the same thing worked.&lt;/p>
&lt;p>That is a great way to never fix anything.&lt;/p>
&lt;p>The pipeline is not complicated. Two enrichment jobs run, a validation job reads what they wrote, a notification goes out at the end. Most orders go through without anyone ever thinking about them.&lt;/p>
&lt;p>The stuck ones had nothing visibly wrong. No exception. No failed job. Nothing red in the dashboard, nothing in the error logs. Just a record sitting in an intermediate status waiting on a validation step that was never going to run.&lt;/p>
&lt;p>And the retry always worked, which is the perfect disguise. Intermittent, self healing, no stack trace, no customer screaming. Nobody escalates that. It goes on the pile with the flaky tests and the one integration that times out on Mondays.&lt;/p>
&lt;p>I looked at it three times over about two months and gave up twice.&lt;/p>
&lt;p>When I finally got it, the answer had been in our logs the whole time. Naming the exact batch id. At a log level we had deliberately filtered out.&lt;/p>
&lt;figure class="post-figure">
&lt;img loading="lazy" src="https://houdeshell.dev/post/2026-09-05_continuation-that-never-woke-up/lost-wakeup-timeline.png" alt="Timeline of the race. An enrichment batch lane runs along the top: the continuation is registered and the lock released, then the batch succeeds and fires its wakeup. An outer transaction lane runs below it, still open the whole time. The wakeup falls from the top lane to the bottom one and lands on nothing, because the continuation batch has not been written to storage yet. The gap between the two is shaded and labelled the window." title="The lock comes off at the red dot. The continuation is not in storage until the purple one.">
&lt;figcaption>The lock comes off at the red dot. The continuation is not in storage until the purple one.&lt;/figcaption>
&lt;/figure>
&lt;h2 id="tldr" class="md-heading">
TL;DR
&lt;a class="md-heading-anchor" href="#tldr" aria-label="Link to TL;DR">#&lt;/a>
&lt;/h2>
&lt;p>&lt;strong>Symptom:&lt;/strong> a batch stuck in &lt;code>Awaiting&lt;/code> forever. The batch it was waiting on already Succeeded. The job inside it sits in the &lt;code>created&lt;/code> set and never gets a state applied.&lt;/p>
&lt;p>&lt;strong>Cause:&lt;/strong> a continuation created &lt;em>nested inside&lt;/em> an enclosing batch, whose antecedent was created with the &lt;em>static&lt;/em> &lt;code>BatchJob.StartNew&lt;/code>. The antecedent commits and starts running immediately. The continuation does not exist in storage until the enclosing batch commits, which can be hundreds of milliseconds later. If the antecedent finishes inside that window, the wakeup is lost, and nothing retries it.&lt;/p>
&lt;p>&lt;strong>Fix:&lt;/strong> one word.&lt;/p>
&lt;div class="code-block" data-lang="diff">&lt;span class="code-lang" aria-hidden="true">diff&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gd">-var enrichmentBatch = BatchJob.StartNew(enrich =&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+var enrichmentBatch = batch.StartNew(enrich =&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>&lt;strong>Go grep your logs for this.&lt;/strong> Warn level, from &lt;code>Hangfire.Batches.BatchContinuationsSupportAttribute&lt;/code>:&lt;/p>
&lt;div class="code-block" data-lang="text">&lt;span class="code-lang" aria-hidden="true">text&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Could not start a continuation batch &amp;#39;&amp;lt;batchId&amp;gt;&amp;#39; for batch &amp;#39;&amp;lt;batchId&amp;gt;&amp;#39;: it does not exist or about to expire&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>If that string is in there, this is happening to you right now.&lt;/p>
&lt;p>One caveat before any of the rest. Hangfire.Pro is a commercial, closed source product. Everything below about its internals came out of decompiling the shipped assembly, so it is implementation detail and it can change between releases. Version under test: &lt;strong>Hangfire.Pro 3.0.5&lt;/strong>, Hangfire.Core 1.8.23, SQL Server storage.&lt;/p>
&lt;h2 id="what-it-looks-like-in-the-dashboard" class="md-heading">
What it looks like in the dashboard
&lt;a class="md-heading-anchor" href="#what-it-looks-like-in-the-dashboard" aria-label="Link to What it looks like in the dashboard">#&lt;/a>
&lt;/h2>
&lt;p>Batches, then Awaiting Batches. There is a batch that has been sitting there for two days.&lt;/p>
&lt;p>Open it. The &lt;strong>Created&lt;/strong> tab lists the &lt;code>ValidationJob&lt;/code> that never went anywhere. Now go find the enrichment batch it was waiting on.&lt;/p>
&lt;p>Succeeded. Hours ago.&lt;/p>
&lt;p>Those two facts cannot both be right. A continuation still waiting, on something that already finished.&lt;/p>
&lt;h2 id="the-clue-i-had-the-whole-time" class="md-heading">
The clue I had the whole time
&lt;a class="md-heading-anchor" href="#the-clue-i-had-the-whole-time" aria-label="Link to The clue I had the whole time">#&lt;/a>
&lt;/h2>
&lt;p>There is exactly one log line for this, and it names the batch id:&lt;/p>
&lt;div class="code-block" data-lang="text">&lt;span class="code-lang" aria-hidden="true">text&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Could not start a continuation batch &amp;#39;...&amp;#39; for batch &amp;#39;...&amp;#39;: it does not exist or about to expire&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>It is at Warn, from Hangfire&amp;rsquo;s own namespace, and that is the whole problem. Hangfire is chatty at Info, so the standard move is to push &lt;code>Hangfire.*&lt;/code> up to Error or drop it on the floor. We had done that. Sensible at the time.&lt;/p>
&lt;p>So the library had been telling us exactly which batch was broken, by id, for months, into a filter.&lt;/p>
&lt;p>I am not going to pretend that felt good.&lt;/p>
&lt;p>If you read nothing else here, go run that grep. It takes a minute and it either finds nothing or it ruins your afternoon in a useful way.&lt;/p>
&lt;h2 id="batches-quickly-for-people-who-have-only-used-plain-hangfire" class="md-heading">
Batches, quickly, for people who have only used plain Hangfire
&lt;a class="md-heading-anchor" href="#batches-quickly-for-people-who-have-only-used-plain-hangfire" aria-label="Link to Batches, quickly, for people who have only used plain Hangfire">#&lt;/a>
&lt;/h2>
&lt;p>Plain Hangfire gives you &lt;code>BackgroundJob.Enqueue&lt;/code> and &lt;code>BackgroundJob.ContinueJobWith&lt;/code>. A job, and a job that runs after a job.&lt;/p>
&lt;p>Hangfire.Pro adds batches. A batch is a group of jobs treated as one unit, so you can say &amp;ldquo;when all of these finish, do this&amp;rdquo; without tracking individual job ids. &lt;code>BatchJob.StartNew&lt;/code> makes one. &lt;code>BatchJob.ContinueBatchWith&lt;/code> makes a batch that waits on another batch. Batches nest, and a parent is not done until its children are.&lt;/p>
&lt;p>Here is the part that matters later: Pro has no tables of its own. On SQL Server a batch is a pile of rows in the generic &lt;code>Hash&lt;/code>, &lt;code>Set&lt;/code> and &lt;code>List&lt;/code> tables, keyed by convention.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Key&lt;/th>
&lt;th>Table&lt;/th>
&lt;th>Holds&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>batch:{id}&lt;/code>&lt;/td>
&lt;td>Hash&lt;/td>
&lt;td>&lt;code>CreatedAt&lt;/code>, &lt;code>Description&lt;/code>, and &lt;code>ParentId&lt;/code> if nested&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:state&lt;/code>&lt;/td>
&lt;td>Hash&lt;/td>
&lt;td>single field &lt;code>Data&lt;/code>, current state as JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:states&lt;/code>&lt;/td>
&lt;td>List&lt;/td>
&lt;td>state history&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:created&lt;/code> / &lt;code>:pending&lt;/code> / &lt;code>:processing&lt;/code> / &lt;code>:succeeded&lt;/code> / &lt;code>:finished&lt;/code>&lt;/td>
&lt;td>Set&lt;/td>
&lt;td>job ids by state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:created:batches&lt;/code> and the same &lt;code>:batches&lt;/code> suffixes&lt;/td>
&lt;td>Set&lt;/td>
&lt;td>nested batch ids by state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:continuations&lt;/code>&lt;/td>
&lt;td>Hash&lt;/td>
&lt;td>continuations with non default options&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batch:{id}:continuations:succeeded&lt;/code> / &lt;code>:finished&lt;/code>&lt;/td>
&lt;td>Set&lt;/td>
&lt;td>registered continuation batch ids&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>batches:started&lt;/code> / &lt;code>:awaiting&lt;/code> / &lt;code>:awaiting-job&lt;/code> / &lt;code>:completed&lt;/code> / &lt;code>:succeeded&lt;/code> / &lt;code>:deleted&lt;/code>&lt;/td>
&lt;td>Set&lt;/td>
&lt;td>the per state indexes the dashboard reads, &lt;code>Score&lt;/code> is the Unix seconds timestamp of entry&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>States are &lt;code>Created&lt;/code>, &lt;code>Started&lt;/code>, &lt;code>Awaiting&lt;/code>, &lt;code>Completed&lt;/code>, &lt;code>Succeeded&lt;/code>, &lt;code>Deleted&lt;/code>. Learn that key layout. It is the only reason the SQL at the end of this post is possible.&lt;/p>
&lt;h2 id="the-code" class="md-heading">
The code
&lt;a class="md-heading-anchor" href="#the-code" aria-label="Link to The code">#&lt;/a>
&lt;/h2>
&lt;p>Simplified, but this is the shape. One outer batch holds the enrichment and validation pipeline plus some unrelated shipping, tax and inventory work, and a single continuation notifies downstream once all of it is done.&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">EnqueueOrderPostProcessing&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OrderPayload&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">batchId&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartNew&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">batch&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">EnqueueEnrichmentAndValidation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">batch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ... several more batches created here for shipping, tax and inventory work ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">shippingBatch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartNew&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">b&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">ShippingJobs&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Recalculate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">taxBatch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContinueBatchWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">shippingBatch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">TaxJobs&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Recalculate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContinueBatchWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">taxBatch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">InventoryJobs&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">x&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Reserve&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContinueBatchWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">batchId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">batch&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">INotifier&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">n&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">NotifyDownstream&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">batchId&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">private&lt;/span> &lt;span class="k">void&lt;/span> &lt;span class="n">EnqueueEnrichmentAndValidation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IBatchAction&lt;/span> &lt;span class="n">batch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">OrderPayload&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">foreach&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kt">var&lt;/span> &lt;span class="n">chunk&lt;/span> &lt;span class="k">in&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">OrderIds&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Chunk&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BatchSize&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">chunkList&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">chunk&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ToList&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// BUG IS HERE: static BatchJob.StartNew commits immediately, and these jobs start running now&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">enrichmentBatch&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">BatchJob&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StartNew&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">enrich&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enrich&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">PricingJob&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">j&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunkList&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_featureFlags&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">IsEnabled&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">FeatureFlag&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">FraudCheck&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="c1">// a database round trip, per chunk&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enrich&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">FraudCheckJob&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">j&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunkList&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...but this continuation is a CHILD of the outer batch, so it is not written to&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// storage until the enclosing BatchJob.StartNew commits&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContinueBatchWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">enrichmentBatch&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">validate&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">validate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Enqueue&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="n">ValidationJob&lt;/span>&lt;span class="p">&amp;gt;(&lt;/span>&lt;span class="n">j&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunkList&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Look at those two calls sitting next to each other. &lt;code>BatchJob.StartNew&lt;/code> and &lt;code>batch.ContinueBatchWith&lt;/code>.&lt;/p>
&lt;p>They read like siblings.&lt;/p>
&lt;h2 id="what-i-thought-it-was-first" class="md-heading">
What I thought it was first
&lt;a class="md-heading-anchor" href="#what-i-thought-it-was-first" aria-label="Link to What I thought it was first">#&lt;/a>
&lt;/h2>
&lt;p>Chunking. Obviously chunking.&lt;/p>
&lt;p>The failures clustered on bigger orders, bigger orders meant more chunks, and more chunks meant more of everything. I spent a while convinced the loop was racing itself, that two chunks were somehow stepping on each other&amp;rsquo;s batch ids. Added logging around the chunk boundaries. Found nothing, because there was nothing.&lt;/p>
&lt;p>Then the feature flag, because &lt;code>IsEnabled&lt;/code> does a database round trip and it is inside the loop. Wrong again, but closer than I knew at the time. The flag call was not the cause. It was making the window wider.&lt;/p>
&lt;p>What finally moved it was reading the warning properly. Not &amp;ldquo;some continuation failed&amp;rdquo; but &lt;em>&amp;ldquo;it does not exist&amp;rdquo;&lt;/em>. Present tense. Storage was asked for a batch and there was nothing there.&lt;/p>
&lt;p>That is a strange thing to be true about a batch you are in the middle of creating. Unless it has not been created yet.&lt;/p>
&lt;h2 id="two-ways-to-make-a-batch" class="md-heading">
Two ways to make a batch
&lt;a class="md-heading-anchor" href="#two-ways-to-make-a-batch" aria-label="Link to Two ways to make a batch">#&lt;/a>
&lt;/h2>
&lt;p>This is the whole post, so it is worth being slow about.&lt;/p>
&lt;p>&lt;strong>&lt;code>BatchJob.StartNew(...)&lt;/code>&lt;/strong>, the static facade, goes to &lt;code>BatchJobClient.Create&lt;/code>. It opens its own storage connection, takes a distributed lock on the new batch id, writes everything, commits. Immediately. The jobs inside are enqueued and a worker can pick one up the instant the call returns. By the time your next line of C# runs, that batch might already be executing.&lt;/p>
&lt;p>&lt;strong>&lt;code>batch.StartNew(...)&lt;/code> and &lt;code>batch.ContinueBatchWith(...)&lt;/code>&lt;/strong>, called on the &lt;code>IBatchAction&lt;/code> you are currently inside, create a &lt;strong>nested&lt;/strong> batch. &lt;code>BatchAction.Create&lt;/code> stamps &lt;code>ParentId&lt;/code> into the hash and queues the child in memory. Then in &lt;code>BatchFactory.Create&lt;/code>:&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Transaction&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">SetRangeInHash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;batch:&amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">BatchId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Batch&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(!&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ContainsKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;ParentId&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_stateMachine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ApplyState&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Read the condition. A nested batch does not get its state applied there. All of it goes into the &lt;strong>outer batch&amp;rsquo;s transaction&lt;/strong>, and none of it exists in storage until the enclosing &lt;code>BatchJob.StartNew&lt;/code> returns.&lt;/p>
&lt;p>Same verb. Same shape where you type it. Completely different commit semantics, and nothing in the type system is going to mention it.&lt;/p>
&lt;h3 id="one-thing-that-is-not-obvious" class="md-heading">
One thing that is not obvious
&lt;a class="md-heading-anchor" href="#one-thing-that-is-not-obvious" aria-label="Link to One thing that is not obvious">#&lt;/a>
&lt;/h3>
&lt;p>An aside, because it cost me an afternoon: &lt;code>IBatchAction&lt;/code> only declares &lt;code>BatchId&lt;/code>. The reason &lt;code>batch.StartNew(...)&lt;/code> compiles at all is that it inherits &lt;code>IBackgroundJobClient&lt;/code>, &lt;code>IBatchJobClient&lt;/code> and &lt;code>IBackgroundJobClientV2&lt;/code>. You will not work that out from the interface declaration.&lt;/p>
&lt;h2 id="pro-is-not-naive-about-this" class="md-heading">
Pro is not naive about this
&lt;a class="md-heading-anchor" href="#pro-is-not-naive-about-this" aria-label="Link to Pro is not naive about this">#&lt;/a>
&lt;/h2>
&lt;p>Worth saying, because the obvious version of this race is handled. When a batch is created in &lt;code>BatchAwaitingState&lt;/code>, &lt;code>BatchContinuationsSupportAttribute.OnStateElection&lt;/code> calls &lt;code>AddContinuation&lt;/code>. Paraphrased from the decompiled source:&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">using&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">AcquireDistributedBatchLock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parentBatchId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">TimeSpan&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">FromMinutes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="p">)))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">using&lt;/span> &lt;span class="nn">var&lt;/span> &lt;span class="n">tx&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CreateWriteTransaction&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">AddToSet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;batch:&amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">parentBatchId&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34;:continuations:succeeded&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">BatchId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">var&lt;/span> &lt;span class="n">parentState&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">GetBatchState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parentBatchId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">parentState&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">InvalidOperationException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Can not find antecedent batch ... to create a continuation.&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">parentState&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">IsFinal&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">CandidateState&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">ShouldStartContinuation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parentState&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">options&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">?&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">BatchStartedState&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">Reason&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;Antecedent batch was already finished&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">:&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="n">BatchDeletedState&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">Reason&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;Continuation condition was not satisfied&amp;#34;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Commit&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// registration is committed here, separately, and the lock is released&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>It takes a lock on the antecedent. It re-reads the antecedent&amp;rsquo;s state under that lock. If the antecedent already finished, it starts the continuation right there rather than waiting on a wakeup that is never coming.&lt;/p>
&lt;p>Somebody thought about this.&lt;/p>
&lt;p>The catch is what &lt;code>IsFinal&lt;/code> actually sets. A &lt;strong>candidate state&lt;/strong>. The continuation&amp;rsquo;s own &lt;code>BatchAwaitingState&lt;/code> row gets written afterwards by &lt;code>BatchStateMachine.ApplyState&lt;/code>, into the outer batch&amp;rsquo;s transaction.&lt;/p>
&lt;p>Which is still open.&lt;/p>
&lt;h2 id="the-lost-wakeup" class="md-heading">
The lost wakeup
&lt;a class="md-heading-anchor" href="#the-lost-wakeup" aria-label="Link to The lost wakeup">#&lt;/a>
&lt;/h2>
&lt;p>The other half. When the antecedent goes final, &lt;code>OnStateApplied&lt;/code> calls &lt;code>ExecuteContinuationsIfExist&lt;/code>, which walks the continuation sets and calls into the state changer for each id:&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ChangeState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="n">BatchStateChangeContext&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Storage&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Connection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">continuationBatchId&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="n">BatchStartedState&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">Reason&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;Antecedent batch has finished&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">BatchAwaitingState&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">StateName&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Warn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Could not start a continuation batch &amp;#39;&amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">continuationBatchId&lt;/span> &lt;span class="p">+&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;&amp;#39; for batch &amp;#39;&amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">BatchId&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="s">&amp;#34;&amp;#39;: it does not exist or about to expire&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>And &lt;code>BatchStateChanger.ChangeState&lt;/code> opens with this:&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">var&lt;/span> &lt;span class="n">hash&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Connection&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">GetAllEntriesFromHash&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;batch:&amp;#34;&lt;/span> &lt;span class="p">+&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">BatchId&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">hash&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="kc">null&lt;/span> &lt;span class="p">||&lt;/span> &lt;span class="n">hash&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Count&lt;/span> &lt;span class="p">==&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>No hash, no batch. Return null, log a warning, move on.&lt;/p>
&lt;h3 id="the-sequence" class="md-heading">
The sequence
&lt;a class="md-heading-anchor" href="#the-sequence" aria-label="Link to The sequence">#&lt;/a>
&lt;/h3>
&lt;p>Put the halves together:&lt;/p>
&lt;ol>
&lt;li>Under the lock on the enrichment batch, the validation continuation is registered into &lt;code>batch:{enrichment}:continuations:succeeded&lt;/code> and committed. Enrichment is not final yet, so the candidate state stays &lt;code>Awaiting&lt;/code>. &lt;strong>The lock is released.&lt;/strong>&lt;/li>
&lt;li>The outer transaction keeps building. More chunks. A feature flag lookup per chunk. Three more batch creations for shipping, tax and inventory. Tens to hundreds of milliseconds of database round trips.&lt;/li>
&lt;li>The enrichment jobs, running since step 1, finish. Enrichment goes Succeeded. &lt;code>ExecuteContinuationsIfExist&lt;/code> finds the validation batch id in the set and calls &lt;code>ChangeState&lt;/code>.&lt;/li>
&lt;li>&lt;code>batch:{validation}&lt;/code> is not in storage. It is inside the outer transaction, uncommitted. &lt;code>ChangeState&lt;/code> returns null, the warning fires, and &lt;strong>nothing ever retries it.&lt;/strong> Continuations fire once, off the antecedent&amp;rsquo;s state transition, and the antecedent is now final forever.&lt;/li>
&lt;li>The outer transaction commits and writes the validation batch as &lt;code>Awaiting&lt;/code>. It stays that way permanently, &lt;code>ValidationJob&lt;/code> sitting in its &lt;code>created&lt;/code> set having never had a state applied.&lt;/li>
&lt;/ol>
&lt;p>The registration and the wakeup are serialized against each other by a lock on the antecedent. But the lock comes off before the continuation&amp;rsquo;s own state row is committed.&lt;/p>
&lt;p>That gap is the bug.&lt;/p>
&lt;p>And it is not a microsecond gap. It is however long the rest of the enclosing batch build takes. Ours was a per chunk feature flag call plus three more batch creations, which is why this showed up sometimes instead of never. A tighter loop hides it for years. A slower database makes it constant.&lt;/p>
&lt;p>So my chunking theory was not wrong, exactly. It was one layer off. More chunks did cause it, but not by racing each other. They just made the window longer.&lt;/p>
&lt;h2 id="three-outcomes-that-look-the-same-and-are-not" class="md-heading">
Three outcomes that look the same and are not
&lt;a class="md-heading-anchor" href="#three-outcomes-that-look-the-same-and-are-not" aria-label="Link to Three outcomes that look the same and are not">#&lt;/a>
&lt;/h2>
&lt;p>Two of these are not this bug, and I checked the wrong one first.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>What you see&lt;/th>
&lt;th>What it means&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Continuation stuck in &lt;strong>Awaiting&lt;/strong> forever, antecedent already final&lt;/td>
&lt;td>The lost wakeup above&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Continuation in &lt;strong>Deleted&lt;/strong>, reason &amp;ldquo;Continuation condition was not satisfied&amp;rdquo;&lt;/td>
&lt;td>The antecedent finished, but not in a state matching the continuation options&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Continuation in &lt;strong>Started&lt;/strong>, reason &amp;ldquo;Antecedent batch was already finished&amp;rdquo;&lt;/td>
&lt;td>The race Pro does handle, caught under the lock&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The middle row deserves its own paragraph, because it catches a lot of people and it is not a race at all. &lt;code>ContinueBatchWith&lt;/code> defaults to &lt;code>BatchContinuationOptions.OnlyOnSucceededState&lt;/code>. One permanently failed job anywhere in the antecedent means the antecedent never reaches Succeeded, and your continuation is not stuck. It is &lt;strong>deleted&lt;/strong>. Quietly, with a reason string you only see if you open that batch in the dashboard.&lt;/p>
&lt;p>If you are hunting a continuation that never ran and it is not in Awaiting, look in Deleted before you assume you have a race.&lt;/p>
&lt;h2 id="the-fix" class="md-heading">
The fix
&lt;a class="md-heading-anchor" href="#the-fix" aria-label="Link to The fix">#&lt;/a>
&lt;/h2>
&lt;div class="code-block" data-lang="diff">&lt;span class="code-lang" aria-hidden="true">diff&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="cl">&lt;span class="gd">- var enrichmentBatch = BatchJob.StartNew(enrich =&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ var enrichmentBatch = batch.StartNew(enrich =&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Now the antecedent and the continuation are both nested children of the same outer batch, created in one transaction. &lt;code>BatchStateMachine.SortContinuations&lt;/code> handles exactly this:&lt;/p>
&lt;div class="code-block" data-lang="csharp">&lt;span class="code-lang" aria-hidden="true">csharp&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-csharp" data-lang="csharp">&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">createdItem&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="n">BatchedBatch&lt;/span> &lt;span class="n">batchedBatch&lt;/span> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">batchedBatch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">InitialState&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="n">BatchAwaitingState&lt;/span> &lt;span class="n">awaiting&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">createdBatchIds&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">awaiting&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;amp;&amp;amp;&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="n">seenBatchIds&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">awaiting&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">AddBatchContinuation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">awaiting&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">batchedBatch&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// deferred until after its antecedent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>A continuation whose antecedent is a sibling in the same batch gets deferred until after that antecedent&amp;rsquo;s state is applied. The enrichment jobs are enqueued in the same commit, so they cannot start before the continuation exists.&lt;/p>
&lt;p>The window closes because there is nothing left in it.&lt;/p>
&lt;p>The trade off is that the enrichment jobs start marginally later, after the outer commit rather than during it. Which is what you wanted anyway.&lt;/p>
&lt;h3 id="the-rule-worth-remembering" class="md-heading">
The rule worth remembering
&lt;a class="md-heading-anchor" href="#the-rule-worth-remembering" aria-label="Link to The rule worth remembering">#&lt;/a>
&lt;/h3>
&lt;p>Do not mix batch scopes.&lt;/p>
&lt;p>Either both the antecedent and the continuation are top level, each a static &lt;code>BatchJob.*&lt;/code> call committing independently. That is safe, because &lt;code>AddContinuation&lt;/code> re-checks the antecedent under a lock and the antecedent is already durable by then. Or both are nested in the same batch, which is the case &lt;code>SortContinuations&lt;/code> was written for. The broken shape is specifically a nested continuation whose antecedent is an external, already committed, already running batch.&lt;/p>
&lt;p>Hangfire.Pro 2.1.0&amp;rsquo;s release notes list &amp;ldquo;Continuations now work properly, when antecedent job/batch and continuation have the same batch&amp;rdquo; as a fixed item. The same batch pattern is something the vendor deliberately made work.&lt;/p>
&lt;h2 id="digging-out-the-wreckage" class="md-heading">
Digging out the wreckage
&lt;a class="md-heading-anchor" href="#digging-out-the-wreckage" aria-label="Link to Digging out the wreckage">#&lt;/a>
&lt;/h2>
&lt;p>Everything in this section is a last resort. It is for a system that already has orphans in it, not maintenance, and raw SQL bypasses the &lt;code>batch:{id}:lock&lt;/code> distributed lock, so do it with the job servers stopped or in a genuinely quiet window. Default schema is &lt;code>HangFire&lt;/code>.&lt;/p>
&lt;h3 id="finding-the-orphans" class="md-heading">
Finding the orphans
&lt;a class="md-heading-anchor" href="#finding-the-orphans" aria-label="Link to Finding the orphans">#&lt;/a>
&lt;/h3>
&lt;p>Awaiting batches, with the state of whatever each one is waiting on:&lt;/p>
&lt;div class="code-block" data-lang="sql">&lt;span class="code-lang" aria-hidden="true">sql&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BatchId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">DATEADD&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SECOND&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CAST&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Score&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BIGINT&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;19700101&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">AwaitingSinceUtc&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">pid&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">COALESCE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ps&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;*** PARENT GONE ***&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ParentState&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Set&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s2&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;batch:&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;:created&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">CreatedJobs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Set&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Hash&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;batch:&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;:state&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Field&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Data&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CROSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">APPLY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSON_VALUE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">st&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;$.ParentBatchId&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Hash&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ps&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ps&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;batch:&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pid&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">ParentBatchId&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;:state&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ps&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Field&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;Data&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;batches:awaiting&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Score&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Read those results carefully, because this is where you can do real damage. A batch whose parent is gone, or whose parent is already &lt;code>BatchSucceededState&lt;/code>, &lt;code>BatchCompletedState&lt;/code> or &lt;code>BatchDeletedState&lt;/code>, is orphaned and will never start. A batch whose parent is &lt;code>BatchStartedState&lt;/code> is legitimately waiting and is doing its job.&lt;/p>
&lt;p>Deleting everything in Awaiting will destroy live work. Do not do that.&lt;/p>
&lt;h3 id="getting-the-payload-back" class="md-heading">
Getting the payload back
&lt;a class="md-heading-anchor" href="#getting-the-payload-back" aria-label="Link to Getting the payload back">#&lt;/a>
&lt;/h3>
&lt;p>The payload is still readable, so you can recover what those jobs were meant to do before cleaning anything up:&lt;/p>
&lt;div class="code-block" data-lang="sql">&lt;span class="code-lang" aria-hidden="true">sql&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DISTINCT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Arguments&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Set&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HangFire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Job&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">j&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TRY_CAST&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">Value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.[&lt;/span>&lt;span class="k">Key&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;batch:%:created&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">j&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">InvocationData&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;%ValidationJob%&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;h3 id="cleaning-up-without-deleting" class="md-heading">
Cleaning up without deleting
&lt;a class="md-heading-anchor" href="#cleaning-up-without-deleting" aria-label="Link to Cleaning up without deleting">#&lt;/a>
&lt;/h3>
&lt;p>For the cleanup itself, do not hard delete. Backdate &lt;code>ExpireAt&lt;/code> on the batch&amp;rsquo;s &lt;code>Hash&lt;/code>, &lt;code>Set&lt;/code> and &lt;code>List&lt;/code> rows and on its row in &lt;code>batches:awaiting&lt;/code>, then let Hangfire&amp;rsquo;s own &lt;code>ExpirationManager&lt;/code> sweep them. It runs &lt;code>delete top (@count) T ... where ExpireAt &amp;lt; @now&lt;/code>, and &lt;code>JobExpirationCheckInterval&lt;/code> defaults to 30 minutes.&lt;/p>
&lt;p>There is a nice property in doing it that way. It self corrects. If you get one wrong and mark a batch that is actually still alive, its next state change calls &lt;code>PersistBatch&lt;/code>, which clears &lt;code>ExpireAt&lt;/code> again and rescues it from your cleanup script.&lt;/p>
&lt;p>Leave the orphaned &lt;code>Job&lt;/code> rows alone. &lt;code>CoreBackgroundJobFactory&lt;/code> creates every job with &lt;code>ExpireAt&lt;/code> set to creation plus 30 days, and a job in a batch that never started never has a state applied, so that expiry never gets cleared. They age out on their own, and they are not sitting in a queue in the meantime.&lt;/p>
&lt;h2 id="prior-art-such-as-it-is" class="md-heading">
Prior art, such as it is
&lt;a class="md-heading-anchor" href="#prior-art-such-as-it-is" aria-label="Link to Prior art, such as it is">#&lt;/a>
&lt;/h2>
&lt;p>I went looking for other people hitting this before I opened the decompiler. There is less out there than you would expect.&lt;/p>
&lt;p>The closest is a forum thread, &lt;a href="https://discuss.hangfire.io/t/batch-continuation-stuck/3113"target="_blank" rel="noopener noreferrer">Batch Continuation Stuck&lt;/a>. The reporter got impressively close: they narrowed it to &amp;ldquo;Job 1 in Batch 1 starts at the exact same time that Job 2 in Batch 2 is created&amp;rdquo;, with database timestamps colliding at the millisecond. Nobody from the project ever answered. No resolution.&lt;/p>
&lt;p>&lt;a href="https://github.com/HangfireIO/Hangfire/issues/2080"target="_blank" rel="noopener noreferrer">HangfireIO/Hangfire#2080&lt;/a>, &amp;ldquo;Hangfire.Pro multiple continuations - batches can be stuck in the Started state&amp;rdquo;, is open, but it is about jobs stuck Enqueued and fetched job counts. Different animal. &lt;a href="https://github.com/HangfireIO/Hangfire/issues/1035"target="_blank" rel="noopener noreferrer">HangfireIO/Hangfire#1035&lt;/a>, &amp;ldquo;Continuations stuck in Awaiting State&amp;rdquo;, is a 2018 report against Hangfire 1.6.17 with a screenshot and not much else.&lt;/p>
&lt;p>Pro 3.0.1/2.3.3 and 3.0.2/2.3.4 did ship nested batch fixes: empty nested batches remaining Started, nested batch jobs not triggering batch updates, a lock ordering deadlock when attaching nested batch continuations. None of them is this. 3.0.5 is a CSS only patch.&lt;/p>
&lt;p>There is a decent reason for the silence, and it is not that nobody hit this. Pro is closed source and paid, so Pro bugs go to a support email rather than a public tracker. Whatever conversations happened, happened in someone&amp;rsquo;s inbox. Searching that warning string finds nothing because the people who hit it had nowhere public to put it, which is its own small argument about paid closed source infrastructure, and I will leave it there.&lt;/p>
&lt;h2 id="what-i-actually-take-from-this" class="md-heading">
What I actually take from this
&lt;a class="md-heading-anchor" href="#what-i-actually-take-from-this" aria-label="Link to What I actually take from this">#&lt;/a>
&lt;/h2>
&lt;p>The library is not doing anything indefensible. It guards the obvious race on purpose, with a lock and a re-read, and it guards it correctly. The failure only surfaces at the seam between two APIs that read as equivalent at the call site and are not underneath.&lt;/p>
&lt;p>&lt;code>BatchJob.StartNew&lt;/code> and &lt;code>batch.StartNew&lt;/code> differ by four characters. One means &amp;ldquo;write this now and let the workers have it&amp;rdquo;. The other means &amp;ldquo;add this to the transaction I am already building&amp;rdquo;. Nothing where you type it tells you which is which, and both compile, and both look completely reasonable in review. I reviewed it. It looked completely reasonable.&lt;/p>
&lt;p>Anyway. Go grep for &lt;code>Could not start a continuation batch&lt;/code>.&lt;/p></description></item><item><title>Finding It Isn't Fixing It</title><link>https://houdeshell.dev/post/2026-08-20_finding-it-isnt-fixing-it/</link><pubDate>Thu, 20 Aug 2026 09:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-08-20_finding-it-isnt-fixing-it/</guid><description>&lt;p>GitHub found the broken component about three hours into the August 17 outage. They were another four hours and thirty-nine minutes from done.&lt;/p>
&lt;p>The &lt;a href="https://www.githubstatus.com/incidents/zkxwbgr0cnmx"target="_blank" rel="noopener noreferrer">status timeline&lt;/a> has it at 16:36 UTC: &amp;ldquo;identified the problematic component and have taken corrective actions.&amp;rdquo; Then 17:30, Git Operations degraded again. 19:13, they partially disabled authentication token retries. Resolved 21:15. Seven hours forty-seven minutes, and most of it came after they knew what was wrong.&lt;/p>
&lt;p>Nobody deployed anything. From &lt;a href="https://github.blog/news-insights/company-news/the-august-17-outage-and-the-work-ahead/"target="_blank" rel="noopener noreferrer">the writeup&lt;/a>, about this one and the one before it: &amp;ldquo;Neither outage was caused by a code or configuration change. Both incidents were capacity failures at their core.&amp;rdquo; Load balancers in Central US saturated on a new traffic peak, an Istio sidecar hit its concurrency limit without scaling, and four HAProxy nodes went with it.&lt;/p>
&lt;p>Then the retries piled on. The Copilot token service normally sits around 7-9K RPS. During the incident it took 70-100K. That&amp;rsquo;s not user traffic. That&amp;rsquo;s retry logic doing what it was told.&lt;/p>
&lt;p>The growth in that post is the context. 1.4 billion commits in April, 2.9 billion in August. Azure went from 12% of platform load in May to 58%. Move that fast and something in the stack is going to be the piece that didn&amp;rsquo;t scale.&lt;/p>
&lt;p>&lt;strong>Coming back up is a different engineering problem than not going down, and it usually gets less attention.&lt;/strong>&lt;/p>
&lt;p>Prevention gets the budget: headroom, redundancy, review gates. The recovery path gets a quarterly drill where everything cooperates. GitHub&amp;rsquo;s remediation list is mostly the second category. Retry budgets, variable timeouts, isolating critical systems, removing shared dependencies.&lt;/p>
&lt;p>Measure how long it takes you to come back. That number matters as much as the one on your status page.&lt;/p></description></item><item><title>Davit: A Native UI for Apple's Container Tool</title><link>https://houdeshell.dev/post/2026-06-15_davit-native-container-ui/</link><pubDate>Mon, 15 Jun 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-06-15_davit-native-container-ui/</guid><description>&lt;p>I&amp;rsquo;ve spent the last week living in &lt;a href="https://houdeshell.dev/post/2026-06-12_macos-container-tool/">Apple&amp;rsquo;s container tool&lt;/a> from the terminal. It&amp;rsquo;s good. But some days I want a window, a list of what&amp;rsquo;s running, and a button. &lt;a href="https://github.com/wouterdebie/davit"target="_blank" rel="noopener noreferrer">Davit&lt;/a> is that window.&lt;/p>
&lt;figure class="post-figure">
&lt;img loading="lazy" src="https://houdeshell.dev/post/2026-06-15_davit-native-container-ui/davit-dashboard.png" alt="Davit’s dashboard showing container services stopped, with a sidebar for Containers, Images, Volumes, Networks, and Machines" title="Davit&amp;#39;s dashboard — container services stopped">
&lt;figcaption>Davit's dashboard — container services stopped&lt;/figcaption>
&lt;/figure>
&lt;p>It&amp;rsquo;s a native SwiftUI app. No Electron, no web view stuffed in a title bar. That alone put it ahead of most of the desktop container tooling I&amp;rsquo;ve tried.&lt;/p>
&lt;p>The part I like: it doesn&amp;rsquo;t shell out to the &lt;code>container&lt;/code> binary and scrape the output. &lt;strong>It talks to the same platform the CLI does, straight over XPC.&lt;/strong> So the dashboard, the live CPU chart, the container list, the log tail, the filesystem browser all read from the source instead of parsing text a human was supposed to read.&lt;/p>
&lt;p>The screenshot above is the first thing I saw, and it made me laugh. &amp;ldquo;Container services stopped,&amp;rdquo; with a Start button. That&amp;rsquo;s the &lt;code>container system start&lt;/code> dance from my last post, except now it&amp;rsquo;s a button I don&amp;rsquo;t have to remember.&lt;/p>
&lt;p>Install is a one-liner:&lt;/p>
&lt;div class="terminal">&lt;div class="terminal-bar" aria-hidden="true">&lt;span class="terminal-dot dot-red">&lt;/span>&lt;span class="terminal-dot dot-yellow">&lt;/span>&lt;span class="terminal-dot dot-green">&lt;/span>&lt;span class="terminal-title">zsh&lt;/span>&lt;/div>&lt;pre class="terminal-body">&lt;code>&lt;span class="t-out">brew install wouterdebie/tap/davit&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>If you don&amp;rsquo;t already have Apple&amp;rsquo;s container platform, Davit can install it for you.&lt;/p>
&lt;p>It&amp;rsquo;s not GUI-only, either. The same binary does &lt;code>davit run&lt;/code>, &lt;code>davit build&lt;/code>, and a full &lt;code>davit compose up&lt;/code>/&lt;code>down&lt;/code>/&lt;code>logs&lt;/code>/&lt;code>exec&lt;/code> with docker-compatible flags. So I can click around when I want to look, and drop to the same tool in a script when I don&amp;rsquo;t.&lt;/p>
&lt;p>Apple silicon, and you&amp;rsquo;ll want the container platform on the box. It&amp;rsquo;s young, and it&amp;rsquo;s MIT. Worth a look.&lt;/p></description></item><item><title>Helpful macOS Container Tool Commands</title><link>https://houdeshell.dev/post/2026-06-13_helpful-container-commands/</link><pubDate>Sat, 13 Jun 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-06-13_helpful-container-commands/</guid><description>&lt;p>I&amp;rsquo;ve been living in &lt;a href="https://houdeshell.dev/post/2026-06-12_macos-container-tool/">Apple&amp;rsquo;s container tool&lt;/a> for the last few days. A handful of commands come up a lot, and other team members have asked how, so I&amp;rsquo;m writing them down. Here are the three I reach for most.&lt;/p>
&lt;ul>
&lt;li>&lt;code>container machine run --root&lt;/code> runs your default machine as root.&lt;/li>
&lt;li>&lt;code>usermod -aG sudo $user&lt;/code> adds your user to the sudoers group once you&amp;rsquo;re root (install &lt;code>sudo&lt;/code> first).&lt;/li>
&lt;li>&lt;code>container machine logs&lt;/code> shows the logs for your default machine.&lt;/li>
&lt;/ul></description></item><item><title>Running Ubuntu on Apple's Container</title><link>https://houdeshell.dev/post/2026-06-12_ubuntu-on-apple-container/</link><pubDate>Fri, 12 Jun 2026 14:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-06-12_ubuntu-on-apple-container/</guid><description>&lt;p>After &lt;a href="https://houdeshell.dev/post/2026-06-12_macos-container-tool/">getting Apple&amp;rsquo;s &lt;code>container&lt;/code> running&lt;/a>, the thing I actually wanted was a normal Ubuntu box to poke around in. That took one more step than I expected: systemd.&lt;/p>
&lt;p>A container runs whatever its &lt;code>CMD&lt;/code> is as PID 1. The stock &lt;code>ubuntu:26.04&lt;/code> image doesn&amp;rsquo;t ship an init, so you boot into a bare shell and not much else. If you want services, units, and a machine that behaves like a real Ubuntu install, systemd has to be the thing running as init. So you build your own image.&lt;/p>
&lt;p>Here&amp;rsquo;s the Dockerfile. Most of it is the well-worn systemd-in-a-container cleanup: install systemd, then delete the units that make no sense inside a container so it doesn&amp;rsquo;t sit there trying to start hardware that isn&amp;rsquo;t there.&lt;/p>
&lt;div class="code-block" data-lang="dockerfile">&lt;span class="code-lang" aria-hidden="true">dockerfile&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> ubuntu:26.04&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> container container&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt update -y &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt upgrade -y &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt install -y systemd init&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nb">cd&lt;/span> /lib/systemd/system/sysinit.target.wants/&lt;span class="p">;&lt;/span> &lt;span class="k">for&lt;/span> i in *&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$i&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>systemd-tmpfiles-setup.service &lt;span class="o">]&lt;/span> &lt;span class="o">||&lt;/span> rm -f &lt;span class="nv">$i&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">done&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/multi-user.target.wants/*&lt;span class="p">;&lt;/span>&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /etc/systemd/system/*.wants/*&lt;span class="p">;&lt;/span>&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/local-fs.target.wants/*&lt;span class="p">;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/sockets.target.wants/*udev*&lt;span class="p">;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/sockets.target.wants/*initctl*&lt;span class="p">;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/basic.target.wants/*&lt;span class="p">;&lt;/span>&lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>rm -f /lib/systemd/system/anaconda.target.wants/*&lt;span class="p">;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">VOLUME&lt;/span> &lt;span class="p">[&lt;/span> &lt;span class="s2">&amp;#34;/sys/fs/cgroup&amp;#34;&lt;/span> &lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;/usr/sbin/init&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Build it, create a machine from it, and run:&lt;/p>
&lt;div class="terminal">&lt;div class="terminal-bar" aria-hidden="true">&lt;span class="terminal-dot dot-red">&lt;/span>&lt;span class="terminal-dot dot-yellow">&lt;/span>&lt;span class="terminal-dot dot-green">&lt;/span>&lt;span class="terminal-title">zsh&lt;/span>&lt;/div>&lt;pre class="terminal-body">&lt;code>&lt;span class="t-prompt">❯&lt;/span> &lt;span class="t-bin">container&lt;/span> &lt;span class="t-cmd">build -t local/devel -f devel.dockerfile&lt;/span>
&lt;span class="t-out">[+] Building 356.1s (7/7) FINISHED&lt;/span>
&lt;span class="t-out"> =&amp;gt; [resolver] fetching image docker.io/library/ubuntu:26.04&lt;/span>
&lt;span class="t-out"> =&amp;gt; [internal] load build definition from Dockerfile&lt;/span>
&lt;span class="t-out"> =&amp;gt; [linux/arm64 1/3] RUN apt update -y &amp;amp;&amp;amp; apt upgrade -y &amp;amp;&amp;amp; apt install -y systemd init&lt;/span>
&lt;span class="t-out"> =&amp;gt; [linux/arm64 2/3] RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do ... )&lt;/span>
&lt;span class="t-out"> =&amp;gt; exporting to oci image format&lt;/span>
&lt;span class="t-out">local/devel:latest&lt;/span>
&lt;span class="t-out">&lt;/span>
&lt;span class="t-prompt">❯&lt;/span> &lt;span class="t-bin">container&lt;/span> &lt;span class="t-cmd">machine create --name devel --set-default local/devel&lt;/span>
&lt;span class="t-out">devel&lt;/span>
&lt;span class="t-out">&lt;/span>
&lt;span class="t-prompt">❯&lt;/span> &lt;span class="t-bin">container&lt;/span> &lt;span class="t-cmd">machine run&lt;/span>
&lt;span class="t-out">chris.houdeshell@devel:/Users/chris.houdeshell$&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>That last line is the Ubuntu shell. You&amp;rsquo;re in.&lt;/p>
&lt;p>What the flags are doing, quickly:&lt;/p>
&lt;ul>
&lt;li>&lt;code>container build -t local/devel -f devel.dockerfile&lt;/code>: &lt;code>-t&lt;/code> tags the image as &lt;code>local/devel&lt;/code>; &lt;code>-f&lt;/code> points at the Dockerfile by name, since I didn&amp;rsquo;t name it &lt;code>Dockerfile&lt;/code>.&lt;/li>
&lt;li>&lt;code>container machine create --name devel --set-default local/devel&lt;/code>: builds a VM named &lt;code>devel&lt;/code> from that image. &lt;code>--set-default&lt;/code> makes it the machine everything else targets, so I don&amp;rsquo;t have to spell out the name on every command.&lt;/li>
&lt;li>&lt;code>container machine run&lt;/code>: boots the default machine and drops me into its shell.&lt;/li>
&lt;/ul>
&lt;p>One thing worth knowing: that build ran just shy of six minutes. The tool is young and the build path isn&amp;rsquo;t quick yet, so don&amp;rsquo;t assume it hung.&lt;/p></description></item><item><title>Apple's Container Tool</title><link>https://houdeshell.dev/post/2026-06-12_macos-container-tool/</link><pubDate>Fri, 12 Jun 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-06-12_macos-container-tool/</guid><description>&lt;p>Apple has a CLI for running Linux containers on macOS now. It&amp;rsquo;s called &lt;a href="https://github.com/apple/container"target="_blank" rel="noopener noreferrer">container&lt;/a>, and the part I care about is that it doesn&amp;rsquo;t need Docker Desktop.&lt;/p>
&lt;p>The design choice that makes it interesting: there&amp;rsquo;s no single shared Linux VM that every container piles into. Each container gets its own lightweight VM. So you get real isolation for free, boots are quick, and it all sits on top of Apple&amp;rsquo;s Containerization Swift package. Images are plain OCI, so the registries and the &lt;code>pull&lt;/code>/&lt;code>build&lt;/code>/&lt;code>push&lt;/code> workflow you already know carry over. The requirements are stricter than I&amp;rsquo;d like, though: Apple silicon, macOS 26 or newer.&lt;/p>
&lt;p>You install it from a signed package on the &lt;a href="https://github.com/apple/container/releases"target="_blank" rel="noopener noreferrer">releases page&lt;/a>, then start the background service. The service is where I tripped.&lt;/p>
&lt;p>Run anything before it&amp;rsquo;s up and you get this:&lt;/p>
&lt;div class="terminal">&lt;div class="terminal-bar" aria-hidden="true">&lt;span class="terminal-dot dot-red">&lt;/span>&lt;span class="terminal-dot dot-yellow">&lt;/span>&lt;span class="terminal-dot dot-green">&lt;/span>&lt;span class="terminal-title">zsh&lt;/span>&lt;/div>&lt;pre class="terminal-body">&lt;code>&lt;span class="t-prompt">❯&lt;/span> &lt;span class="t-bin">container&lt;/span> &lt;span class="t-cmd">machine create --name dev --set-default fedora:latest&lt;/span>
&lt;span class="t-err">Error: interrupted: &amp;#34;XPC connection error: Connection invalid&amp;#34;&lt;/span>
&lt;span class="t-out">Ensure container system service has been started with `container system start`.&lt;/span>
&lt;span class="t-out">&lt;/span>
&lt;span class="t-prompt">❯&lt;/span> &lt;span class="t-bin">container&lt;/span> &lt;span class="t-cmd">system start&lt;/span>
&lt;span class="t-out">Launching container-apiserver...&lt;/span>
&lt;span class="t-out">Testing access to container-apiserver...&lt;/span>
&lt;span class="t-out">Verifying machine API server is running...&lt;/span>
&lt;span class="t-out">No default kernel configured.&lt;/span>
&lt;span class="t-out">Install the recommended default kernel from [https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst]? [Y/n]: y&lt;/span>
&lt;span class="t-out">Installing kernel...&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>&lt;strong>&lt;code>XPC connection error: Connection invalid&lt;/code> is not a bug.&lt;/strong> The system service just isn&amp;rsquo;t running. Start it with &lt;code>container system start&lt;/code>, say yes to the default kernel, and you&amp;rsquo;re good.&lt;/p>
&lt;p>Docker Desktop on a Mac has always felt like more machinery than I needed. This is lighter. Give it a spin.&lt;/p></description></item><item><title>A Single Border and a Better Radius</title><link>https://houdeshell.dev/post/2026-06-10_a-single-border-and-a-better-radius/</link><pubDate>Wed, 10 Jun 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-06-10_a-single-border-and-a-better-radius/</guid><description>&lt;p>I read the release notes. I just opened a Finder window and it &lt;em>feels&lt;/em> better, and it took me a second to figure out why.&lt;/p>
&lt;figure class="post-figure">
&lt;img loading="lazy" src="https://houdeshell.dev/post/2026-06-10_a-single-border-and-a-better-radius/finder-macos27.png" alt="Finder in macOS 27 with a single vertical border between the sidebar and content, and softened window corners">
&lt;/figure>
&lt;p>It&amp;rsquo;s two things. There&amp;rsquo;s now a single vertical border between the sidebar and the content pane, so the two regions actually feel like two regions instead of one dark expanse where the divider was only implied by a shift in shade. And the inner corners got a slightly larger, more consistent radius that lines up with the outer window. That&amp;rsquo;s it. No new feature, nothing to learn.&lt;/p>
&lt;p>&lt;strong>The smallest possible line in the right place does more than any amount of gradient or shadow.&lt;/strong> One border to say &amp;ldquo;this is a boundary,&amp;rdquo; one radius to make the corners agree with each other, and the window stops feeling vague.&lt;/p>
&lt;p>Good detail work is invisible until you go back to the old version and feel how much you weren&amp;rsquo;t seeing.&lt;/p></description></item></channel></rss>