quonfig-openfeature 1.0.3 → 2.0.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 +76 -0
- data/README.md +38 -1
- data/VERSION +1 -1
- data/lib/quonfig/openfeature/errors.rb +1 -1
- data/lib/quonfig/openfeature/provider.rb +62 -4
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20da9ec1d24f20fa576a6430d8cd765b6d821a2cc052e0bfb8f59a5511b8447a
|
|
4
|
+
data.tar.gz: 333ea7e9395c479af7e7768bdb256c79860e7c495e009e9c51e8d14af51807e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3319e1dbd57f6bcfa125befb424e8c2f811dc3c01f921b94c3426a0e62eed5f3ab100a49c372236e68adaa1fde3ca5d259d55af09800c40e0a4043393915c2d
|
|
7
|
+
data.tar.gz: 5cc0aecc4fcd057d426ae31381572bb27e97160dea9e154cc84e85bf668849618d5a2def62c950f3713353aef3c6f1d0c8e386055bcc35cd58b0affd1e3e0321
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 - 2026-10-27
|
|
4
|
+
|
|
5
|
+
**BREAKING / UPGRADE (major, qfg-zvyy.6).** This release tracks sdk-ruby 2.0.0, the cross-SDK
|
|
6
|
+
typed-getter contract. Require `quonfig` (sdk-ruby) `>= 2.0.0` (floor raised from `>= 1.11.0`). No
|
|
7
|
+
change to this provider's code: it passes through the SDK's `*_details` results, so provider users
|
|
8
|
+
inherit the contract below.
|
|
9
|
+
|
|
10
|
+
### UPGRADE to 2.0
|
|
11
|
+
|
|
12
|
+
A value a `fetch_*` method cannot read as its type returns your default with reason `ERROR` and
|
|
13
|
+
error code `TYPE_MISMATCH`. Strings are never coerced, and the only number conversions are exact
|
|
14
|
+
ones. What changes for provider users:
|
|
15
|
+
|
|
16
|
+
- `fetch_float_value` on an int config now returns the value (`30` -> `30.0`). 1.x returned your
|
|
17
|
+
default with `TYPE_MISMATCH`.
|
|
18
|
+
- `fetch_integer_value` on a double config returns the value when it is whole and within
|
|
19
|
+
+/-(2^53-1) (`3.0` -> `3`). 1.x returned your default for every double. `3.9`, `2^53` and `1e300`
|
|
20
|
+
still return your default with `TYPE_MISMATCH`, never truncated.
|
|
21
|
+
- `fetch_number_value` on a `duration` config returns your default with `TYPE_MISMATCH`. 1.x
|
|
22
|
+
returned the duration in milliseconds (`30000`, reason `STATIC`). Durations are native-only: use
|
|
23
|
+
`provider.client.get_duration` (integer milliseconds). `fetch_float_value` on a duration already
|
|
24
|
+
returned your default.
|
|
25
|
+
- A malformed stored int or double (for example `"twelve"`) or a malformed ENV_VAR value returns
|
|
26
|
+
your default with error code `TYPE_MISMATCH`, as in sdk-go. 1.x reported `GENERAL`. An unset
|
|
27
|
+
ENV_VAR still returns your default with `GENERAL`.
|
|
28
|
+
- sdk-ruby logs one WARN per key per client when a read returns your default because of a type
|
|
29
|
+
mismatch, a malformed value, an unset ENV_VAR or a decryption failure. The line names the key and
|
|
30
|
+
types, never the value. A wrong decryption key now degrades to your defaults with these WARNs.
|
|
31
|
+
- If you call the native client via `provider.client`: typed getters with a `default:` return the
|
|
32
|
+
default instead of raising, `enabled?` is true only for the bool `true` and never raises, and
|
|
33
|
+
`get_bool` on a stored string `"true"` is a type mismatch. See the sdk-ruby CHANGELOG
|
|
34
|
+
"UPGRADE to 2.0".
|
|
35
|
+
|
|
36
|
+
These already held under 1.11.0 through the provider, and the new tests now pin them: a stored
|
|
37
|
+
string `"true"` / `"false"` read as a boolean, a stored `"3"` read as a number, an int or bool read as
|
|
38
|
+
a string, an int read as a boolean, and a string or bool read as an object all return your default
|
|
39
|
+
with `TYPE_MISMATCH`.
|
|
40
|
+
|
|
41
|
+
### Tests
|
|
42
|
+
|
|
43
|
+
- New `test/integration/test_typed_getter_contract.rb` (31 tests) drives the real provider through
|
|
44
|
+
the OpenFeature client against the integration-test-data `getter.contract.*` fixtures. 7 of the 31
|
|
45
|
+
fail on sdk-ruby 1.11.0; all pass on 2.0.0.
|
|
46
|
+
|
|
47
|
+
## 1.1.0 - 2026-10-23
|
|
48
|
+
|
|
49
|
+
- **UPGRADE: raise the `quonfig` floor from `>= 1.10.0` to `>= 1.11.0`. Requires sdk-ruby 1.11.0:
|
|
50
|
+
SSE event and config body size caps, `Client#callback_errors_total`.** The provider code does not
|
|
51
|
+
change for 1.11.0. The new floor means every install of this provider gets those sdk-ruby changes:
|
|
52
|
+
an oversized SSE event (over 4 MiB) or config body (over 16 MiB) is treated as a failed attempt
|
|
53
|
+
instead of being read without limit, and `Dir.home` failures no longer raise from `Client.new`.
|
|
54
|
+
Read the 1.11.0 entry in the sdk-ruby CHANGELOG. Build and test against `quonfig` 1.11.0
|
|
55
|
+
(Gemfile.lock).
|
|
56
|
+
- **UPGRADE: raise the `quonfig` floor from `>= 1.7.2` to `>= 1.10.0` (qfg-9p4i.12).** The provider
|
|
57
|
+
now uses sdk-ruby 1.10.0's init-failure surface (`on_init_failure: :fail | :wait | :continue`,
|
|
58
|
+
`init_timeout_ms`, `Client#ready?` / `#wait_for_ready`, `Quonfig::Errors::InitializationError`).
|
|
59
|
+
Read the 1.10.0 entry in the sdk-ruby CHANGELOG: `Client.new` now retries a failed boot until
|
|
60
|
+
`init_timeout_ms` (default 10s) before it raises, instead of raising on the first failed attempt.
|
|
61
|
+
- **Added (minor): the provider emits OpenFeature events and its status follows `on_init_failure`
|
|
62
|
+
(init-failure design B.5).** Pass `on_init_failure:` and `init_timeout_ms:` to `Provider.new`; they
|
|
63
|
+
are forwarded to `Quonfig::Client.new` as before.
|
|
64
|
+
- `:fail` (the default): `init` raises `Quonfig::Errors::InitializationError` when no config loads,
|
|
65
|
+
so `set_provider_and_wait` raises and the provider is ERROR. sdk-ruby returns no client under
|
|
66
|
+
`:fail`, so nothing retries in the background; the process restart is the retry.
|
|
67
|
+
- `:wait`: `init` blocks until config loads. The provider is NOT_READY for the whole wait, then READY.
|
|
68
|
+
- `:continue` with no config: `init` resolves, then the provider emits `PROVIDER_ERROR` (error code
|
|
69
|
+
`PROVIDER_NOT_READY`), so its status is ERROR rather than READY while every evaluation returns the
|
|
70
|
+
code default. When the client's background retry loads config, the provider emits
|
|
71
|
+
`PROVIDER_READY`. Before, the provider reported READY and emitted no events.
|
|
72
|
+
- A client injected with `client:` gets the same treatment: if it has no config at `init`, the
|
|
73
|
+
provider reports ERROR until it heals.
|
|
74
|
+
- **Fix: an evaluation before config loads reports `PROVIDER_NOT_READY`.** sdk-ruby 1.10.0's
|
|
75
|
+
`*_details` getters return error code `PROVIDER_NOT_READY` on a client with no config; the provider
|
|
76
|
+
mapped that to `GENERAL`. `Quonfig::Errors::InitializationError` (any reason, not only the timeout
|
|
77
|
+
subclass) now maps to `PROVIDER_NOT_READY` too.
|
|
78
|
+
|
|
3
79
|
## 1.0.3 - 2026-10-16
|
|
4
80
|
|
|
5
81
|
- Build and test against `quonfig` 1.8.0 (Gemfile.lock). The runtime floor stays at `>= 1.7.2`: this
|
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
|
|
@@ -114,13 +134,30 @@ Quonfig features require the native `quonfig` SDK:
|
|
|
114
134
|
|
|
115
135
|
1. **Log levels** -- `should_log?` and the `semantic_logger_filter` are native-only.
|
|
116
136
|
2. **`string_list` configs** -- accessed via `fetch_object_value` and used as `Array<String>`.
|
|
117
|
-
3. **`duration` configs** --
|
|
137
|
+
3. **`duration` configs** -- native-only: read them with `provider.client.get_duration` (integer
|
|
138
|
+
milliseconds). `fetch_float_value` and `fetch_number_value` on a duration config return your
|
|
139
|
+
default with `TYPE_MISMATCH`.
|
|
118
140
|
4. **`bytes` configs** -- not accessible via OpenFeature (no binary type in OF).
|
|
119
141
|
5. **`keys` / raw config inspection** -- native-only via `provider.client`.
|
|
120
142
|
6. **Context keys use dot-notation** -- `"user.email"`, not nested hashes.
|
|
121
143
|
7. **`targeting_key` maps to `user.id` by default** -- configure
|
|
122
144
|
`targeting_key_mapping` if you target a different namespace.
|
|
123
145
|
|
|
146
|
+
## Typed reads
|
|
147
|
+
|
|
148
|
+
Each `fetch_*` method reads only values of its type (the cross-SDK typed-getter contract,
|
|
149
|
+
`quonfig` >= 2.0.0). A value of another type returns your default with reason `ERROR` and error
|
|
150
|
+
code `TYPE_MISMATCH`:
|
|
151
|
+
|
|
152
|
+
- A stored string is never coerced: `"false"` is not a boolean and `"3"` is not a number.
|
|
153
|
+
- Numbers convert only when exact: `fetch_float_value` on an int config returns `30.0`, and
|
|
154
|
+
`fetch_integer_value` on a double config returns the value only when it is whole and within
|
|
155
|
+
+/-(2^53-1) (`3.0` -> `3`; `3.9` is a mismatch, never truncated).
|
|
156
|
+
- A malformed stored or ENV_VAR value (an int `"twelve"`) also returns your default with
|
|
157
|
+
`TYPE_MISMATCH`. An unset ENV_VAR returns your default with `GENERAL`.
|
|
158
|
+
|
|
159
|
+
sdk-ruby logs one WARN per key when a read returns your default for one of these reasons.
|
|
160
|
+
|
|
124
161
|
## Offline / test mode
|
|
125
162
|
|
|
126
163
|
Pass `datadir:` to evaluate against a Quonfig workspace on disk -- the same
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2.0.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::
|
|
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 =
|
|
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
|
|
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::
|
|
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:
|
|
4
|
+
version: 2.0.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-
|
|
11
|
+
date: 2026-10-27 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:
|
|
33
|
+
version: 2.0.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:
|
|
40
|
+
version: 2.0.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: minitest
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|