Class: Quonfig::ConfigLoader

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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, options = nil, logger: nil)
  if options.nil? && store_or_base_client.respond_to?(:options)
    # Legacy shape: ConfigLoader.new(base_client)
    @options = store_or_base_client.options
    @store = nil
  else
    @store = store_or_base_client
    @options = 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_idObject (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_generationObject (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_countObject (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

#versionObject (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_configObject



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

#etagObject

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_fromObject

'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