Skip to content

feat(server): migrate to Falcon fiber stack with non-blocking caching and TLS hardening - #1139

Merged
gildesmarais merged 15 commits into
mainfrom
feat/falcon-stack
Sep 6, 2026
Merged

feat(server): migrate to Falcon fiber stack with non-blocking caching and TLS hardening#1139
gildesmarais merged 15 commits into
mainfrom
feat/falcon-stack

Conversation

@gildesmarais

@gildesmarais gildesmarais commented Sep 5, 2026

Copy link
Copy Markdown
Member

What changed

  • Server & Reactor: Replaced Puma with Falcon (0.57.0) and added config/falcon.rb with app preloading (preload { [FalconConfig.rackup_file] }), 55s default request timeout, and dual HTTP/1.1 and HTTP/2 endpoint support.
  • TLS Hardening: Configured modern RFC 7540 AEAD cipher suites, minimum TLS 1.2, and OP_NO_COMPRESSION on Falcon's SSLContext; hardened bin/docker-healthcheck with on-disk certificate validity inspection.
  • Blocking I/O Removal: Removed Rack::Cache and its synchronous disk I/O / file locks from app.rb, dependencies, and Docker setup, relying on in-memory Feeds::Cache and HTTP cache response headers.
  • Request Throttling & Stampede Defense: Added 64 KiB body cap on POST /api/v1/feeds to reject oversized payloads with 400 Bad Request, and added single-flight request coalescing (Concurrent::Map + Concurrent::IVar) in Feeds::Cache to prevent cache stampedes.
  • Telemetry Allocation Elimination: Hoisted AppLogger to a process-level singleton instance variable, eliminating per-request Logger, LogDevice, and Monitor allocations under Fiber scheduling.
  • Docker & Dev Tooling: Updated Dockerfile CMD to bundle exec falcon host ./config/falcon.rb, dev scripts (bin/dev, bin/dev-ruby, bin/dev-with-frontend), and regenerated OpenAPI schema / frontend types.

Why

Puma's thread-per-request architecture and Rack::Cache's synchronous disk locks stall the event loop under heavy or concurrent scraping. Falcon provides an async fiber reactor compatible with HTTPX's non-blocking I/O. Removing Rack::Cache, capping request bodies, coalescing concurrent feed builds, and enforcing RFC 7540 AEAD ciphers ensure resilient, non-blocking operation at the edge and eliminate memory/CPU exhaustion vectors.

Risk

  • Low: Falcon runs in single-worker mode in development and multi-worker (WEB_CONCURRENCY, default 2) in production; graceful fallback to plain HTTP when TLS certificates are omitted.
  • Timeout hierarchy: Falcon outer timeout (55s) properly encapsulates upstream feed builds (50s) and Botasaurus scrapes (45s).

Review map

  1. config/falcon.rb — Falcon configuration, timeout alignment (55s), modern AEAD TLS cipher suite, and preload definition.
  2. app.rb & Gemfile — Removal of Rack::Cache and rack-timeout.
  3. app/web/feeds/cache.rb — Single-flight coalescing with Concurrent::IVar to prevent thundering herds.
  4. app/web/api/v1/create_feed.rb — 64 KiB request body limit and 400 Bad Request envelope.
  5. app/web/telemetry/app_logger.rb — Process singleton logger replacing per-fiber allocations.
  6. bin/docker-healthcheck — TLS-aware healthcheck with certificate expiration validation.
  7. spec/config/falcon_config_spec.rb & spec/html2rss/web/feeds/cache_spec.rb — RSpec test coverage for Falcon configuration, timeouts, and cache coalescing.

Validation

All checks executed inside the Dev Container (devcontainer-app-1):

  • make ready (RuboCop, Zeitwerk verification, YARD public docs check, ESLint/Prettier, full RSpec test suite — 354 examples, 0 failures, exit 0)
  • make openapi-verify (public/openapi.yaml schema generation and generated TypeScript client verification — exit 0)

Base automatically changed from experiment/httpx-default to main September 5, 2026 23:24
…2rss to master

Task: task_01
Intent: stack on experiment/httpx-default, track html2rss master, add falcon, remove puma and rack-timeout
Task: task_02
Intent: remove Rack::Timeout configuration from Boot::Setup and adjust unit specs
…ru, delete config/puma.rb

Task: task_03
Intent: add Falcon service configuration with dual-mode endpoint binding, clean up config.ru, remove Puma config
…-aware

Task: task_04
Intent: update Dockerfile CMD, dev scripts, and TLS-aware docker-healthcheck for Falcon
…options

Task: task_05
Intent: update README and docs architecture/guide for Falcon server and HTTPX stack
…r-request fiber allocations

In Falcon, each request executes in an ephemeral fiber. Using Thread.current
caused a new Logger and LogDevice to be allocated on every request. Hoisting
to a module singleton eliminates this allocation churn.
… async fiber reactor

Rack::Cache used a file-based metastore and entitystore with synchronous flocking
and disk I/O, which blocks the Async fiber reactor thread. Feed responses are
already cached in memory by Feeds::Cache and downstream HTTP caching is governed
by HttpCache headers.
…alescing in cache

- Enforce a 64 KiB maximum request body size on POST /api/v1/feeds to prevent
  memory exhaustion attacks.
- Add in-flight request coalescing (single-flight) to Feeds::Cache using
  Concurrent::IVar to prevent cache stampedes and duplicate scrapes against
  Botasaurus and upstream sources.
@gildesmarais gildesmarais changed the title feat(server): migrate from Puma to Falcon / Fiber stack feat(server): migrate to Falcon fiber stack with non-blocking caching and TLS hardening Sep 6, 2026
- Release reload_mutex before executing app.call in DevelopmentReloader
  so requests process concurrently in development mode.
- Add default BOTASAURUS_SCRAPER_URL to devcontainer docker compose.
@gildesmarais
gildesmarais merged commit 4f1441f into main Sep 6, 2026
15 checks passed
@gildesmarais
gildesmarais deleted the feat/falcon-stack branch September 6, 2026 00:42
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