quonfig-openfeature 1.0.2 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d3186451493a7eeba724e7fb79fce280cb82e9dea6e71a6617db94daca23c5
4
- data.tar.gz: eda5a0634c8f48442467a04dd1f454b98531d9b3f6ebc325f97b42e940f32fbe
3
+ metadata.gz: 518ef7e90608d00dc60292d93fa9c9b6a14037df9194597a1cc309a4c02ea166
4
+ data.tar.gz: bc52ae8cda109bd7aa20dcc5e0a235716a503d497987d4000d45a97ae6541bba
5
5
  SHA512:
6
- metadata.gz: d14775ff24ddae1e6f60acf8b0d41de6e6232c41da78bbc3ae7b180f317d6efad58c6d3e347057884244bfaa05746f2cd9f77c1f9110dd33aa652bbe2ee46fd0
7
- data.tar.gz: f7346df35c6cfaa28d5b3282cb2aa4a218cb4cdec12407497ede4505240ab3eafadbc08eb016e07312a9d7ea0dc44a2755e9e778766b7a1423d04978ffeb538a
6
+ metadata.gz: '080feecf4971e26beff1c05427d6ffa0d9c494c26696968502ae7fe8ea462e6eb770a1a57d6486bf8d39093133902b8672cebea3f3f4cd3fc4a92ed0355ba5ae'
7
+ data.tar.gz: e783174efaa4e7c8577259bec7ebaa7ccc3eb1f900a844fed68e1449cabffeada4b2925fc5d650f15c51c52d27da35a79380bec8d716e6cf4259bd4f5d93ee6b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 - 2026-10-23
4
+
5
+ - **UPGRADE: raise the `quonfig` floor from `>= 1.10.0` to `>= 1.11.0`. Requires sdk-ruby 1.11.0:
6
+ SSE event and config body size caps, `Client#callback_errors_total`.** The provider code does not
7
+ change for 1.11.0. The new floor means every install of this provider gets those sdk-ruby changes:
8
+ an oversized SSE event (over 4 MiB) or config body (over 16 MiB) is treated as a failed attempt
9
+ instead of being read without limit, and `Dir.home` failures no longer raise from `Client.new`.
10
+ Read the 1.11.0 entry in the sdk-ruby CHANGELOG. Build and test against `quonfig` 1.11.0
11
+ (Gemfile.lock).
12
+ - **UPGRADE: raise the `quonfig` floor from `>= 1.7.2` to `>= 1.10.0` (qfg-9p4i.12).** The provider
13
+ now uses sdk-ruby 1.10.0's init-failure surface (`on_init_failure: :fail | :wait | :continue`,
14
+ `init_timeout_ms`, `Client#ready?` / `#wait_for_ready`, `Quonfig::Errors::InitializationError`).
15
+ Read the 1.10.0 entry in the sdk-ruby CHANGELOG: `Client.new` now retries a failed boot until
16
+ `init_timeout_ms` (default 10s) before it raises, instead of raising on the first failed attempt.
17
+ - **Added (minor): the provider emits OpenFeature events and its status follows `on_init_failure`
18
+ (init-failure design B.5).** Pass `on_init_failure:` and `init_timeout_ms:` to `Provider.new`; they
19
+ are forwarded to `Quonfig::Client.new` as before.
20
+ - `:fail` (the default): `init` raises `Quonfig::Errors::InitializationError` when no config loads,
21
+ so `set_provider_and_wait` raises and the provider is ERROR. sdk-ruby returns no client under
22
+ `:fail`, so nothing retries in the background; the process restart is the retry.
23
+ - `:wait`: `init` blocks until config loads. The provider is NOT_READY for the whole wait, then READY.
24
+ - `:continue` with no config: `init` resolves, then the provider emits `PROVIDER_ERROR` (error code
25
+ `PROVIDER_NOT_READY`), so its status is ERROR rather than READY while every evaluation returns the
26
+ code default. When the client's background retry loads config, the provider emits
27
+ `PROVIDER_READY`. Before, the provider reported READY and emitted no events.
28
+ - A client injected with `client:` gets the same treatment: if it has no config at `init`, the
29
+ provider reports ERROR until it heals.
30
+ - **Fix: an evaluation before config loads reports `PROVIDER_NOT_READY`.** sdk-ruby 1.10.0's
31
+ `*_details` getters return error code `PROVIDER_NOT_READY` on a client with no config; the provider
32
+ mapped that to `GENERAL`. `Quonfig::Errors::InitializationError` (any reason, not only the timeout
33
+ subclass) now maps to `PROVIDER_NOT_READY` too.
34
+
35
+ ## 1.0.3 - 2026-10-16
36
+
37
+ - Build and test against `quonfig` 1.8.0 (Gemfile.lock). The runtime floor stays at `>= 1.7.2`: this
38
+ provider does not read `terminal_failure?` or `connection_state`, so nothing in it depends on
39
+ 1.8.0. A fresh `bundle install` / `bundle update quonfig` resolves to 1.8.0, which changes behavior
40
+ OpenFeature users see through this provider:
41
+ - **UPGRADE: rule operators follow one cross-SDK semantics (qfg-goi1.5.7).** Pass dates in the
42
+ evaluation context as epoch milliseconds or ISO 8601 / RFC3339 with `Z` or a numeric offset.
43
+ Date-only, offset-less and free-form date strings no longer parse, so those `PROP_BEFORE` /
44
+ `PROP_AFTER` criteria stop matching. Whole-number floats read as plain digits (`1234567.0` ->
45
+ `"1234567"`) in string operators, and `PROP_SEMVER_*` accepts abbreviated versions (`"4.1"`).
46
+ - **UPGRADE: a rejected SDK key no longer latches the client off (qfg-goi1.10.26).** A
47
+ 401/403/404/410 poll cycle enters an auth-failed state that probes every 150-300 s and recovers
48
+ by itself when the key works again, logging one ERROR (status and host, never the key) instead
49
+ of stopping the stream for the life of the process.
50
+ - Object flags (`fetch_object_value`) now get a fresh copy of the JSON value on every call, so a
51
+ caller mutating a resolved Hash or Array no longer changes later resolutions (qfg-goi1.10.3).
52
+ - Fixes: weighted-rollout `confidential` / `decryptWith` variants are redacted in telemetry
53
+ (qfg-goi1.2.39), `EnvVarParseError` messages use the cross-SDK wording (qfg-goi1.10.21), and
54
+ install/SSE robustness fixes (qfg-goi1.10.2, qfg-goi1.2.35).
55
+
3
56
  ## 1.0.2 - 2026-10-09
4
57
 
5
58
  - Raise the `quonfig` dependency floor from `>= 1.4.0` to `>= 1.7.2` so provider users inherit
data/README.md CHANGED
@@ -56,6 +56,26 @@ is_pro = client.fetch_boolean_value(flag_key: 'pro-feature', default_value: fals
56
56
  evaluation_context: ctx)
57
57
  ```
58
58
 
59
+ ## Startup failure and provider status
60
+
61
+ The provider forwards `on_init_failure:` and `init_timeout_ms:` to `Quonfig::Client.new`, and its
62
+ OpenFeature status follows the mode:
63
+
64
+ | `on_init_failure` | `init` when no config loads | Provider status | Later |
65
+ |-------------------|-----------------------------|-----------------|-------|
66
+ | `:fail` (default) | raises `Quonfig::Errors::InitializationError` after `init_timeout_ms` (at once if the SDK key is rejected) | ERROR | No client exists, so nothing retries; restart the process |
67
+ | `:wait` | blocks and retries with no upper bound | NOT_READY while waiting | READY when config loads |
68
+ | `:continue` | returns; emits `PROVIDER_ERROR` | ERROR; evaluations return your default with `PROVIDER_NOT_READY` | emits `PROVIDER_READY` when the background retry loads config |
69
+
70
+ ```ruby
71
+ provider = Quonfig::OpenFeature::Provider.new(
72
+ sdk_key: 'qf_sk_production_...',
73
+ on_init_failure: :continue, # :fail (default), :wait or :continue
74
+ init_timeout_ms: 10_000
75
+ )
76
+ OpenFeature::SDK.set_provider_and_wait(provider)
77
+ ```
78
+
59
79
  ## Context mapping
60
80
 
61
81
  OpenFeature context is flat; Quonfig context is nested by namespace. This provider
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.2
1
+ 1.1.0
@@ -21,7 +21,7 @@ module Quonfig
21
21
  return ErrorCode::TYPE_MISMATCH if defined?(::Quonfig::Errors::TypeMismatchError) && err.is_a?(::Quonfig::Errors::TypeMismatchError)
22
22
 
23
23
  if (defined?(::Quonfig::Errors::UninitializedError) && err.is_a?(::Quonfig::Errors::UninitializedError)) ||
24
- (defined?(::Quonfig::Errors::InitializationTimeoutError) && err.is_a?(::Quonfig::Errors::InitializationTimeoutError))
24
+ (defined?(::Quonfig::Errors::InitializationError) && err.is_a?(::Quonfig::Errors::InitializationError))
25
25
  return ErrorCode::PROVIDER_NOT_READY
26
26
  end
27
27
 
@@ -31,9 +31,26 @@ module Quonfig
31
31
  #
32
32
  # client = OpenFeature::SDK.build_client
33
33
  # client.fetch_boolean_value(flag_key: 'my-flag', default_value: false)
34
+ #
35
+ # Provider status follows the SDK's +on_init_failure+ mode (init-failure
36
+ # design B.5). Pass +on_init_failure:+ and +init_timeout_ms:+ through
37
+ # +Provider.new+; they are forwarded to +Quonfig::Client.new+.
38
+ #
39
+ # * +:fail+ (default): +init+ raises, so the provider is ERROR. sdk-ruby
40
+ # returns no client, so nothing retries; restart the process.
41
+ # * +:wait+: +init+ blocks until config loads (NOT_READY), then READY.
42
+ # * +:continue+: +init+ resolves; with no config yet the provider emits
43
+ # PROVIDER_ERROR (ERROR, evaluations return the default with
44
+ # PROVIDER_NOT_READY), then PROVIDER_READY when the client heals.
34
45
  class Provider
46
+ include ::OpenFeature::SDK::Provider::EventEmitter
47
+
35
48
  NAME = 'quonfig'
36
49
 
50
+ # How long the heal watcher waits for OpenFeature to record the READY it
51
+ # dispatches when +init+ returns, before it reports PROVIDER_ERROR.
52
+ INIT_DISPATCH_WAIT_S = 5
53
+
37
54
  ResolutionDetails = ::OpenFeature::SDK::Provider::ResolutionDetails
38
55
  Reason = ::OpenFeature::SDK::Provider::Reason
39
56
  ErrorCode = ::OpenFeature::SDK::Provider::ErrorCode
@@ -63,16 +80,19 @@ module Quonfig
63
80
  environment: environment,
64
81
  extra: quonfig_options
65
82
  )
66
- @initialized = !@client.nil?
83
+ @initialized = false
67
84
  end
68
85
 
69
86
  # Initialize the underlying Quonfig client. Called by
70
- # +OpenFeature::SDK.set_provider_and_wait+.
87
+ # +OpenFeature::SDK.set_provider_and_wait+. Raises under
88
+ # +on_init_failure: :fail+ when no config loads, which OpenFeature
89
+ # records as ERROR.
71
90
  def init(_evaluation_context = nil)
72
91
  return if @initialized
73
92
 
74
- @client = ::Quonfig::Client.new(@quonfig_options)
93
+ @client ||= ::Quonfig::Client.new(@quonfig_options)
75
94
  @initialized = true
95
+ watch_for_heal(@client) unless client_ready?(@client)
76
96
  nil
77
97
  end
78
98
 
@@ -83,6 +103,8 @@ module Quonfig
83
103
  @client = nil
84
104
  @initialized = false
85
105
  client&.stop
106
+ @heal_thread&.join(1)
107
+ @heal_thread = nil
86
108
  nil
87
109
  end
88
110
 
@@ -141,6 +163,41 @@ module Quonfig
141
163
 
142
164
  private
143
165
 
166
+ def client_ready?(client)
167
+ !client.respond_to?(:ready?) || client.ready?
168
+ end
169
+
170
+ # +on_init_failure: :continue+ left the client without config. Report
171
+ # ERROR once OpenFeature has recorded the READY it dispatches after +init+
172
+ # returns, then READY when the client's background retry heals.
173
+ def watch_for_heal(client)
174
+ @heal_thread = Thread.new do
175
+ await_init_dispatch
176
+ unless client.ready?
177
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR,
178
+ error_code: ErrorCode::PROVIDER_NOT_READY,
179
+ message: 'Quonfig has no config yet (on_init_failure: :continue); ' \
180
+ 'serving code defaults while it retries')
181
+ end
182
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY) if client.wait_for_ready && @client.equal?(client)
183
+ end
184
+ end
185
+
186
+ # OpenFeature dispatches PROVIDER_READY after +init+ returns. Wait for it
187
+ # so our PROVIDER_ERROR lands after it, not before. +provider_state+ is
188
+ # private in openfeature-sdk 0.5.x and public in 0.6.x.
189
+ def await_init_dispatch
190
+ config = @configuration
191
+ return unless config.respond_to?(:provider_state, true)
192
+
193
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + INIT_DISPATCH_WAIT_S
194
+ while config.send(:provider_state, self) == ::OpenFeature::SDK::ProviderState::NOT_READY
195
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
196
+
197
+ sleep 0.01
198
+ end
199
+ end
200
+
144
201
  # Map a Quonfig::EvaluationDetails to the OpenFeature ResolutionDetails
145
202
  # the SDK consumes. The Quonfig SDK's *_details methods don't raise, so
146
203
  # this is a pure mapping layer.
@@ -187,6 +244,7 @@ module Quonfig
187
244
  case error_code
188
245
  when ::Quonfig::EvaluationDetails::ERROR_FLAG_NOT_FOUND then ErrorCode::FLAG_NOT_FOUND
189
246
  when ::Quonfig::EvaluationDetails::ERROR_TYPE_MISMATCH then ErrorCode::TYPE_MISMATCH
247
+ when ::Quonfig::EvaluationDetails::ERROR_PROVIDER_NOT_READY then ErrorCode::PROVIDER_NOT_READY
190
248
  else ErrorCode::GENERAL
191
249
  end
192
250
  end
@@ -204,7 +262,7 @@ module Quonfig
204
262
 
205
263
  mapped_ctx = Context.map_context(evaluation_context, @targeting_key_mapping)
206
264
  yield(client, mapped_ctx)
207
- rescue ::Quonfig::Errors::UninitializedError, ::Quonfig::Errors::InitializationTimeoutError => e
265
+ rescue ::Quonfig::Errors::UninitializedError, ::Quonfig::Errors::InitializationError => e
208
266
  ResolutionDetails.new(value: default_value, reason: Reason::ERROR,
209
267
  error_code: ErrorCode::PROVIDER_NOT_READY,
210
268
  error_message: e.message)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quonfig-openfeature
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.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-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: openfeature-sdk
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.7.2
33
+ version: 1.11.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 1.7.2
40
+ version: 1.11.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement