quonfig 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +154 -49
- data/lib/quonfig/client.rb +547 -65
- data/lib/quonfig/config_loader.rb +6 -0
- data/lib/quonfig/options.rb +34 -1
- data/lib/quonfig/sse_config_client.rb +9 -0
- data/lib/quonfig/telemetry/telemetry_reporter.rb +51 -0
- data/lib/quonfig/version.rb +1 -1
- metadata +2 -2
|
@@ -31,6 +31,12 @@ module Quonfig
|
|
|
31
31
|
# SSE does not change it).
|
|
32
32
|
attr_reader :held_generation, :install_count
|
|
33
33
|
|
|
34
|
+
# Repoint the failover-telemetry sink. The one caller is
|
|
35
|
+
# Client#after_fork_in_child: a forked child gets fresh aggregators (the
|
|
36
|
+
# parent still owns and flushes its own), and the loader has to record
|
|
37
|
+
# into the child's copy rather than the inherited one (qfg-lv4n.1).
|
|
38
|
+
attr_writer :failover_aggregator
|
|
39
|
+
|
|
34
40
|
# +store+: the Quonfig::ConfigStore to populate on successful fetch.
|
|
35
41
|
# +options+: a Quonfig::Options instance (supplies sdk_key + config_api_urls).
|
|
36
42
|
# +logger+: optional logger override (defaults to module LOG).
|
data/lib/quonfig/options.rb
CHANGED
|
@@ -140,10 +140,21 @@ module Quonfig
|
|
|
140
140
|
|
|
141
141
|
# In datadir mode the SDK evaluates config from a local workspace and does
|
|
142
142
|
# not connect to the delivery service.
|
|
143
|
+
#
|
|
144
|
+
# NOTE: this says nothing about telemetry. Datadir mode is a config-DELIVERY
|
|
145
|
+
# mode; usage telemetry still flows whenever an SDK key is configured. See
|
|
146
|
+
# #telemetry_allowed?.
|
|
143
147
|
def local_only?
|
|
144
148
|
!@datadir.nil?
|
|
145
149
|
end
|
|
146
150
|
|
|
151
|
+
# True when an SDK key is configured. The SDK key is what identifies the
|
|
152
|
+
# workspace telemetry is attributed to, so it is the single gate on whether
|
|
153
|
+
# any collection happens at all (qfg-j001 / qfg-5x9x).
|
|
154
|
+
def sdk_key?
|
|
155
|
+
!(@sdk_key.nil? || @sdk_key.to_s.empty?)
|
|
156
|
+
end
|
|
157
|
+
|
|
147
158
|
def datadir?
|
|
148
159
|
!@datadir.nil?
|
|
149
160
|
end
|
|
@@ -202,6 +213,13 @@ module Quonfig
|
|
|
202
213
|
# Debounce window in milliseconds. Filesystem events arriving
|
|
203
214
|
# inside the window are coalesced into a single re-read. Ignored
|
|
204
215
|
# when +:data_dir_auto_reload+ is +false+.
|
|
216
|
+
# @option options [Boolean] :allow_telemetry_in_local_mode (false)
|
|
217
|
+
# @deprecated No-op since 1.3.0 (qfg-5x9x). Telemetry is gated on SDK-key
|
|
218
|
+
# presence alone, so datadir mode no longer suppresses it and this flag
|
|
219
|
+
# has nothing left to unlock. Still accepted so existing callers keep
|
|
220
|
+
# working; slated for removal in 2.0.0. To turn telemetry off, use the
|
|
221
|
+
# standard opt-outs (+:collect_evaluation_summaries+ +false+,
|
|
222
|
+
# +:context_upload_mode+ +:none+).
|
|
205
223
|
def init(
|
|
206
224
|
api_urls: nil,
|
|
207
225
|
telemetry_url: nil,
|
|
@@ -226,6 +244,7 @@ module Quonfig
|
|
|
226
244
|
context_max_size: DEFAULT_MAX_EVAL_SUMMARIES,
|
|
227
245
|
collect_evaluation_summaries: true,
|
|
228
246
|
collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
|
|
247
|
+
# Deprecated no-op since 1.3.0 (qfg-5x9x) — see the @option doc above.
|
|
229
248
|
allow_telemetry_in_local_mode: false,
|
|
230
249
|
global_context: {},
|
|
231
250
|
logger_key: nil,
|
|
@@ -287,6 +306,7 @@ module Quonfig
|
|
|
287
306
|
@collect_sync_interval = collect_sync_interval
|
|
288
307
|
@collect_evaluation_summaries = collect_evaluation_summaries
|
|
289
308
|
@collect_max_evaluation_summaries = collect_max_evaluation_summaries
|
|
309
|
+
# Retained for back-compat only; nothing reads it (qfg-5x9x).
|
|
290
310
|
@allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
|
|
291
311
|
@is_fork = false
|
|
292
312
|
@global_context = global_context
|
|
@@ -342,8 +362,21 @@ module Quonfig
|
|
|
342
362
|
end
|
|
343
363
|
end
|
|
344
364
|
|
|
365
|
+
# The telemetry gate: an explicit per-collector opt-out (+option+) AND the
|
|
366
|
+
# presence of an SDK key. Mode is deliberately NOT part of this decision.
|
|
367
|
+
#
|
|
368
|
+
# qfg-5x9x: this used to read
|
|
369
|
+
# `option && (!local_only? || @allow_telemetry_in_local_mode)`
|
|
370
|
+
# which zeroed every collect_max_* the moment a datadir was set, so a
|
|
371
|
+
# datadir client holding a perfectly valid SDK key built no aggregators and
|
|
372
|
+
# therefore never constructed a reporter — it emitted nothing at all.
|
|
373
|
+
# Datadir + SDK key is a supported combination and telemetry has to flow
|
|
374
|
+
# there like it does in delivery mode. Conversely a keyless client (the
|
|
375
|
+
# open-source / no-account path) has no workspace to attribute data to, so
|
|
376
|
+
# nothing is collected and the reporter never starts. Mirrors sdk-node's
|
|
377
|
+
# `isTelemetryEnabled` and sdk-go's `Options.TelemetryEnabled()`.
|
|
345
378
|
def telemetry_allowed?(option)
|
|
346
|
-
option &&
|
|
379
|
+
option && sdk_key?
|
|
347
380
|
end
|
|
348
381
|
|
|
349
382
|
def remove_trailing_slash(url)
|
|
@@ -129,6 +129,15 @@ module Quonfig
|
|
|
129
129
|
@worker = Thread.new { run_loop(&on_envelope) }
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# True while the worker thread that owns the stream (and its reconnect
|
|
133
|
+
# loop) is running. Stays true across a reconnect — the worker owns the
|
|
134
|
+
# retry, so a blip is not "no channel". Used by Client#connection_state
|
|
135
|
+
# so that diagnostic derives from liveness rather than a stored flag
|
|
136
|
+
# (qfg-lv4n.1).
|
|
137
|
+
def alive?
|
|
138
|
+
@worker&.alive? || false
|
|
139
|
+
end
|
|
140
|
+
|
|
132
141
|
# Shut down. Interrupts the in-flight stream by closing the underlying
|
|
133
142
|
# socket from this thread — the worker thread observes the resulting
|
|
134
143
|
# IOError, sees @stopped == true, and exits cleanly.
|
|
@@ -51,6 +51,9 @@ module Quonfig
|
|
|
51
51
|
@stopped = Concurrent::AtomicBoolean.new(false)
|
|
52
52
|
@thread = nil
|
|
53
53
|
@at_exit_registered = false
|
|
54
|
+
# Set on #start. Everything that can EMIT is gated on it so a forked
|
|
55
|
+
# child never speaks for the process that created this reporter.
|
|
56
|
+
@owner_pid = nil
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
def enabled?
|
|
@@ -80,6 +83,11 @@ module Quonfig
|
|
|
80
83
|
return if @thread&.alive?
|
|
81
84
|
return unless enabled?
|
|
82
85
|
|
|
86
|
+
# Claim ownership for THIS process. fork(2) copies the reporter, its
|
|
87
|
+
# aggregators, and the process-wide at_exit closure registered below;
|
|
88
|
+
# the pid recorded here is what lets the copy know it is not the
|
|
89
|
+
# owner and must stay silent (qfg-lv4n.1, dd-trace-rb's pattern).
|
|
90
|
+
@owner_pid = Process.pid
|
|
83
91
|
@stopped.make_false
|
|
84
92
|
register_at_exit_handler
|
|
85
93
|
@thread = Thread.new do
|
|
@@ -106,6 +114,8 @@ module Quonfig
|
|
|
106
114
|
end
|
|
107
115
|
|
|
108
116
|
def stop
|
|
117
|
+
return if foreign_process?('stop')
|
|
118
|
+
|
|
109
119
|
@stopped.make_true
|
|
110
120
|
thread = @thread
|
|
111
121
|
@thread = nil
|
|
@@ -121,7 +131,13 @@ module Quonfig
|
|
|
121
131
|
|
|
122
132
|
# Drain all aggregators and POST the batch. Public so tests can
|
|
123
133
|
# trigger a sync without waiting for the background loop.
|
|
134
|
+
#
|
|
135
|
+
# Silent in any process other than the one that started the reporter:
|
|
136
|
+
# after a fork the child holds a full copy of the PARENT's un-flushed
|
|
137
|
+
# window, and the parent is still going to flush it itself.
|
|
124
138
|
def sync
|
|
139
|
+
return if foreign_process?('sync')
|
|
140
|
+
|
|
125
141
|
events = []
|
|
126
142
|
if (summaries_event = @evaluation_summaries_aggregator&.drain_event)
|
|
127
143
|
events << summaries_event
|
|
@@ -151,8 +167,41 @@ module Quonfig
|
|
|
151
167
|
@at_exit_registered
|
|
152
168
|
end
|
|
153
169
|
|
|
170
|
+
# Pid of the process that started this reporter, or nil if it was never
|
|
171
|
+
# started. Visible for tests / diagnostics.
|
|
172
|
+
attr_reader :owner_pid
|
|
173
|
+
|
|
174
|
+
# Called on the INHERITED reporter in a forked child, from
|
|
175
|
+
# +Quonfig::Client#after_fork_in_child+, once the child has dropped its
|
|
176
|
+
# reference to it. Makes the copied window unreachable so nothing can
|
|
177
|
+
# ever emit it — belt to the +@owner_pid+ braces.
|
|
178
|
+
#
|
|
179
|
+
# Deliberately does NOT stop, close, or join anything: the thread does
|
|
180
|
+
# not exist in the child, and the HTTP connection's fd is shared with
|
|
181
|
+
# the parent.
|
|
182
|
+
def discard_inherited!
|
|
183
|
+
@stopped.make_true
|
|
184
|
+
@thread = nil
|
|
185
|
+
@context_shape_aggregator = nil
|
|
186
|
+
@example_contexts_aggregator = nil
|
|
187
|
+
@evaluation_summaries_aggregator = nil
|
|
188
|
+
@failover_aggregator = nil
|
|
189
|
+
end
|
|
190
|
+
|
|
154
191
|
private
|
|
155
192
|
|
|
193
|
+
# True when this reporter belongs to a different process — i.e. we are
|
|
194
|
+
# a fork(2) copy. Never true before #start (nothing has been claimed,
|
|
195
|
+
# and nothing was registered at_exit either).
|
|
196
|
+
def foreign_process?(operation)
|
|
197
|
+
return false if @owner_pid.nil?
|
|
198
|
+
return false if @owner_pid == Process.pid
|
|
199
|
+
|
|
200
|
+
LOG.debug "[quonfig] Telemetry #{operation} skipped in forked child " \
|
|
201
|
+
"pid=#{Process.pid} owner_pid=#{@owner_pid}"
|
|
202
|
+
true
|
|
203
|
+
end
|
|
204
|
+
|
|
156
205
|
# Rails / Passenger / Puma workers often terminate via SIGTERM without
|
|
157
206
|
# a chance to call Client#stop. Register a Kernel.at_exit hook on
|
|
158
207
|
# first start so the in-flight batch still gets flushed.
|
|
@@ -174,6 +223,8 @@ module Quonfig
|
|
|
174
223
|
# no-op. Bounded so a stuck reporter thread or dead telemetry
|
|
175
224
|
# endpoint can't hang process exit.
|
|
176
225
|
def final_drain_on_exit
|
|
226
|
+
return if foreign_process?('at_exit drain')
|
|
227
|
+
|
|
177
228
|
@stopped.make_true
|
|
178
229
|
thread = @thread
|
|
179
230
|
@thread = nil
|
data/lib/quonfig/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quonfig
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeff Dwyer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|