Skip to content

Upgrade: Puma→Falcon migration section as a runbook (blockers fixed) - #501

Merged
pftg merged 4 commits into
masterfrom
fix/falcon-runbook-blockers
Aug 20, 2026
Merged

pftg merged 4 commits into
masterfrom
fix/falcon-runbook-blockers

Conversation

@pftg

@pftg pftg commented Aug 20, 2026

Copy link
Copy Markdown
Member

Supersedes #496 — same rewrite, plus the four defects an independent execution-based pass found in it.

Rewrites ## Migration from Puma/Unicorn on the site's #2 organic page from 207 lines of filler into a 5-step runbook. URL, slug, H2 and the #migration-from-pumaunicorn anchor unchanged.

What the pass confirmed (kept as written)

  • Thread#[] is fiber-local; thread_variable_set is thread-local. Verified by execution on Ruby 4.0.6. This inverts the usual migration advice: the cross-request leak lives in the API that sounds thread-safe, while Thread.current[:cache] silently stops caching. It's the section's centerpiece.
  • No Falcon directive bounds per-worker fiber concurrency. Searched all of falcon 0.57.0 and async-http; the cluster-deployment doc confirms it's deliberate.
  • falcon host with falcon.rb at app root is correct — and the rest of the page was the stale part (fixed separately in P0: recover #2 organic page from a 404, fix unbootable published configs + broken sample code #498).

What it caught, fixed here

  • BLOCKER: the section claimed shell-outs block the whole worker and handed readers a grep for Open3|Process.spawn|system(|%x(. Falcon's scheduler implements process_wait — three concurrent 1s shell-outs measure ~1.0s, not 3.0s. It warned about the four safe cases while the real blocker (C extensions bypassing the hooks) is what that grep can't find. Now says shelling out is fine and greps the Gemfile for native code.
  • BLOCKER: "falcon host takes no path argument" — host.rb:35 is many :paths, default: ["falcon.rb"]. A fabricated absence claim on a commit about removing fabrications.
  • MAJOR: the thread_variable_set grep was scoped to app lib config, but the leak it illustrates comes from a gem. Reader gets zero hits and concludes clear.
  • MAJOR: the config omitted preload, then asked for an RSS-per-worker comparison against a Puma config using preload_app! — a rollback could trip on our own config. Restored with a comment.
  • Plus: the systemd example runs 8 workers not 4 (env override), and a quote had its subject swapped inside the quotation marks.

Notes

  • Three fabrications the rewrite removed from this window: async: true in database.yml, the outdated load :rack DSL, and "no explicit timeout needed as fibers are cooperative".
  • Expects P0: recover #2 organic page from a 404, fix unbootable published configs + broken sample code #498 to merge first (it fixes seven invalid falcon serve invocations elsewhere on this page, including the systemd unit and Dockerfile, plus the "genuinely" tell just below this section).
  • bin/hugo-build green · zero em dashes · anchor and TOC entry verified.

🤖 Generated with Claude Code

pftg and others added 2 commits August 20, 2026 19:28
Replaces 207 lines of generic filler (a Pre-Migration Checklist of snippets annotated 'should be fine') with a 114-line runbook the reader can execute top to bottom: fiber-safety greps, the config diff, what actually breaks, the isolation level, a canary with an explicit rollback trigger, and when to stay on Puma. H2 and slug unchanged - #migration-from-pumaunicorn is a live ranking anchor.

Four fabrications removed, each verified against primary sources: 'async: true' is not a database.yml key; the 'load :rack' DSL is superseded by the service/Falcon::Environment::Rack shape in 0.57; 'no explicit timeout needed as fibers are cooperative' was unsupported; and the cross-link claimed a C extension forced the sibling post's rollback when that post documents a MiniMagick shell-out.

Centerpiece correction: Thread#[] is FIBER-local ('Each fiber has its own bucket for Thread#[] storage'), while thread_variable_set is thread-local. The standard advice to 'move thread-local state to fiber-local' is backwards for the API people actually reach for, so the leak hides in the one that sounds safe.

Reviewer verdicts. Copy editor: MAJOR - 'two edits make the runbook fail if a reader executes it top to bottom'; all cuts and the Gemfile/rollback ambiguity fix applied. Cold-eyes 9-check: NOT PUBLISH-READY on two copy-paste-and-it-fails blockers - 'bundle exec falcon.rb' does not resolve from the app root, and File.basename(__dir__) names the service 'config' when the file sits under config/. Both fixed by moving falcon.rb to the application root per the deployment guide, where falcon host reads it and takes no path argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An independent pass ran the claims instead of reading them, against
falcon-0.57.0 / Ruby 4.0.6 / async 2.44.1.

BLOCKER - "hit 3 blocks the whole worker" was false for everything the
grep found. Falcon's Async::Scheduler implements process_wait, so
Open3.capture2, system() and backticks all yield: three concurrent
one-second shell-outs measured ~1.0s, not 3.0s. The section warned
about the four safe cases and pointed its grep at them, while the real
blocker - a C extension issuing a syscall directly - is exactly what
that grep cannot see. Rewritten to say shelling out is fine and to
grep the Gemfile for native code instead.

BLOCKER - "falcon host ... takes no path argument" is a fabricated
absence claim on a commit about removing fabrications. host.rb:35 is
`many :paths, default: ["falcon.rb"]`; the default is not the only
accepted value. The __dir__ half of the sentence carries the point and
is kept.

MAJOR - the thread_variable_set grep was scoped to app lib config while
the example it illustrates is a gem stashing tenant state. A reader ran
the command called "the one that costs you data", got zero hits, and
concluded clear. Now says to search gems too.

MAJOR - the config omitted `preload`, then the canary step asked for an
RSS-per-worker comparison against a Puma config that used preload_app!.
Preloaded Puma versus non-preloaded Falcon would have tripped a
rollback on our own config. `preload "config/environment"` restored with
a comment saying why.

Also: the systemd example runs 8 workers, not the 4 in its config
fallback, because .env.production sets WEB_CONCURRENCY - the text now
says to read the env file. And a quoted line had its subject swapped
inside the quotation marks ("Falcon will" for "it will"); restored
verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d3e2f37-55a0-4d87-a396-f4d9472c67cc


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

pftg and others added 2 commits August 20, 2026 21:02
A codex pass ran the commands instead of reading them and found that
`grep -rn "require" Gemfile` finds zero native gems. In a Gemfile
`require` only ever appears as `require: false`, a Bundler auto-require
flag with nothing to do with C extensions. On a realistic Rails Gemfile
carrying pg, nokogiri, image_processing, bcrypt, sassc-rails and grpc it
returns four gems - bootsnap, debug, brakeman, rubocop-rails-omakase -
and none of the seven native ones. A confident non-empty list of exactly
the wrong answers, which is the same defect the previous commit set out
to remove.

Replaced with the command that actually answers the question, verified
to list 36 native gems here:
  bundle exec ruby -e 'Gem::Specification.select { |s| s.extensions.any? }.each { |s| puts s.name }'
and the explanation now says why Bundler is asked rather than the
Gemfile read.

MAJOR from the same pass: a failed `preload` is rescued and only warned
about (managed/service.rb:48), and its path resolves against the
directory holding falcon.rb rather than the app root. So a misplaced
config produces unpreloaded workers, a normal-looking boot, and exactly
the inflated RSS-per-worker number my own comment warns about - which
the canary step then asks the reader to compare against Puma. Added the
log check to that step.

Also restored "load-bearing twice over" with the real second reason
(root-relative resolution breaks preload and config.ru together),
reworded the WEB_CONCURRENCY line so it no longer points readers at a
block that contradicts this section, and corrected "Falcon's
Async::Scheduler" to the async gem's - Falcon runs on it, it is not a
Falcon class.

Verified by execution in that pass, so recording it: process_wait holds
for Open3.capture2/capture3, system, backticks, Process.spawn with both
Process.wait and Process::Status.wait, and IO.popen - all 1.01s for
three concurrent one-second shell-outs, and 1.02s end to end in a real
falcon host worker. And `preload` is valid inside a service block and
does run pre-fork: parent logged the preload, then forked 15 children
that all reported the parent's pid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#498 and this branch both edited the migration section of the same post.
#498 landed first, replacing seven invalid `falcon --config ... serve`
invocations with `falcon host`. This branch had already deleted that
whole section and rewritten it as a runbook, so git matched three
orphaned fragments of the removed text against the new prose and
conflicted on them:

  # config/falcon.rb for staging   /   # After: config/falcon.rb
  #!/usr/bin/env falcon-host

All three are remnants of text this branch removed on purpose, so the
resolution takes HEAD in each. Nothing from #498 is discarded by that -
its edits outside the rewritten section merged cleanly.

Verified after resolving, since a bad merge here would silently restore
a defect we just shipped a P0 for:
- zero conflict markers
- zero `falcon --config` (the unbootable invocation #498 removed)
- zero `:fiber_pool` / `allow_concurrency` (the fabricated configs)
- aliases: still carries the in-production 404 recovery
- preload "config/environment" still present in all four places
- the native-extension check is still the Gem::Specification command,
  not the `grep "require" Gemfile` that returned the wrong gems
- code fences balanced (86)
@pftg
pftg merged commit e03e611 into master Aug 20, 2026
5 checks passed
@pftg
pftg deleted the fix/falcon-runbook-blockers branch August 20, 2026 19:29
pftg added a commit that referenced this pull request Aug 20, 2026
Sync for 2026-08-20's merged work (#499, #501, #506, #509, #510). All
three land in workflows/blog-pipeline.md because they are gates, not
background - per the bundle's own rule that a lesson mattering six weeks
later belongs in a concept rather than the log.

1. Technical claims must be executed, not read. Every wrong technical
   claim shipped today came from reading source and inferring behaviour.
   The Kamal guide asserted a stale traefik: key sits in deploy.yml doing
   nothing; kamal config answers 'unknown key: traefik'. The bad
   inference came from Validator::Configuration#allow_extensions? => true,
   which governs YAML extension keys and not arbitrary config keys.

2. Frontmatter is published copy. #509 shipped live with a body softened
   to 'whether retired or quietly substituted' while twitter_description
   still asserted 'a retired model took out five features at once'.

3. Citation lists use ## Sources. 4 posts already used it; 16 had drifted
   across three forms, and the bold variant survived the first survey
   because my grep matched only the other two.

Concept timestamp + verified stamp added honestly (claude/opus-5, today).
workflows/index.md entry widened to name the three gates so a cold
session finds them without opening the file.

Validated: uv run okf_validate.py .okf --strict -> conformant, 0 errors.
Warning count unchanged at 64 (measured against a clean tree first, so
the number is a baseline rather than a claim).
pftg added a commit that referenced this pull request Aug 20, 2026
docs(okf): three publishing gates learned from claims we shipped wrong

Sync for 2026-08-20's merged work (#499, #501, #506, #509, #510). All
three land in workflows/blog-pipeline.md because they are gates, not
background - per the bundle's own rule that a lesson mattering six weeks
later belongs in a concept rather than the log.

1. Technical claims must be executed, not read. Every wrong technical
   claim shipped today came from reading source and inferring behaviour.
   The Kamal guide asserted a stale traefik: key sits in deploy.yml doing
   nothing; kamal config answers 'unknown key: traefik'. The bad
   inference came from Validator::Configuration#allow_extensions? => true,
   which governs YAML extension keys and not arbitrary config keys.

2. Frontmatter is published copy. #509 shipped live with a body softened
   to 'whether retired or quietly substituted' while twitter_description
   still asserted 'a retired model took out five features at once'.

3. Citation lists use ## Sources. 4 posts already used it; 16 had drifted
   across three forms, and the bold variant survived the first survey
   because my grep matched only the other two.

Concept timestamp + verified stamp added honestly (claude/opus-5, today).
workflows/index.md entry widened to name the three gates so a cold
session finds them without opening the file.

Validated: uv run okf_validate.py .okf --strict -> conformant, 0 errors.
Warning count unchanged at 64 (measured against a clean tree first, so
the number is a baseline rather than a claim).
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