quonfig 0.0.15 → 0.0.17
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 +13 -0
- data/README.md +143 -12
- data/lib/quonfig/client.rb +230 -22
- data/lib/quonfig/datadir_watcher.rb +113 -0
- data/lib/quonfig/options.rb +25 -2
- data/lib/quonfig/sse_config_client.rb +536 -225
- data/lib/quonfig/version.rb +1 -1
- data/lib/quonfig.rb +3 -1
- data/quonfig.gemspec +4 -1
- metadata +8 -7
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
5
|
+
module Quonfig
|
|
6
|
+
# Watches a datadir for changes and fires +on_change+ once per debounced
|
|
7
|
+
# burst. Wraps the `listen` gem (https://github.com/guard/listen), which
|
|
8
|
+
# uses platform-native backends (FSEvents on macOS, inotify on Linux,
|
|
9
|
+
# polling fallback on Windows).
|
|
10
|
+
#
|
|
11
|
+
# The caller owns parse-then-swap: this class only fires the trigger.
|
|
12
|
+
# Registration failures (read-only fs, immutable container, native backend
|
|
13
|
+
# missing) are surfaced via +on_error+; in that case +start+ returns
|
|
14
|
+
# +false+ and no listener is held.
|
|
15
|
+
#
|
|
16
|
+
# Mirrors sdk-node/src/datadirWatcher.ts (qfg-mol-0kr) modulo Ruby idioms:
|
|
17
|
+
# listen does not have an equivalent to Node's `fs.watch({recursive:true})`,
|
|
18
|
+
# but it watches recursively by default.
|
|
19
|
+
class DatadirWatcher
|
|
20
|
+
# Indirection seam for tests. Production code uses ::Listen; tests can
|
|
21
|
+
# swap in a class that raises from `.to` to exercise the registration-
|
|
22
|
+
# failure path without needing a read-only filesystem.
|
|
23
|
+
LISTEN_FACTORY = nil # resolved lazily so the gem can be required late
|
|
24
|
+
|
|
25
|
+
def initialize(datadir:, debounce_ms:, on_change:, on_error:)
|
|
26
|
+
@datadir = datadir
|
|
27
|
+
@debounce_seconds = debounce_ms.to_f / 1000.0
|
|
28
|
+
@on_change = on_change
|
|
29
|
+
@on_error = on_error
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
@scheduled_task = nil
|
|
32
|
+
@listener = nil
|
|
33
|
+
@closed = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Start the underlying file watcher. Returns true on success, false if
|
|
37
|
+
# registration failed (in which case +on_error+ has already been called
|
|
38
|
+
# and the caller should continue without auto-reload).
|
|
39
|
+
#
|
|
40
|
+
# Blocks until the listener is in its :processing_events state (or a
|
|
41
|
+
# short safety timeout elapses) so a customer writing to the datadir
|
|
42
|
+
# immediately after the Client constructor returns is detected, rather
|
|
43
|
+
# than racing the listen backend's async setup.
|
|
44
|
+
def start
|
|
45
|
+
resolved = File.realpath(@datadir)
|
|
46
|
+
factory = self.class::LISTEN_FACTORY || ::Listen
|
|
47
|
+
@listener = factory.to(resolved) do |_modified, _added, _removed|
|
|
48
|
+
schedule_reload
|
|
49
|
+
end
|
|
50
|
+
@listener.start
|
|
51
|
+
wait_for_listener_ready
|
|
52
|
+
true
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
@on_error.call(e)
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Stop the watcher and cancel any pending debounce. Idempotent.
|
|
59
|
+
def stop
|
|
60
|
+
task, listener = @mutex.synchronize do
|
|
61
|
+
@closed = true
|
|
62
|
+
t = @scheduled_task
|
|
63
|
+
l = @listener
|
|
64
|
+
@scheduled_task = nil
|
|
65
|
+
@listener = nil
|
|
66
|
+
[t, l]
|
|
67
|
+
end
|
|
68
|
+
begin
|
|
69
|
+
task&.cancel
|
|
70
|
+
rescue StandardError
|
|
71
|
+
# best-effort; caller already in shutdown
|
|
72
|
+
end
|
|
73
|
+
begin
|
|
74
|
+
listener&.stop
|
|
75
|
+
rescue StandardError
|
|
76
|
+
# best-effort
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Block briefly until the listener reports :processing_events. Listen's
|
|
83
|
+
# state machine supports wait_for_state; we cap at 500 ms so a broken
|
|
84
|
+
# backend cannot wedge the SDK boot. (The native FSEvents backend on
|
|
85
|
+
# macOS still has ~100ms latency *after* this returns — that is a
|
|
86
|
+
# property of the OS, not something we can synchronize away. Tests that
|
|
87
|
+
# need to observe the first post-init write should sleep accordingly.)
|
|
88
|
+
def wait_for_listener_ready
|
|
89
|
+
return unless @listener.respond_to?(:wait_for_state)
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
@listener.wait_for_state(:processing_events, timeout: 0.5)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
# If the FSM doesn't transition, keep going — events may still flow.
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def schedule_reload
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
return if @closed
|
|
101
|
+
|
|
102
|
+
@scheduled_task&.cancel
|
|
103
|
+
on_change = @on_change
|
|
104
|
+
@scheduled_task = Concurrent::ScheduledTask.execute(@debounce_seconds) do
|
|
105
|
+
# Re-check closed under the mutex so a stop() landing between cancel
|
|
106
|
+
# and execute cannot resurrect a fired callback.
|
|
107
|
+
should_fire = @mutex.synchronize { !@closed }
|
|
108
|
+
on_change.call if should_fire
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/quonfig/options.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Quonfig
|
|
|
6
6
|
# Options passed to Quonfig::Client at construction time.
|
|
7
7
|
class Options
|
|
8
8
|
attr_reader :sdk_key, :environment, :api_urls, :sse_api_urls, :telemetry_destination, :config_api_urls,
|
|
9
|
-
:on_no_default, :initialization_timeout_sec, :on_init_failure, :collect_sync_interval, :datadir, :enable_sse, :enable_polling, :poll_interval, :global_context, :logger_key, :logger, :enable_quonfig_user_context
|
|
9
|
+
:on_no_default, :initialization_timeout_sec, :on_init_failure, :collect_sync_interval, :datadir, :enable_sse, :enable_polling, :poll_interval, :global_context, :logger_key, :logger, :enable_quonfig_user_context,
|
|
10
|
+
:data_dir_auto_reload, :data_dir_auto_reload_debounce_ms
|
|
10
11
|
attr_accessor :is_fork
|
|
11
12
|
|
|
12
13
|
module ON_INITIALIZATION_FAILURE
|
|
@@ -119,6 +120,24 @@ module Quonfig
|
|
|
119
120
|
|
|
120
121
|
private
|
|
121
122
|
|
|
123
|
+
# @!method initialize(options = {})
|
|
124
|
+
# @option options [Boolean] :data_dir_auto_reload (false)
|
|
125
|
+
# Datadir mode only. When +true+, the SDK watches the workspace
|
|
126
|
+
# directory and re-reads the envelope whenever files inside it
|
|
127
|
+
# change. Parse-then-swap: a failed parse keeps the previous
|
|
128
|
+
# envelope. Default debounce window is 200 ms; tune via
|
|
129
|
+
# +:data_dir_auto_reload_debounce_ms+. Listen-registration failure
|
|
130
|
+
# (read-only fs, missing native backend) is logged and the SDK
|
|
131
|
+
# continues serving the envelope captured at init.
|
|
132
|
+
#
|
|
133
|
+
# On Ruby 3.1+ the SDK's +Process._fork+ hook tears the watcher
|
|
134
|
+
# down in the parent before fork and rebuilds it in each child;
|
|
135
|
+
# no customer wiring is required for Puma cluster / Unicorn /
|
|
136
|
+
# Sidekiq / Resque. See README "Fork safety".
|
|
137
|
+
# @option options [Integer] :data_dir_auto_reload_debounce_ms (200)
|
|
138
|
+
# Debounce window in milliseconds. Filesystem events arriving
|
|
139
|
+
# inside the window are coalesced into a single re-read. Ignored
|
|
140
|
+
# when +:data_dir_auto_reload+ is +false+.
|
|
122
141
|
def init(
|
|
123
142
|
api_urls: nil,
|
|
124
143
|
telemetry_url: nil,
|
|
@@ -141,7 +160,9 @@ module Quonfig
|
|
|
141
160
|
global_context: {},
|
|
142
161
|
logger_key: nil,
|
|
143
162
|
logger: nil,
|
|
144
|
-
enable_quonfig_user_context: false
|
|
163
|
+
enable_quonfig_user_context: false,
|
|
164
|
+
data_dir_auto_reload: false,
|
|
165
|
+
data_dir_auto_reload_debounce_ms: 200
|
|
145
166
|
)
|
|
146
167
|
@sdk_key = sdk_key
|
|
147
168
|
@environment = environment
|
|
@@ -163,6 +184,8 @@ module Quonfig
|
|
|
163
184
|
@logger_key = logger_key
|
|
164
185
|
@logger = logger
|
|
165
186
|
@enable_quonfig_user_context = enable_quonfig_user_context
|
|
187
|
+
@data_dir_auto_reload = data_dir_auto_reload
|
|
188
|
+
@data_dir_auto_reload_debounce_ms = data_dir_auto_reload_debounce_ms
|
|
166
189
|
|
|
167
190
|
# defaults that may be overridden by context_upload_mode
|
|
168
191
|
@collect_shapes = false
|