Skip to content

Upgrade: Puma→Falcon migration section rewritten as a runbook (in-place, #2 page) - #496

Closed
pftg wants to merge 1 commit into
masterfrom
upgrade/falcon-puma-migration-runbook
Closed

pftg wants to merge 1 commit into
masterfrom
upgrade/falcon-puma-migration-runbook

Conversation

@pftg

@pftg pftg commented Aug 20, 2026

Copy link
Copy Markdown
Member

Rewrites the ## Migration from Puma/Unicorn section of the site's #2 organic page in place. URL, slug, H2 and the #migration-from-pumaunicorn anchor are unchanged - a separate Puma→Falcon post would have split a ranking cluster (20.09 §12a).

207 lines → 114. The old section opened with "requires understanding the differences and planning the transition carefully", and its "Pre-Migration Checklist" was Ruby snippets annotated "should be fine" / "OK in Falcon". It never said what breaks or how to roll back.

Four fabrications removed, each verified against primary sources

Removed Why
async: true in database.yml Not a Rails key. Real keys: pool, checkout_timeout, idle_timeout, reaping_frequency
load :rack / rack hostname do DSL Superseded in 0.57 by service + Falcon::Environment::Rack
"No explicit timeout needed as fibers are cooperative" Unsupported
"the C extension that forced it" (cross-link) The tuning post documents a MiniMagick shell-out, not a C extension

The centerpiece correction

Thread#[] / Thread.current[:foo] is fiber-local - "Each fiber has its own bucket for Thread#[] storage." It is thread_variable_set that 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:

  • thread_variable_set for per-request state → fibers share one thread → cross-request data leak
  • Thread.current[:cache] as a memo → rebuilt per request → silent cache-miss regression

Also corrected: there is no Falcon equivalent of Puma's threads min, max. count sets worker processes; nothing bounds fibers per worker, so the ceiling lives in your DB pool.

Reviewer verdicts

Copy editor - MAJOR: "two edits make the runbook fail if a reader executes it top to bottom". All cuts applied, plus the Gemfile/rollback ambiguity fix.

Cold-eyes 9-check - NOT PUBLISH-READY → fixed:

  • B1 bundle exec falcon.rb does not resolve from the app root, and contradicted the two launch commands already on the page
  • B2 File.basename(__dir__) names the service "config" when the file sits under config/

Both resolved by moving falcon.rb to the application root per the deployment guide, where falcon host reads it and takes no path argument. falcon serve is documented as "not designed for deployment".

Gates

  • bin/hugo-build green (1171 pages), post-rebase onto the theme rebuild (e1fa540)
  • Scroll gate desktop 1280x800 + mobile 390x844: 0 console errors, 0 failed requests, no horizontal overflow, all 7 headings resolve
  • Content-only diff (one markdown file), so visual suites correctly skipped

Merge order

Expects #493 to land first. That PR fixes the Rails Integration block (:fiber_pool, allow_concurrency) which this branch deliberately does not touch. Rebased onto current master so it can merge either way, but #493 first keeps the page internally consistent.

Known defects outside this window (not mine to fix)

  • L279 async_query_executor = :fiber_pool - invalid value (owned by P0: recover #2 organic page from a 404 (Falcon alias) + R9 verdict #493)
  • L1010 after_connect: in database.yml - not a real key, same fabrication class
  • Production Configuration block mixes the legacy load :rack + old shebang with the modern service/Falcon::Environment::Rack DSL in one block
  • Six falcon --config ... serve commands elsewhere on the page pair with the legacy DSL

🤖 Generated with Claude Code

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>
@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: fb4c1160-8aa3-45d1-9996-bf4d981b940a


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

pftg commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

🛑 Independent codex cross-check: 2 blockers + 2 majors. Converting to draft. All findings verified by execution against falcon-0.57.0, Ruby 4.0.6, async 2.44.1, activerecord 8.1.3.1 — not by reading.

First, the three riskiest claims all HOLD — ship them:

  • Thread#[] inversion: CORRECT. Executed on 4.0.6: inside a fiber Thread.current[:x] is nil while thread_variable_get reads thread-wide and mutations leak out. The quoted doc line is verbatim. The centerpiece survives.
  • No per-worker fiber bound: CORRECT. No concurrency directive anywhere in falcon 0.57.0 lib/ or async-http's server; the cluster-deployment doc confirms it's deliberate.
  • falcon host + falcon.rb at app root: CORRECT — and the rest of the page is the stale part. falcon --config config/falcon.rb serve is invalid CLI (Could not parse token "serve"). It appears four times, including the systemd ExecStart (L448) and the Dockerfile CMD (L514) — both would fail to boot.

BLOCKER 1 — "hit 3 blocks the whole worker" is false for all four things its grep finds. Falcon runs Async::Scheduler, which implements process_wait. Measured, three concurrent 1s shell-outs: Open3.capture2 1.01s, system() 1.02s, backticks 1.02s — blocking would be ~3.0s. They yield. The grep targets exactly the safe cases; the real blocker (C extensions) is what the grep can't find. Same falsified mechanism also lives in falcon-web-server-production-tuning-benchmarks:191, which this section links to approvingly.

BLOCKER 2 — falcon host does take path arguments. host.rb:35: many :paths, default: ["falcon.rb"]. It's the default, not the only accepted value — a fabricated absence claim on a commit whose purpose was removing fabrications.

MAJOR 1 — the thread_variable_set grep is scoped to app lib config, but the example it illustrates is a gem stashing tenant state. Reader runs it, gets zero hits, concludes clear.

MAJOR 2 — the config drops preload, then the canary step asks the reader to compare RSS per worker against a Puma config that used preload_app!. Rollback could trip on an artifact of our own config.

Also: systemd count is 8 (env override), not 4; a quote has its subject substituted inside quotation marks; the Rails guide now recommends the Falcon::Rails gem; and "When to stay on Puma" restates the tuning post near item-for-item without a cross-link.

Repro scripts in the session scratchpad.

@pftg

pftg commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Superseded by #501, which is this same rewrite plus the four defects the independent execution-based pass found in it (the two blockers and two majors listed in the comment above).

Closing rather than pushing onto this branch because its worktree belongs to an agent that is currently offline; #501 branches from this exact commit (5756415), so nothing is lost.

@pftg pftg closed this Aug 20, 2026
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