Upgrade: Puma→Falcon migration section as a runbook (blockers fixed) - #501
Merged
Merged
Conversation
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>
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #496 — same rewrite, plus the four defects an independent execution-based pass found in it.
Rewrites
## Migration from Puma/Unicornon the site's #2 organic page from 207 lines of filler into a 5-step runbook. URL, slug, H2 and the#migration-from-pumaunicornanchor unchanged.What the pass confirmed (kept as written)
Thread#[]is fiber-local;thread_variable_setis 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, whileThread.current[:cache]silently stops caching. It's the section's centerpiece.falcon hostwithfalcon.rbat 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
Open3|Process.spawn|system(|%x(. Falcon's scheduler implementsprocess_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.falcon hosttakes no path argument" —host.rb:35ismany :paths, default: ["falcon.rb"]. A fabricated absence claim on a commit about removing fabrications.thread_variable_setgrep was scoped toapp lib config, but the leak it illustrates comes from a gem. Reader gets zero hits and concludes clear.preload, then asked for an RSS-per-worker comparison against a Puma config usingpreload_app!— a rollback could trip on our own config. Restored with a comment.Notes
async: truein database.yml, the outdatedload :rackDSL, and "no explicit timeout needed as fibers are cooperative".falcon serveinvocations elsewhere on this page, including the systemd unit and Dockerfile, plus the "genuinely" tell just below this section).bin/hugo-buildgreen · zero em dashes · anchor and TOC entry verified.🤖 Generated with Claude Code