Class: ConvertSdk::ApiManager Private
- Inherits:
-
Object
- Object
- ConvertSdk::ApiManager
- Defined in:
- lib/convert_sdk/api_manager.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
The outbound delivery manager — it owns the VisitorsQueue, the tracking endpoint, queue release, and THE wire-payload builder.
Wire-translation boundary #2 (the only outbound converter)
Config#to_internal is the single INBOUND snake_case=>camelCase site; this
class's payload builder is the single OUTBOUND one. Everything in between —
StoreData, the queued events — is ALREADY wire-shaped, string-keyed data.
The payload is therefore built EXCLUSIVELY here as string-keyed camelCase
hashes and serialized with JSON.generate — never string-concatenated JSON,
never symbol keys anywhere in the wire hashes. The result is byte-identical to
the JS wire contract (+api-manager.ts:197-234+).
The payload shape
{
"accountId" => …, "projectId" => …,
"enrichData" => false, "source" => "ruby-sdk",
"visitors" => [
{ "visitorId" => …, "segments" => {…}?, "events" => [ {…}, … ] }
]
}
POSTed to {track_endpoint with [project_id] replaced}/track/{sdkKey} via the
single HttpClient port (the ConvertAgent User-Agent invariant rides
automatically; an Authorization: Bearer {secret} header is passed through the
port's headers param when a secret is configured — the port enforces the
HTTPS-only guard). An empty queue is a no-op.
enrichData / source (verified against JS source)
enrichData is false: the JS formula is !objectDeepValue(config,'dataStore')
(+api-manager.ts:94+), which is false whenever a dataStore is configured; the
Ruby SDK always provides at least a MemoryStore, and the research register is
silent on treating a MemoryStore-only config as "no store", so JS parity holds.
source is "ruby-sdk" — the Ruby analogue of JS +config?.network?.source ||
'js-sdk'+ (+api-manager.ts:115+).
Lock discipline (NFR2/NFR13)
#release_queue drains the queue with an atomic drain-and-swap INSIDE the queue lock, then builds the payload and performs the HTTP POST OUTSIDE the lock. The enqueue path never blocks the caller on network I/O. A failed POST does NOT raise (the full queue-retention behaviour lands in Story 4.2); it is logged and swallowed so the Client boundary never crashes the host.
Constant Summary collapse
- SOURCE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
The SDK identifier sent as the tracking payload
source(JS analogue ofconfig?.network?.source || 'js-sdk'— api-manager.ts:115). "ruby-sdk"- ENRICH_DATA =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
JS parity:
!objectDeepValue(config,'dataStore')is false whenever a dataStore is configured, and Ruby always provides one (api-manager.ts:94). false
Instance Attribute Summary collapse
-
#queue ⇒ VisitorsQueue
readonly
private
The underlying per-visitor event queue.
Instance Method Summary collapse
-
#enqueue(visitor_id, event, segments: nil) ⇒ void
private
Enqueue one wire-shaped event for a visitor (delegates to the queue's per-visitor merge), then drive the two automatic delivery triggers:.
-
#initialize(config:, data_manager:, http_client:, event_manager:, log_manager:) ⇒ ApiManager
constructor
private
A new instance of ApiManager.
-
#release_queue(reason = nil) ⇒ void
private
Release the queue — the SINGLE delivery implementation all three triggers (explicit
flush, size, interval) converge on.
Constructor Details
#initialize(config:, data_manager:, http_client:, event_manager:, log_manager:) ⇒ ApiManager
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a new instance of ApiManager.
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 |
# File 'lib/convert_sdk/api_manager.rb', line 70 def initialize(config:, data_manager:, http_client:, event_manager:, log_manager:) @config = config @data_manager = data_manager @http_client = http_client @event_manager = event_manager @log_manager = log_manager @queue = VisitorsQueue.new(log_manager: log_manager) # The SECOND and FINAL BackgroundTimer instance (architecture Decision 6 — # one class, two instances: the refresh timer is 2.7's, this is the flush # timer, owned here). It is built and registered with ForkGuard NOW but # NEVER started in the factory (NFR4 — no threads until first use); it is # lazily started on the first enqueue. A +nil+ flush_interval is the # timer-off mode (BackgroundTimer#start is then a guarded no-op — the # Lambda recipe for 4.6: explicit flush + size trigger still deliver). @flush_timer = BackgroundTimer.new( interval: @config.flush_interval, log_manager: log_manager, name: "flush" ) { flush_tick } ForkGuard.register_timer(@flush_timer) # Story 4.4 — child queue-ownership clear. ForkGuard fires this callback in # a forked child (after marking timers dead). The child inherits a COPY of # the parent's queued events; clearing it here is what makes the child # start EMPTY so it never double-delivers the parent's events (the parent's # timer still runs there and delivers them). ForkGuard stays generic — it # knows nothing about the queue; ApiManager owns its own clear (architecture # Decision 6 callback-registry design). ForkGuard.register_child_callback(-> { clear_queue_ownership }) end |
Instance Attribute Details
#queue ⇒ VisitorsQueue (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the underlying per-visitor event queue.
101 102 103 |
# File 'lib/convert_sdk/api_manager.rb', line 101 def queue @queue end |
Instance Method Details
#enqueue(visitor_id, event, segments: nil) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Enqueue one wire-shaped event for a visitor (delegates to the queue's per-visitor merge), then drive the two automatic delivery triggers:
- LAZY-START the flush timer (NFR4 — the first enqueue in each process is "first use"; idempotent + re-arms after a fork via 2.6's BackgroundTimer).
- SIZE trigger — when the queue reaches
event_batch_size, release with reason"size"DIRECTLY on this thread (JS api-manager.ts:197-198). The enqueue itself is pure in-memory and the size-trigger release POSTs OUTSIDE the queue lock, so the caller is never blocked on the network (NFR2) — only the brief queue-lock acquisition.
119 120 121 122 123 124 |
# File 'lib/convert_sdk/api_manager.rb', line 119 def enqueue(visitor_id, event, segments: nil) guard_fork_boundary @queue.enqueue(visitor_id, event, segments: segments) ensure_flush_timer! release_queue("size") if @queue.size >= @config.event_batch_size end |
#release_queue(reason = nil) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Release the queue — the SINGLE delivery implementation all three triggers
(explicit flush, size, interval) converge on. Drain-and-swap INSIDE the
queue lock, then build the wire payload and POST it OUTSIDE the lock (the
enqueue path is never blocked on network I/O — NFR2). An empty queue is a
no-op.
On SUCCESS: an info line and the SystemEvents::API_QUEUE_RELEASED
lifecycle event fire with a JS-parity payload (+reason+ + visitor count).
On FAILURE (a failed HttpClient::Response — story 1.5 returns it WITHOUT
raising): the drained visitors are RE-ENQUEUED via VisitorsQueue#requeue
(preserving per-visitor merge), a warn records the retention, and NO
event fires. There is NO inline retry — a frozen divergence from PHP's
3-attempt backoff; the next attempt is the next timer tick or size trigger.
The bounded queue (drop-oldest + warn at the 1000 cap) keeps a sustained
outage from growing host memory without bound (NFR10).
Never raises into the host (NFR9): a rescue StandardError logs and
swallows. Note the re-enqueue happens BEFORE the rescue so a transport-layer
failed Response retains; a raise from the rescue path itself (after the
drain) cannot retain, but the never-crash contract takes precedence there.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/convert_sdk/api_manager.rb', line 150 def release_queue(reason = nil) # Story 4.4 — the SINGLE fork-safety PID boundary all three flush triggers # (explicit flush, size, interval) inherit from one place. A cheap # ForkGuard.forked? check (an integer PID comparison — Datadog idiom) covers # the Process.daemon path that BYPASSES the _fork hook: a stale process # re-arms (marks the inherited dead timers dead, clears the inherited queue, # resets owner_pid) BEFORE proceeding. The check fires BEFORE the # empty-queue early return so a freshly daemonised process re-arms its # timers even when nothing is queued yet. guard_fork_boundary visitors = @queue.drain! return if visitors.empty? deliver(visitors, reason) rescue StandardError => e # Never-crash boundary: a delivery failure must not crash the host. @log_manager.error("ApiManager#release_queue: #{e.class}: #{e.}") end |