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