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 //.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 \d{4,} 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 (//.js). The shared pattern rejects path separators, . / .., any leading dot, and any leading hyphen. 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.

NOTE: this is a tightening of the previous local pattern, which allowed leading hyphens. Webpack content hashes never start with - in practice, but custom adapters emitting hyphen-prefixed hashes will now see those hashes silently dropped from the staged set. See CHANGELOG for the upgrade note.

ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN

Class Method Summary collapse

Class Method Details

.call(cache_dir:, current_hashes:, mode:) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/react_on_rails_pro/rolling_deploy_cache_stager.rb', line 79

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