Class: ConvertSdk::Client
- Inherits:
-
Object
- Object
- ConvertSdk::Client
- Defined in:
- lib/convert_sdk/client.rb
Overview
The SDK runtime handle returned by create.
Client owns the wiring of the injected managers (config, logging, HTTP,
store, events, data) and drives the config lifecycle at construction:
- Direct data mode (+data:+ supplied) — the inline object is normalised to string keys and installed straight into DataManager; NO config fetch happens (a single network-free path for testing / advanced setups).
- Fetch mode (+sdk_key:+ only) — config is fetched via
GET {config_endpoint}/config/{sdkKey}(+?environment=...+ when set) through HttpClient ONLY, withAuthorization: Bearer {sdk_key_secret}attached when a secret is configured. A failed fetch is logged atwarnand the client is constructed WITHOUT config (degrade-gracefully, NFR12) — it never raises.
ready exactly once (FR9)
The first successful config install (fetched OR direct) fires
SystemEvents::READY exactly once for the client's lifetime; the once-guard
is the :first marker DataManager#install_config computes atomically inside
its config mutex. Subsequent installs (Story 2.7's refresh) fire
SystemEvents::CONFIG_UPDATED, never ready again.
Never-crash boundary
Every public method rescues StandardError, logs it, and returns its
per-contract value — only create's ArgumentError (raised by
Config on misconfiguration) is allowed to escape. The endpoints are touched
ONLY through HttpClient (the single hardened HTTP port); the Client never
touches the network library directly or builds wire headers beyond passing
the Bearer header VALUE through the port.
Decisioning surface (fully wired)
#create_context injects the per-context decisioning managers (ExperienceManager, FeatureManager, SegmentsManager) and the outbound ApiManager into each Context, so a context returned by a factory-built client decides through the real Story 2.9–3.2 machinery and enqueues bucketing / conversion events for delivery. No background threads are started by the Client or the factory (NFR4 lazy start); the refresh timer (Story 2.7), flush/fork/at_exit (Epic 4) wiring is lazy. Constructor injection throughout — no globals.
Instance Attribute Summary collapse
-
#api_manager ⇒ ApiManager
readonly
The outbound event queue + delivery surface (Story 4.1).
-
#config ⇒ Config
readonly
The configuration this client was built with.
-
#data_manager ⇒ DataManager
readonly
The config snapshot reader surface.
Instance Method Summary collapse
-
#config_available? ⇒ Boolean
True once a config snapshot is installed (degrade probe).
-
#create_context(visitor_id = nil, attributes = nil) ⇒ Context?
Create a per-visitor decisioning Context — the object an integrator holds for the lifetime of one request/job.
-
#ensure_fresh_config! ⇒ self
Decision-time freshness hook for timer-off mode (Lambda/CLI).
-
#ensure_refresh_timer! ⇒ self
Lazily start the background config-refresh timer (NFR4 — never in the factory).
-
#flush(reason = nil) ⇒ self
(also: #release_queues)
Explicitly release the queued visitor events synchronously (FR40).
-
#initialize(config:, log_manager:, http_client:, data_store_manager:, event_manager:, data_manager:, api_manager:, experience_manager: nil, feature_manager: nil, segments_manager: nil) ⇒ Client
constructor
A new instance of Client.
-
#on(event) {|payload, err| ... } ⇒ self
Subscribe to a lifecycle event.
-
#postfork ⇒ self
Manually re-arm the SDK after a fork (Story 4.4 AC#4 — frozen API name
postfork).
Constructor Details
#initialize(config:, log_manager:, http_client:, data_store_manager:, event_manager:, data_manager:, api_manager:, experience_manager: nil, feature_manager: nil, segments_manager: nil) ⇒ Client
Returns a new instance of Client.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/convert_sdk/client.rb', line 66 def initialize(config:, log_manager:, http_client:, data_store_manager:, event_manager:, data_manager:, api_manager:, experience_manager: nil, feature_manager: nil, segments_manager: nil) @config = config @log_manager = log_manager @http_client = http_client @data_store_manager = data_store_manager @event_manager = event_manager @data_manager = data_manager @api_manager = api_manager @experience_manager = experience_manager @feature_manager = feature_manager @segments_manager = segments_manager # The lazily-started config-refresh BackgroundTimer (Story 2.7). Created # here (interval bound, registered with ForkGuard) but NEVER started in the # factory — #ensure_refresh_timer! starts it on first decision-path use # (NFR4). A nil data_refresh_interval makes #start a guarded no-op (2.6), # so timer-off mode never spawns a thread. @refresh_timer = build_refresh_timer # Wire the synchronous timer-off refresh callable into the DataManager so # its decision-time TTL check (#ensure_fresh_config!) runs one full refresh # cycle through the single HTTP port (the I/O happens under DataManager's # thundering-herd fetch mutex, never the config mutex). @data_manager.refetch = -> { refresh_config } bootstrap_config # Story 4.4 AC#5 — register the PID-guarded at_exit flush ONCE per client # (the gem's ONLY at_exit site; the third and final single-site after the # BackgroundTimer thread-spawn site and the ForkGuard _fork site). # Registering an at_exit handler creates NO thread (NFR4-safe). The test # harness disables live registration (the handler body is tested directly). register_at_exit_flush rescue StandardError => e # Construction must never crash the host: log and continue config-less. @log_manager.error("Client#initialize: #{e.class}: #{e.}") end |
Instance Attribute Details
#api_manager ⇒ ApiManager (readonly)
Returns the outbound event queue + delivery surface (Story 4.1).
109 110 111 |
# File 'lib/convert_sdk/client.rb', line 109 def api_manager @api_manager end |
#config ⇒ Config (readonly)
Returns the configuration this client was built with.
103 104 105 |
# File 'lib/convert_sdk/client.rb', line 103 def config @config end |
#data_manager ⇒ DataManager (readonly)
Returns the config snapshot reader surface.
106 107 108 |
# File 'lib/convert_sdk/client.rb', line 106 def data_manager @data_manager end |
Instance Method Details
#config_available? ⇒ Boolean
Returns true once a config snapshot is installed (degrade probe).
128 129 130 |
# File 'lib/convert_sdk/client.rb', line 128 def config_available? @data_manager.config_available? end |
#create_context(visitor_id = nil, attributes = nil) ⇒ Context?
Create a per-visitor decisioning ConvertSdk::Context — the object an integrator holds for the lifetime of one request/job.
The visitor_id is validated for presence (blank/nil → an error log line
nilreturn, NOT anArgumentError: validation here is request-time, and the never-crash contract forbids raising into the host on a per-request call; only ConvertSdk.create's config-time misconfiguration raises). Creation is the SDK's "first use" trigger, so it fires the lazy-start refresh-timer hook (#ensure_refresh_timer!, NFR4) — no threads start before the first context.
Each call returns a NEW, independent ConvertSdk::Context (no caching, no shared mutable in-memory visitor state across instances — FR12); contexts for the same visitor share only the store-backed StoreData (stickiness). Attributes are deep-stringified at the ConvertSdk::Context boundary.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/convert_sdk/client.rb', line 150 def create_context(visitor_id = nil, attributes = nil) if visitor_id.nil? || (visitor_id.respond_to?(:strip) && visitor_id.strip.empty?) @log_manager.error("Client#create_context: blank visitor_id; returning nil") return nil end # Story 4.4 — re-arm when a Process.daemon bypass left owner_pid stale. # Mirrors ApiManager#guard_fork_boundary and Client#postfork: after rearm! # marks the inherited refresh timer dead, the ensure_refresh_timer! call # below spawns a fresh thread (BackgroundTimer#start is a no-op only when # @running is true; mark_dead resets it to false). The check is a free PID # comparison; always false on JRuby. ForkGuard.rearm! if ForkGuard.forked? # Context creation is "first use" — lazily arm the background refresh timer. ensure_refresh_timer! build_context(visitor_id, attributes) rescue StandardError => e @log_manager.error("Client#create_context: #{e.class}: #{e.}") nil end |
#ensure_fresh_config! ⇒ self
Decision-time freshness hook for timer-off mode (Lambda/CLI). Delegates to DataManager#ensure_fresh_config!, which performs an on-demand TTL check and a synchronous, thundering-herd-guarded refetch when the cached config is stale. A no-op when the refresh timer is enabled. Never raises.
191 192 193 194 195 196 197 |
# File 'lib/convert_sdk/client.rb', line 191 def ensure_fresh_config! @data_manager.ensure_fresh_config! self rescue StandardError => e @log_manager.error("Client#ensure_fresh_config!: #{e.class}: #{e.}") self end |
#ensure_refresh_timer! ⇒ self
Lazily start the background config-refresh timer (NFR4 — never in the
factory). Called at the first decision-path entry (consumed by 2.8/2.11);
idempotent (2.6 BackgroundTimer#start is idempotent and re-arms after a
fork). A nil data_refresh_interval makes this a guarded no-op (no thread
is ever created — timer-off mode). Never raises into the host.
178 179 180 181 182 183 184 |
# File 'lib/convert_sdk/client.rb', line 178 def ensure_refresh_timer! @refresh_timer.start self rescue StandardError => e @log_manager.error("Client#ensure_refresh_timer!: #{e.class}: #{e.}") self end |
#flush(reason = nil) ⇒ self Also known as: release_queues
Explicitly release the queued visitor events synchronously (FR40). THE single flush entry point — delegates to ApiManager#release_queue, which drains-and-swaps inside the queue lock and POSTs OUTSIDE it (the enqueue path is never blocked on network I/O). A failed POST is logged inside the ApiManager and never raised; the full failed-POST queue-retention behaviour lands in Story 4.2. An empty queue is a no-op.
release_queues is the frozen-name alias (FR40) and shares this exact path.
NOTE (Story 4.4 seam): this is the single point where a ForkGuard PID check will gate the flush in a forked child — added here so all flush callers (explicit, at_exit, timer) inherit it from one place.
Never raises into the host (NFR9 never-crash boundary).
216 217 218 219 220 221 222 |
# File 'lib/convert_sdk/client.rb', line 216 def flush(reason = nil) @api_manager.release_queue(reason) self rescue StandardError => e @log_manager.error("Client#flush: #{e.class}: #{e.}") self end |
#on(event) {|payload, err| ... } ⇒ self
Subscribe to a lifecycle event. Public API; delegates to EventManager#on (which normalises SystemEvents constants and matching strings to one key and replays deferred one-shot events to late subscribers).
119 120 121 122 123 124 125 |
# File 'lib/convert_sdk/client.rb', line 119 def on(event, &) @event_manager.on(event, &) self rescue StandardError => e @log_manager.error("Client#on: #{e.class}: #{e.}") self end |
#postfork ⇒ self
Manually re-arm the SDK after a fork (Story 4.4 AC#4 — frozen API name
postfork). The exotic-setup escape hatch: in the default Puma/Unicorn/
Sidekiq deployments the Process._fork hook (ForkGuard) and the PID checks
at flush boundaries detect forks AUTOMATICALLY with zero configuration, so
postfork is rarely needed. It exists for setups that bypass _fork
entirely and never reach a flush boundary in time (or integrators who prefer
an explicit +on_worker_boot+/+after_fork+ call, LaunchDarkly-style).
Delegates to the SAME ForkGuard.rearm! path as automatic detection: marks both registered timers dead (lazy re-arm on next use), clears queue ownership in this process, and resets the owning PID. Idempotent (calling it in the owning process simply resets owner_pid to the current PID and re-fires the harmless clears). Never raises into the host (NFR9).
240 241 242 243 244 245 246 |
# File 'lib/convert_sdk/client.rb', line 240 def postfork ForkGuard.rearm! self rescue StandardError => e @log_manager.error("Client#postfork: #{e.class}: #{e.}") self end |