Skip to content

fix: the boot poll could not detect the thing it exists to detect - #125

Merged
zmaril merged 3 commits into
mainfrom
fix/boot-poll-timeout
Aug 22, 2026
Merged

fix: the boot poll could not detect the thing it exists to detect#125
zmaril merged 3 commits into
mainfrom
fix/boot-poll-timeout

Conversation

@zmaril

@zmaril zmaril commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Reported as "the preview loads but nothing happens when I click".

The real bug

The /boot poll had no timeout:

const r = await fetch("/boot", {cache: "no-store"});   // ← unbounded

A stopped machine doesn't refuse the request — the host holds it open while waking. So the fetch neither succeeded nor threw, catch never ran, misses stayed at zero, and the disconnect banner never showed. The websocket goes silent rather than closing, so that detector missed it too.

Both paths assumed failure looks like an error. Here it looks like patience — and a server that is slow and a server that is gone are the same thing to someone clicking.

const BOOT_TIMEOUT = 3000;
const r = await fetch("/boot", {
  cache: "no-store",
  signal: AbortSignal.timeout(BOOT_TIMEOUT),
});

Bounded, and shorter than its own 4s interval so polls take turns rather than stacking behind a hanging one. Two misses (~8s) shows "Server unreachable — waiting…"; the existing boot-id branch reloads once the machine is genuinely up.

The test reads both numbers out of the generated page rather than trusting the comment, and I checked it bites:

the boot poll waits 5000ms but fires every 4000ms, so they overlap

A correction, from the Fly logs

My first version of this PR also widened the preview health-check grace period from 10s to 45s, on the theory that a cold start was slow. The logs disprove that:

18:01:57  machine started in 2.29s
18:02:00  demo mode: seeded 7 runs
18:02:00  Health check on port 8080 is now passing
18:02:00  machine became reachable in 2.52s

Startup is ~3 seconds, seeding included. Ten seconds of grace was never too short and 45 would have helped nothing. The ~36s wake is Fly's proxy failing to start a stopped machine — its own dashboard says "Proxy is having trouble reaching app" — which is the other side of the wire from this repo. That change is reverted.

The boot-poll fix stands on its own: whatever makes the wake slow, the detector couldn't fire.

One thing the logs did earn

The daemon binds 0.0.0.0 and printed powderman on http://localhost:8080. That's the exact string Fly Doctor pattern-matches to report "App is not listening to the expected port… make sure your app is listening on 0.0.0.0 and not localhost" — so a line meant to be helpful was arguing for a bug that wasn't there, in a console someone reads while debugging. It prints the bind address now.

166 tests pass; fmt clean, clippy clean under -D warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XjCx5QszLXroGYh1YBxybw

Reported as "the preview loads but nothing happens when I click", which is
exactly the state the disconnect banner was written to announce and did not.

A preview machine idle-stops. Waking one takes about 36 seconds, measured:
three requests to a cold preview answered 503 at 35.7s, 503 at 35.9s, and
200 at 36.8s. During that window the page has rendered, the socket
underneath it is dead, and every click goes nowhere.

Neither detector fired. The socket goes silent rather than closing, so no
close event. And the /boot poll had no timeout — a stopped machine does not
*refuse* a request, the host holds it open while the machine wakes, so the
fetch neither succeeded nor threw. `misses` stayed at zero and the banner
never showed. A server that is slow and a server that is gone are the same
thing to someone clicking, and only the second was handled.

The poll is bounded now, and bounded shorter than its own interval so the
polls take turns rather than stacking. The test checks both, and reads the
two numbers out of the page rather than trusting a comment: a timeout
longer than the interval fails with "waits 5000ms but fires every 4000ms".

Previews also get a 45s health-check grace period. Ten seconds meant the
first probe ran against a machine that was still starting — opening SQLite
and seeding the demo history — so a waking preview answered 503 to the
person waiting on it rather than making them wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjCx5QszLXroGYh1YBxybw
@github-actions

Copy link
Copy Markdown

powderman preview

https://powderman-pr-125.fly.dev

The workbench, live: split/join/swap areas, the F3 palette, the widgets, themes, tooltips, the status bar.
The fleet and run execution are inert here (a Fly machine has no herdr) — this is for driving the interface.

On branches with the MCP server, point an agent at it:

claude mcp add --transport http powderman-preview "https://powderman-pr-125.fly.dev/mcp"

Updated on every push. Destroyed when this PR closes.

The Fly logs disprove the reasoning I gave for widening the health-check
grace period:

  18:01:57  machine started in 2.29s
  18:02:00  demo mode: seeded 7 runs
  18:02:00  Health check on port 8080 is now passing
  18:02:00  machine became reachable in 2.52s

Startup is ~3 seconds, seeding included. Ten seconds of grace was never too
short, and 45 would not have helped anything. The ~36s I measured is Fly's
proxy failing to wake a stopped machine — its own dashboard says "Proxy is
having trouble reaching app" — which is on the other side of the wire from
anything in this repo. Reverted.

What is worth changing is the line above it. The daemon binds 0.0.0.0 and
printed "powderman on http://localhost:8080", which is the exact string Fly
Doctor pattern-matches to report "App is not listening to the expected
port... make sure your app is listening on 0.0.0.0 and not localhost". A
line meant to be helpful was arguing for a bug that was not there, in a
console someone reads while debugging. It prints the bind address now.

The boot-poll timeout in the previous commit stands on its own: a stopped
machine holds the request rather than refusing it, so the detector could
not fire, whatever the reason the wake is slow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjCx5QszLXroGYh1YBxybw
@zmaril
zmaril merged commit 46357ff into main Aug 22, 2026
4 checks passed
@zmaril
zmaril deleted the fix/boot-poll-timeout branch August 22, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant