Class: Quonfig::ConfigLoader
- Inherits:
-
Object
- Object
- Quonfig::ConfigLoader
- Defined in:
- lib/quonfig/config_loader.rb
Overview
Fetches config envelopes from the Quonfig delivery API and installs them into a ConfigStore.
Wire format matches sdk-node's Transport + ConfigStore:
GET /api/v2/configs
-> 200 { "configs": [...], "meta": { "version": "...", "environment": "..." } }
-> 304 Not Modified (ETag honored via If-None-Match)
The fetch is synchronous; Client is responsible for timing out the initial
fetch per init_timeout_ms.
Constant Summary collapse
- LOG =
Quonfig::InternalLogger.new(self)
- CONFIGS_PATH =
'/api/v2/configs'
Instance Attribute Summary collapse
-
#environment_id ⇒ Object
readonly
Returns the value of attribute environment_id.
-
#held_generation ⇒ Object
readonly
qfg-7h5d.1.9 (canonical ordering).
-
#install_count ⇒ Object
readonly
qfg-7h5d.1.9 (canonical ordering).
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Instance Method Summary collapse
-
#apply_envelope(envelope) ⇒ Object
Apply a ConfigEnvelope (from SSE) to the store.
- #calc_config ⇒ Object
-
#etag ⇒ Object
Backward-compatible reader: the primary leg's last ETag.
-
#fetch! ⇒ Object
Fetch configs from /api/v2/configs with per-leg ETag / If-None-Match caching.
-
#initialize(store_or_base_client, options = nil, logger: nil) ⇒ ConfigLoader
constructor
store: the Quonfig::ConfigStore to populate on successful fetch. -
#resolved_from ⇒ Object
'primary' / 'secondary' / '' for the leg that produced the currently-held config (config_api_urls index 0 = primary, 1 = secondary).
- #rm(key) ⇒ Object
- #set(config, source) ⇒ Object
Constructor Details
#initialize(store_or_base_client, options = nil, logger: nil) ⇒ ConfigLoader
store: the Quonfig::ConfigStore to populate on successful fetch.
options: a Quonfig::Options instance (supplies sdk_key + config_api_urls).
logger: optional logger override (defaults to module LOG).
Backward compat: callers that pass a single base_client (mock client
used by tests that expects .options) are still supported.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/quonfig/config_loader.rb', line 40 def initialize(store_or_base_client, = nil, logger: nil) if .nil? && store_or_base_client.respond_to?(:options) # Legacy shape: ConfigLoader.new(base_client) @options = store_or_base_client. @store = nil else @store = store_or_base_client @options = end @api_config = Concurrent::Map.new # qfg-7h5d.1.14: per-leg ETag is load-bearing for the parallel hedge. The # hedge runs the primary and secondary legs concurrently; a SINGLE shared # ETag would (a) let a 304 from one leg mask the other and (b) be a data # race with two legs writing it. Each leg keeps its own slot keyed by # config_api_urls index, guarded by @etag_mutex (snapshot before the # request, write-back after — the network wait happens with no lock held). @etags = {} @etag_mutex = Mutex.new @version = nil @environment_id = nil @logger = logger || LOG # Canonical-ordering state (qfg-7h5d.1.9). @install_mutex makes the # guard-check-and-install atomic across every install path (initial fetch, # failover/poll fetch, SSE snapshot, SSE update, fallback poller) — these # run on different threads and must never interleave a stale install with a # fresh one. @held_generation = nil @install_count = 0 @resolved_from_index = nil @install_mutex = Mutex.new end |
Instance Attribute Details
#environment_id ⇒ Object (readonly)
Returns the value of attribute environment_id.
21 22 23 |
# File 'lib/quonfig/config_loader.rb', line 21 def environment_id @environment_id end |
#held_generation ⇒ Object (readonly)
qfg-7h5d.1.9 (canonical ordering). Diagnostic surface read by the failover/ ordering chaos probe and by operators:
held_generation — Meta.generation of the currently-installed envelope
(nil before the first install).
install_count — number of envelopes actually installed (rejected-older
and same-generation snapshots do NOT bump this).
resolved_from — 'primary' / 'secondary' / '' — which config_api_urls leg
produced the currently-held config (HTTP installs only;
SSE does not change it).
32 33 34 |
# File 'lib/quonfig/config_loader.rb', line 32 def held_generation @held_generation end |
#install_count ⇒ Object (readonly)
qfg-7h5d.1.9 (canonical ordering). Diagnostic surface read by the failover/ ordering chaos probe and by operators:
held_generation — Meta.generation of the currently-installed envelope
(nil before the first install).
install_count — number of envelopes actually installed (rejected-older
and same-generation snapshots do NOT bump this).
resolved_from — 'primary' / 'secondary' / '' — which config_api_urls leg
produced the currently-held config (HTTP installs only;
SSE does not change it).
32 33 34 |
# File 'lib/quonfig/config_loader.rb', line 32 def install_count @install_count end |
#version ⇒ Object (readonly)
Returns the value of attribute version.
21 22 23 |
# File 'lib/quonfig/config_loader.rb', line 21 def version @version end |
Instance Method Details
#apply_envelope(envelope) ⇒ Object
Apply a ConfigEnvelope (from SSE) to the store. Called by the SSE client when a new event arrives. SSE is a single-leg live stream, so it carries no config_api_urls index and does not change #resolved_from — but it IS guarded by the same reject-older rule (a late SSE snapshot must not regress an established client).
137 138 139 |
# File 'lib/quonfig/config_loader.rb', line 137 def apply_envelope(envelope) install_envelope(envelope, source: :sse, source_index: nil) end |
#calc_config ⇒ Object
141 142 143 144 145 146 147 |
# File 'lib/quonfig/config_loader.rb', line 141 def calc_config rtn = {} @api_config.each_key do |k| rtn[k] = @api_config[k] end rtn end |
#etag ⇒ Object
Backward-compatible reader: the primary leg's last ETag. Pre-hedge this was a single shared @etag; per-leg isolation now means index 0 is the canonical "the ETag" for callers/tests that read one value.
77 78 79 |
# File 'lib/quonfig/config_loader.rb', line 77 def etag @etag_mutex.synchronize { @etags[0] } end |
#fetch! ⇒ Object
Fetch configs from /api/v2/configs with per-leg ETag / If-None-Match caching.
qfg-7h5d.1.14 — PARALLEL-FAILOVER HEDGE. On every init/refresh fetch the PRIMARY leg (config_api_urls) is fired first, on the CALLING thread. If it answers within config_fetch_hedge_delay_ms it WINS and the secondary is NEVER contacted (cold standby — zero extra load on a healthy system). If the primary is SLOW past the hedge delay OR errors fast, the SECONDARY leg (config_api_urls) is ALSO fired IN PARALLEL on a background thread, at-most-once — the primary is NOT cancelled. Whatever arrives is installed through the EXISTING reject-older guard (#install_envelope), so watermark-MAX falls out for free: a higher generation wins, a late OLDER payload never regresses an established client, and a late NEWER payload heals forward.
fetch! returns as soon as the FIRST leg installs (readiness latches off it); any still-running leg keeps running on its own thread, bounded by config_fetch_hedge_abort_ms, and heals forward if it lands a newer generation. There is NO coalescing/in-flight gate — overlapping fetches are safe (per-leg ETag isolation + every install serialized through a coalescing gate would make a manual refresh silently no-op (a contract violation).
Returns one of:
:updated -- at least one leg installed a 200 envelope
:not_modified -- a leg answered 304 (no change) and nothing installed
:failed -- every fired leg failed
118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/quonfig/config_loader.rb', line 118 def fetch! urls = Array(@options.config_api_urls) return :failed if urls.empty? # Single leg (or no secondary configured): no hedge, just fetch on the # calling thread under the SEQUENTIAL per-URL timeout (config_fetch_timeout_ms # is unchanged and still governs any non-hedged path). Preserves the # synchronous, single-request-per-call shape the legacy/mock callers depend # on. return fetch_from(urls[0], 0, timeout_ms: config_fetch_timeout_ms) if urls.length < 2 fetch_hedged(urls) end |
#resolved_from ⇒ Object
'primary' / 'secondary' / '' for the leg that produced the currently-held config (config_api_urls index 0 = primary, 1 = secondary).
83 84 85 86 87 88 89 90 |
# File 'lib/quonfig/config_loader.rb', line 83 def resolved_from case @resolved_from_index when nil then '' when 0 then 'primary' when 1 then 'secondary' else "url#{@resolved_from_index}" end end |
#rm(key) ⇒ Object
153 154 155 |
# File 'lib/quonfig/config_loader.rb', line 153 def rm(key) @api_config.delete(key) end |
#set(config, source) ⇒ Object
149 150 151 |
# File 'lib/quonfig/config_loader.rb', line 149 def set(config, source) @api_config[config.key] = { source: source, config: config } end |