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.
@@ -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
@@ -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