Module: ReactOnRailsPro::RollingDeployCacheStager
- Defined in:
- lib/react_on_rails_pro/rolling_deploy_cache_stager.rb
Overview
Seeds previous deploy bundle hashes into the Node Renderer cache so that during a rolling deploy, new renderer instances can serve requests for bundles referenced by draining Rails instances without hitting the 410 retry path.
Discovery:
* ENV["PREVIOUS_BUNDLE_HASHES"] (comma-separated) — overrides adapter discovery.
* ReactOnRailsPro.configuration.rolling_deploy_adapter#previous_bundle_hashes — the default.
Retrieval:
* rolling_deploy_adapter#fetch(hash) must return a Hash with keys
:bundle (String path to the bundle file) and :assets (Array<String>
of companion asset file paths). Returns nil if the bundle is
unavailable.
Protocol model: each hash is one bundle’s cache entry. Adapters advertise separate hashes for server and RSC bundles; the stager stages each hash at <cache>/<hash>/<hash>.js independently.
Missing previous bundles degrade gracefully (warn + continue) because the runtime 410-retry path is still a valid fallback — a failed rolling-deploy seed is less catastrophic than a failed current bundle seed.
Constant Summary collapse
- DISCOVERY_TIMEOUT_SECONDS =
Duplicated in react_on_rails/lib/react_on_rails/doctor.rb as a hardcoded fallback when the Pro gem isn’t loaded. The cross-package equality is asserted in spec/dummy/spec/rolling_deploy_cache_stager_spec.rb so a change here fails that spec instead of silently drifting past the doctor probe. Note: ‘Timeout.timeout` interrupts the discovery call at a quasi- random thread-execution point. The bundled reference adapters use pure- Ruby HTTP clients that release the GIL, so the interrupt is safe. Adapter authors using native-extension or FFI-backed clients should add their own SDK-level `open_timeout` / `read_timeout` rather than rely solely on this outer wrapper. Same caveat applies to `FETCH_TIMEOUT_SECONDS` below and the upload timeout in `assets_precompile.rb`.
10- FETCH_TIMEOUT_SECONDS =
Per-hash fetch budget during pre-seeding. Large cross-region stores may need adapters to keep fetches comfortably under this limit.
30- STALE_TEMP_DIR_TTL_SECONDS =
Age threshold for sweeping leftover ‘.staging-*` / `.previous-*` dirs. Any temp dir older than this is assumed to be from a crashed or abandoned prior run and safe to remove. If `assets:precompile` itself routinely takes longer than one hour on a persistent-volume deploy (uncommon), raise this so a concurrent seeder’s still-in-use staging dir is not swept mid-operation. The degradation is graceful regardless — the racing ‘replace_bundle_directory` rolls back cleanly on `ENOENT`.
3600- TEMPORARY_DIRECTORY_PATTERN =
Match temp dirs created by ‘temporary_bundle_directory` (and the analogous `.previous-` backup suffix in `replace_bundle_directory`). The 8-hex random suffix defeats false positives where a real bundle hash happens to end with `.staging-<digits>-<short hex>`. PID is `d+` rather than `d4,` because container deployments (Docker, Kubernetes) commonly run the seeding process as PID 1; a stricter floor would silently leave PID-1 staging dirs in the cache to accumulate forever.
/\.(?:staging|previous)-\d+-[0-9a-f]{8,}\z/- SAFE_HASH_PATTERN =
Bundle hashes are used as directory names under the renderer cache path (<cache>/<hash>/<hash>.js). Reject path separators, ‘.` / `..`, and any leading dot. `bundle_directory`’s ‘start_with?` guard already prevents path traversal, but a leading-dot hash (e.g. `.hidden`) would still create a hidden cache subdirectory invisible to `ls`, surprising operators who count bundle-hash entries during incident response.
/\A(?!\.)[A-Za-z0-9_.-]+\z/
Class Method Summary collapse
Class Method Details
.call(cache_dir:, current_hashes:, mode:) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/react_on_rails_pro/rolling_deploy_cache_stager.rb', line 65 def self.call(cache_dir:, current_hashes:, mode:) adapter = ReactOnRailsPro.configuration.rolling_deploy_adapter return handle_missing_adapter unless adapter sweep_stale_temporary_directories(cache_dir) hashes = resolve_previous_hashes(adapter, current_hashes) if hashes.empty? puts "[ReactOnRailsPro] No previous bundle hashes to seed for rolling deploy." return end # Create the cache root once we know we have at least one hash to stage. # bundle_directory then resolves real paths against an existing dir without # needing to mutate the filesystem itself. FileUtils.mkdir_p(cache_dir) normalized_cache_dir = File.realpath(cache_dir) puts "[ReactOnRailsPro] Seeding previous bundle hashes for rolling deploy: #{hashes.inspect}" hashes.each { |hash| seed_previous_hash(adapter, hash, normalized_cache_dir, mode) } end |