featurehub-sdk 1.3.0 → 2.0.1
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/.claude/CLAUDE.md +85 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +18 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -8
- data/README.md +306 -119
- data/examples/rails_example/.ruby-version +1 -1
- data/examples/rails_example/Dockerfile +1 -1
- data/examples/sinatra/.dockerignore +7 -0
- data/examples/sinatra/.ruby-version +1 -1
- data/examples/sinatra/Dockerfile +14 -25
- data/examples/sinatra/Gemfile +5 -4
- data/examples/sinatra/Gemfile.lock +40 -32
- data/examples/sinatra/app/application.rb +21 -9
- data/examples/sinatra/docker-compose.yaml +24 -0
- data/examples/sinatra/feature-flags.yaml +6 -0
- data/examples/sinatra/sinatra.iml +35 -14
- data/examples/sinatra/start.sh +2 -0
- data/featurehub-sdk.gemspec +4 -1
- data/lib/feature_hub/sdk/context.rb +28 -7
- data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
- data/lib/feature_hub/sdk/feature_repository.rb +52 -13
- data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
- data/lib/feature_hub/sdk/interceptors.rb +10 -6
- data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
- data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
- data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
- data/lib/feature_hub/sdk/poll_edge_service.rb +10 -15
- data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
- data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
- data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
- data/lib/feature_hub/sdk/streaming_edge_service.rb +5 -7
- data/lib/feature_hub/sdk/version.rb +1 -10
- data/lib/featurehub-sdk.rb +5 -1
- data/sig/feature_hub/featurehub.rbs +127 -28
- metadata +27 -5
data/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# Official FeatureHub Ruby SDK
|
|
1
|
+
# Official FeatureHub Ruby SDK
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
To control the feature flags from the FeatureHub Admin console, either use our [demo](https://demo.featurehub.io) version for evaluation or install the app using our guide [here](https://docs.featurehub.io/featurehub/latest/installation.html).
|
|
5
6
|
|
|
6
7
|
## SDK installation
|
|
7
8
|
|
|
8
|
-
Add the featurehub
|
|
9
|
+
Add the featurehub-sdk gem to your Gemfile:
|
|
9
10
|
|
|
10
|
-
```
|
|
11
|
-
gem
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'featurehub-sdk'
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
To use it in your code, use:
|
|
15
|
+
To use it in your code:
|
|
16
16
|
|
|
17
17
|
```ruby
|
|
18
18
|
require 'featurehub-sdk'
|
|
@@ -20,216 +20,403 @@ require 'featurehub-sdk'
|
|
|
20
20
|
|
|
21
21
|
## Options to get feature updates
|
|
22
22
|
|
|
23
|
-
There are 2 ways to request
|
|
24
|
-
|
|
25
|
-
- **SSE (Server Sent Events) realtime updates mechanism**
|
|
23
|
+
There are 2 ways to request feature updates via this SDK:
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
- **SSE (Server Sent Events) realtime updates**
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
Makes a persistent connection to the FeatureHub Edge server. Any updates to features come through in near-realtime, automatically updating the repository. Recommended for long-running server applications.
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
processes like command line tools. Batch tools that iterate over data sets and wish to control when updates happen can also benefit from this method.
|
|
29
|
+
- **Polling client (GET request)**
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
Requests updates at a configurable interval (0 = once only). Useful for short-lived processes such as CLI tools or batch jobs.
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Check our example Sinatra app [here](https://github.com/featurehub-io/featurehub-ruby-sdk/tree/main/example/sinatra)
|
|
33
|
+
Both options use `concurrent-ruby` to keep the connection open and update state in the background.
|
|
39
34
|
|
|
40
35
|
## Quick start
|
|
41
36
|
|
|
42
|
-
###
|
|
43
|
-
There are 3 steps to connecting:
|
|
44
|
-
1) Copy FeatureHub API Key from the FeatureHub Admin Console
|
|
45
|
-
2) Create FeatureHub config
|
|
46
|
-
3) Check FeatureHub Repository readiness and request feature state
|
|
37
|
+
### 1. Copy your API Key
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
Find and copy your API Key from the FeatureHub Admin Console on the API Keys page -
|
|
50
|
-
you will use this in your code to configure feature updates for your environments.
|
|
51
|
-
It should look similar to this: ```default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE```.
|
|
52
|
-
There are two options - a Server Evaluated API Key and a Client Evaluated API Key. More on this [here](https://docs.featurehub.io/#_client_and_server_api_keys)
|
|
39
|
+
Find and copy your API Key from the FeatureHub Admin Console on the API Keys page. It will look similar to:
|
|
53
40
|
|
|
54
|
-
|
|
41
|
+
```
|
|
42
|
+
default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE
|
|
43
|
+
```
|
|
55
44
|
|
|
56
|
-
|
|
45
|
+
There are two key types — Server Evaluated and Client Evaluated. More detail [here](https://docs.featurehub.io/#_client_and_server_api_keys).
|
|
57
46
|
|
|
58
|
-
|
|
47
|
+
- **Client Evaluated** keys (contain `*`) send full rollout strategy data to the SDK and evaluate strategies locally, per request. Intended for secure server-side environments such as microservices.
|
|
48
|
+
- **Server Evaluated** keys evaluate on the server side. Suitable for insecure clients or environments where you evaluate one user per connection.
|
|
59
49
|
|
|
60
|
-
|
|
50
|
+
### 2. Create FeatureHub config
|
|
61
51
|
|
|
62
52
|
```ruby
|
|
63
|
-
config = FeatureHub::Sdk::FeatureHubConfig.new(
|
|
64
|
-
|
|
53
|
+
config = FeatureHub::Sdk::FeatureHubConfig.new(
|
|
54
|
+
ENV.fetch("FEATUREHUB_EDGE_URL"),
|
|
55
|
+
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
|
|
56
|
+
)
|
|
65
57
|
config.init
|
|
66
|
-
|
|
67
58
|
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(which holds state) and an Edge Server (which gets the updates and passes them
|
|
71
|
-
on to the Repository). You can have many of them if you wish, but you don't need
|
|
72
|
-
to.
|
|
73
|
-
|
|
74
|
-
to in Rails, you might create an initializer that does this:
|
|
59
|
+
|
|
60
|
+
You only ever need to do this once. A `FeatureHubConfig` holds a `FeatureHubRepository` (state) and an edge service (updates). In Rails, create an initializer:
|
|
75
61
|
|
|
76
62
|
```ruby
|
|
77
|
-
Rails.configuration.fh_client = FeatureHub::Sdk::FeatureHubConfig.new(
|
|
78
|
-
|
|
63
|
+
Rails.configuration.fh_client = FeatureHub::Sdk::FeatureHubConfig.new(
|
|
64
|
+
ENV.fetch("FEATUREHUB_EDGE_URL"),
|
|
65
|
+
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
|
|
66
|
+
).init
|
|
79
67
|
```
|
|
80
68
|
|
|
81
|
-
|
|
69
|
+
In Sinatra:
|
|
82
70
|
|
|
83
71
|
```ruby
|
|
84
72
|
class App < Sinatra::Base
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
configure do
|
|
74
|
+
set :fh_config, FeatureHub::Sdk::FeatureHubConfig.new(
|
|
75
|
+
ENV.fetch("FEATUREHUB_EDGE_URL"),
|
|
76
|
+
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
89
79
|
end
|
|
90
80
|
```
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
By default, this SDK will use SSE client. If you decide to use FeatureHub polling client, after initialising the config, you can add this:
|
|
82
|
+
To use the polling client instead of SSE:
|
|
94
83
|
|
|
95
84
|
```ruby
|
|
96
85
|
config.use_polling_edge_service(30)
|
|
97
|
-
# OR
|
|
98
|
-
config.use_polling_edge_service
|
|
86
|
+
# OR — reads FEATUREHUB_POLL_INTERVAL env var, defaults to 30 seconds
|
|
87
|
+
config.use_polling_edge_service
|
|
99
88
|
```
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#### 3. Check FeatureHub Repository readiness and request feature state
|
|
90
|
+
### 3. Check readiness and request feature state
|
|
104
91
|
|
|
105
|
-
Check for FeatureHub Repository readiness:
|
|
106
92
|
```ruby
|
|
107
93
|
if config.repository.ready?
|
|
108
|
-
#
|
|
94
|
+
# safe to evaluate features
|
|
109
95
|
end
|
|
110
96
|
```
|
|
111
97
|
|
|
112
|
-
|
|
98
|
+
See [Readiness](#readiness) below for details on incorporating this into health checks.
|
|
99
|
+
|
|
100
|
+
## Evaluating features
|
|
101
|
+
|
|
102
|
+
### Without a context (no rollout strategies)
|
|
113
103
|
|
|
114
104
|
```ruby
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"hello world"
|
|
120
|
-
end
|
|
105
|
+
if config.new_context.build.feature("FEATURE_TITLE_TO_UPPERCASE").flag
|
|
106
|
+
"HELLO WORLD"
|
|
107
|
+
else
|
|
108
|
+
"hello world"
|
|
121
109
|
end
|
|
122
110
|
```
|
|
123
111
|
|
|
112
|
+
### With a context (rollout strategies)
|
|
124
113
|
|
|
125
|
-
|
|
114
|
+
Build a context with the attributes you want to use for strategy evaluation, then call `build` to push them to the server (server-evaluated keys) or trigger a poll (client-evaluated keys):
|
|
126
115
|
|
|
127
116
|
```ruby
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
ctx = config.new_context
|
|
118
|
+
.user_key(current_user.id)
|
|
119
|
+
.country("australia")
|
|
120
|
+
.platform("ios")
|
|
121
|
+
.version("2.3.1")
|
|
122
|
+
.attribute_value("plan", "premium")
|
|
123
|
+
.build
|
|
124
|
+
|
|
125
|
+
if ctx.feature("FEATURE_TITLE_TO_UPPERCASE").flag
|
|
126
|
+
# ...
|
|
134
127
|
end
|
|
135
128
|
```
|
|
136
129
|
|
|
137
|
-
Well
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
|
|
130
|
+
#### Well-known context attributes
|
|
131
|
+
|
|
132
|
+
| Method | ContextKey |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `user_key(value)` | `:userkey` |
|
|
135
|
+
| `session_key(value)` | `:session` |
|
|
136
|
+
| `country(value)` | `:country` |
|
|
137
|
+
| `platform(value)` | `:platform` |
|
|
138
|
+
| `device(value)` | `:device` |
|
|
139
|
+
| `version(value)` | `:version` |
|
|
140
|
+
|
|
141
|
+
#### Custom attributes
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
ctx.attribute_value("contract_ids", [2, 17, 45])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### `assign` — bulk-set attributes from a hash
|
|
148
|
+
|
|
149
|
+
`assign` accepts a hash, maps well-known keys to their dedicated setters, and merges anything else as a custom attribute:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
ctx.assign(
|
|
153
|
+
userkey: current_user.id,
|
|
154
|
+
country: "nz",
|
|
155
|
+
plan: "enterprise"
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
String keys are also accepted (`"userkey"` and `:userkey` are equivalent).
|
|
160
|
+
|
|
161
|
+
#### Construct a context with initial attributes
|
|
162
|
+
|
|
163
|
+
Pass a hash directly to `new_context` via the repository, or pre-populate at construction time:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
ctx = FeatureHub::Sdk::ClientContext.new(repository, { userkey: "u1", country: "nz" })
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### One-off feature evaluation with inline attributes
|
|
170
|
+
|
|
171
|
+
If you only need to check one feature and do not want to build a context, you can pass attributes directly to `feature`:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# On the config (delegates to the repository)
|
|
175
|
+
config.feature("SUBMIT_COLOR_BUTTON", { country: "nz" }).string
|
|
176
|
+
|
|
177
|
+
# Or directly on the repository
|
|
178
|
+
config.repository.feature("SUBMIT_COLOR_BUTTON", { country: "nz", userkey: "u1" }).string
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This creates a temporary `ClientContext` internally and evaluates the feature through it.
|
|
182
|
+
|
|
183
|
+
#### `value` — get a raw value with a default
|
|
184
|
+
|
|
185
|
+
`value(key, default_value = nil, attrs = nil)` returns the feature's value directly, or `default_value` if the feature does not exist:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# Simple lookup with a fallback
|
|
189
|
+
color = config.value("SUBMIT_COLOR_BUTTON", "blue")
|
|
190
|
+
|
|
191
|
+
# With inline attributes for strategy evaluation
|
|
192
|
+
color = config.value("SUBMIT_COLOR_BUTTON", "blue", { country: "nz" })
|
|
193
|
+
|
|
194
|
+
# Also available on the repository directly
|
|
195
|
+
color = config.repository.value("SUBMIT_COLOR_BUTTON", "blue")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Feature value accessors
|
|
199
|
+
|
|
200
|
+
| Method | Returns |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `.flag` / `.boolean` | `bool?` |
|
|
203
|
+
| `.string` | `String?` |
|
|
204
|
+
| `.number` | `Float?` |
|
|
205
|
+
| `.raw_json` | `String?` (raw JSON string) |
|
|
206
|
+
| `.json` | `Hash?` (parsed JSON) |
|
|
207
|
+
| `.enabled?` | `bool` (true if flag is on) |
|
|
208
|
+
| `.set?` | `bool` (true if a value has been set) |
|
|
209
|
+
| `.exists?` | `bool` (true if the feature exists in the repository) |
|
|
210
|
+
| `.present?` | `bool` (alias for `exists?`) |
|
|
211
|
+
|
|
212
|
+
## Feature interceptors
|
|
213
|
+
|
|
214
|
+
Interceptors let you override feature values at runtime without changing the repository. They are evaluated before rollout strategies.
|
|
215
|
+
|
|
216
|
+
### Environment variable interceptor
|
|
217
|
+
|
|
218
|
+
Override any feature at runtime using environment variables:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
FEATUREHUB_OVERRIDE_FEATURES=true
|
|
222
|
+
FEATUREHUB_MY_FEATURE=true
|
|
223
|
+
FEATUREHUB_SUBMIT_COLOR_BUTTON=green
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
config.repository.register_interceptor(FeatureHub::Sdk::EnvironmentInterceptor.new)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Local YAML interceptor
|
|
231
|
+
|
|
232
|
+
Override features from a YAML file. Useful during development or testing:
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
# featurehub-overrides.yaml
|
|
236
|
+
flagValues:
|
|
237
|
+
MY_FEATURE: true
|
|
238
|
+
SUBMIT_COLOR_BUTTON: green
|
|
239
|
+
MAX_RETRIES: 3
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
All options are passed as a single hash:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
# Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
|
|
246
|
+
config.repository.register_interceptor(FeatureHub::Sdk::LocalYamlValueInterceptor.new)
|
|
247
|
+
|
|
248
|
+
# Explicit file path
|
|
249
|
+
config.repository.register_interceptor(
|
|
250
|
+
FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "path/to/overrides.yaml")
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Watch for file changes and reload automatically
|
|
254
|
+
config.repository.register_interceptor(
|
|
255
|
+
FeatureHub::Sdk::LocalYamlValueInterceptor.new(watch: true, watch_interval: 5)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# With a custom logger
|
|
259
|
+
config.repository.register_interceptor(
|
|
260
|
+
FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "overrides.yaml", logger: my_logger)
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Supported options: `:filename`, `:watch` (default: `false`), `:watch_interval` (seconds, default: `5`), `:logger`.
|
|
265
|
+
|
|
266
|
+
## Offline / local-only mode with LocalYamlStore
|
|
143
267
|
|
|
268
|
+
`LocalYamlStore` loads features from a YAML file directly into the repository, with no Edge connection required. It uses the same file format as `LocalYamlValueInterceptor`. This is useful for tests, CI environments, or services that manage their own feature state.
|
|
144
269
|
|
|
145
|
-
|
|
270
|
+
```yaml
|
|
271
|
+
# features.yaml
|
|
272
|
+
flagValues:
|
|
273
|
+
MY_FLAG: true
|
|
274
|
+
SUBMIT_COLOR_BUTTON: green
|
|
275
|
+
MAX_RETRIES: 3
|
|
276
|
+
PRICING_CONFIG:
|
|
277
|
+
base: 9.99
|
|
278
|
+
tiers: [19.99, 49.99]
|
|
279
|
+
```
|
|
146
280
|
|
|
147
|
-
|
|
281
|
+
```ruby
|
|
282
|
+
repository = FeatureHub::Sdk::FeatureHubRepository.new
|
|
148
283
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
284
|
+
# Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
|
|
285
|
+
store = FeatureHub::Sdk::LocalYamlStore.new(repository)
|
|
286
|
+
|
|
287
|
+
# Explicit file path
|
|
288
|
+
store = FeatureHub::Sdk::LocalYamlStore.new(repository, filename: "features.yaml")
|
|
289
|
+
|
|
290
|
+
repository.feature("MY_FLAG").flag # => true
|
|
291
|
+
repository.value("SUBMIT_COLOR_BUTTON") # => "green"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The file path defaults to `featurehub-overrides.yaml` or the `FEATUREHUB_LOCAL_YAML` environment variable. Complex values (hashes, arrays) are serialised to a JSON string and stored as a `JSON` feature type.
|
|
295
|
+
|
|
296
|
+
## Caching feature state in Redis
|
|
297
|
+
|
|
298
|
+
`RedisSessionStore` persists feature values from a `FeatureHubRepository` to Redis. On startup it replays cached features into the repository, then listens for live updates and writes newer versions back. A background timer re-reads all features periodically so updates from other processes are picked up automatically.
|
|
299
|
+
|
|
300
|
+
> **Warning:** Do not use `RedisSessionStore` with server-evaluated features. Each server-evaluated context resolves to different values; sharing a single Redis key across processes will cause them to overwrite each other's state.
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
# Requires the 'redis' gem: gem 'redis', '~> 5'
|
|
304
|
+
store = FeatureHub::Sdk::RedisSessionStore.new(
|
|
305
|
+
"redis://localhost:6379",
|
|
306
|
+
config.repository,
|
|
307
|
+
{
|
|
308
|
+
prefix: "myapp", # Redis key prefix (default: "featurehub")
|
|
309
|
+
namespace: 0, # Redis DB index (default: 0)
|
|
310
|
+
timeout: 60, # Seconds between periodic reloads (default: 30)
|
|
311
|
+
password: "secret", # Optional Redis password
|
|
312
|
+
logger: my_logger # Optional logger (default: SDK default logger)
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Register it so it also receives live updates
|
|
317
|
+
config.register_raw_update_listener(store)
|
|
318
|
+
|
|
319
|
+
# Shut down cleanly
|
|
320
|
+
store.close
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Redis keys used:
|
|
324
|
+
- `{prefix}_ids` — a Redis SET of feature IDs
|
|
325
|
+
- `{prefix}_{id}` — the JSON-encoded feature state for each feature
|
|
326
|
+
|
|
327
|
+
## Custom raw update listeners
|
|
328
|
+
|
|
329
|
+
`RawUpdateFeatureListener` is a base class you can subclass to observe every raw feature update that flows through the repository, regardless of source. Register an instance with the repository (or config) and override only the callbacks you need:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class MyAuditListener < FeatureHub::Sdk::RawUpdateFeatureListener
|
|
333
|
+
def process_updates(features, source)
|
|
334
|
+
features.each { |f| Rails.logger.info("bulk update from #{source}: #{f["key"]}") }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def process_update(feature, source)
|
|
338
|
+
Rails.logger.info("single update from #{source}: #{feature["key"]}")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def delete_feature(feature, source)
|
|
342
|
+
Rails.logger.warn("deleted from #{source}: #{feature["key"]}")
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
config.register_raw_update_listener(MyAuditListener.new)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Callbacks are dispatched asynchronously via `Concurrent::Future`. The `source` parameter will be `"streaming"`, `"polling"`, `"local-yaml"`, `"redis-store"`, or `"unknown"`.
|
|
350
|
+
|
|
351
|
+
All listeners are closed automatically when `config.close` or `repository.close` is called.
|
|
352
|
+
|
|
353
|
+
## Using inside popular web servers
|
|
354
|
+
|
|
355
|
+
Most popular web servers fork processes to handle traffic. Forking kills the Edge connection but preserves the cached repository. Call `force_new_edge_service` in your framework's post-fork hook to restart the connection:
|
|
153
356
|
|
|
154
357
|
```ruby
|
|
155
358
|
config.force_new_edge_service
|
|
156
359
|
```
|
|
157
360
|
|
|
158
|
-
####
|
|
361
|
+
#### Passenger
|
|
159
362
|
|
|
160
|
-
In
|
|
363
|
+
In `config.ru`:
|
|
161
364
|
|
|
162
365
|
```ruby
|
|
163
366
|
if defined?(PhusionPassenger)
|
|
164
367
|
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
165
|
-
if forked
|
|
166
|
-
# e.g.
|
|
167
|
-
# App.settings.fh_config.force_new_edge_service
|
|
168
|
-
end
|
|
368
|
+
App.settings.fh_config.force_new_edge_service if forked
|
|
169
369
|
end
|
|
170
370
|
end
|
|
171
|
-
|
|
172
371
|
```
|
|
173
372
|
|
|
174
|
-
####
|
|
373
|
+
#### Puma
|
|
175
374
|
|
|
176
375
|
```ruby
|
|
177
376
|
on_worker_boot do
|
|
178
|
-
|
|
179
|
-
# App.settings.fh_config.force_new_edge_service
|
|
377
|
+
App.settings.fh_config.force_new_edge_service
|
|
180
378
|
end
|
|
181
|
-
|
|
182
379
|
```
|
|
183
380
|
|
|
184
|
-
####
|
|
381
|
+
#### Unicorn
|
|
185
382
|
|
|
186
383
|
```ruby
|
|
187
384
|
after_fork do |_server, _worker|
|
|
188
|
-
|
|
189
|
-
# App.settings.fh_config.force_new_edge_service
|
|
385
|
+
App.settings.fh_config.force_new_edge_service
|
|
190
386
|
end
|
|
191
|
-
|
|
192
387
|
```
|
|
193
388
|
|
|
194
|
-
####
|
|
389
|
+
#### Spring
|
|
195
390
|
|
|
196
391
|
```ruby
|
|
197
|
-
Spring.after_fork do
|
|
198
|
-
|
|
199
|
-
# App.settings.fh_config.force_new_edge_service
|
|
392
|
+
Spring.after_fork do
|
|
393
|
+
App.settings.fh_config.force_new_edge_service
|
|
200
394
|
end
|
|
201
|
-
|
|
202
395
|
```
|
|
203
|
-
|
|
204
396
|
|
|
205
|
-
|
|
397
|
+
## Extracting and restoring state
|
|
206
398
|
|
|
207
|
-
You can
|
|
208
|
-
it, but it should be done so using the JSON mechanism so it parses correctly.
|
|
399
|
+
You can snapshot the repository state and reload it later (e.g. as a warm-start cache):
|
|
209
400
|
|
|
210
401
|
```ruby
|
|
211
402
|
require 'json'
|
|
212
403
|
|
|
404
|
+
# Snapshot
|
|
213
405
|
state = config.repository.extract_feature_state
|
|
214
|
-
|
|
215
|
-
# somehow save it
|
|
216
406
|
save(state.to_json)
|
|
217
407
|
|
|
218
|
-
#
|
|
408
|
+
# Restore
|
|
219
409
|
config.repository.notify(:features, JSON.parse(read_state))
|
|
220
410
|
```
|
|
221
411
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
It is encourage that you include the ready state of the repository in your
|
|
225
|
-
readyness check. If your server cannot connect to your FeatureHub repository
|
|
226
|
-
and cannot sensibly operate without it, it is not ready. Once it has received
|
|
227
|
-
initial state it will remain ready even when it temporarily loses connections.
|
|
412
|
+
## Readiness
|
|
228
413
|
|
|
229
|
-
It is only if the key is invalid
|
|
230
|
-
that the repository is marked not ready. To determine readyness:
|
|
414
|
+
It is recommended to include the repository's ready state in your health/readiness check. The repository becomes ready once it has received its first successful update, and stays ready even through temporary connection loss. It is only not ready if the API key is invalid or no state has ever been received:
|
|
231
415
|
|
|
232
416
|
```ruby
|
|
233
417
|
config.repository.ready?
|
|
234
418
|
```
|
|
235
419
|
|
|
420
|
+
## Examples
|
|
421
|
+
|
|
422
|
+
Check our example Sinatra app [here](https://github.com/featurehub-io/featurehub-ruby-sdk/tree/main/example/sinatra).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.10
|
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.10
|
data/examples/sinatra/Dockerfile
CHANGED
|
@@ -1,42 +1,31 @@
|
|
|
1
|
-
FROM ruby:
|
|
1
|
+
FROM ruby:3.3.10-bookworm
|
|
2
2
|
|
|
3
3
|
MAINTAINER info@featurehub.io
|
|
4
4
|
ENV BUNDLER_VERSION 2.3.15
|
|
5
5
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
6
6
|
|
|
7
7
|
RUN apt update && \
|
|
8
|
-
apt install -y gnupg
|
|
8
|
+
apt install -y gnupg wget tzdata apt-transport-https dirmngr gnupg curl && \
|
|
9
|
+
curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key-2025.txt | gpg --dearmor | tee /etc/apt/trusted.gpg.d/phusion.gpg >/dev/null && \
|
|
10
|
+
sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bookworm main > /etc/apt/sources.list.d/passenger.list' && \
|
|
11
|
+
apt-get update && \
|
|
12
|
+
apt-get install -y nginx passenger
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
apt-get install -y wget tzdata apt-transport-https && \
|
|
13
|
-
apt-get remove -y mysql-common
|
|
14
|
-
|
|
15
|
-
# set up nsswitch
|
|
16
|
-
COPY conf/nsswitch.conf /etc/nsswitch.conf
|
|
14
|
+
#ENV BUNDLE_PATH /bundle
|
|
15
|
+
RUN passenger-config build-native-support
|
|
17
16
|
|
|
18
17
|
RUN echo 'gem: --no-document' >> ~/.gemrc && \
|
|
19
|
-
gem update --system
|
|
18
|
+
gem update --system && \
|
|
20
19
|
gem install bundler -v ${BUNDLER_VERSION} --force
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
RUN apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db 561F9B9CAC40B2F7 && \
|
|
25
|
-
apt-get update && \
|
|
26
|
-
apt-get install -y libnginx-mod-http-passenger=1:6.0.13-1~bullseye1 \
|
|
27
|
-
passenger=1:6.0.13-1~bullseye1 nginx && \
|
|
28
|
-
apt-get clean -y && \
|
|
29
|
-
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*
|
|
30
|
-
# set up passenger
|
|
21
|
+
# set up nsswitch
|
|
22
|
+
COPY examples/sinatra/conf/nsswitch.conf /etc/nsswitch.conf
|
|
31
23
|
|
|
32
|
-
#ENV BUNDLE_PATH /bundle
|
|
33
|
-
RUN passenger-config build-native-support
|
|
34
|
-
RUN gem update --system
|
|
35
24
|
RUN mkdir -p /app/featurehub
|
|
36
|
-
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
|
37
|
-
COPY
|
|
25
|
+
COPY examples/sinatra/conf/nginx.conf /etc/nginx/nginx.conf
|
|
26
|
+
COPY . /app/
|
|
38
27
|
WORKDIR /app/featurehub
|
|
39
28
|
RUN cd /app/featurehub && bundle install
|
|
40
|
-
ADD
|
|
29
|
+
ADD examples/sinatra/ /app/featurehub
|
|
41
30
|
|
|
42
31
|
CMD /usr/sbin/nginx -g \'daemon off;\'
|
data/examples/sinatra/Gemfile
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
source "https://rubygems.org"
|
|
4
4
|
|
|
5
|
-
ruby "
|
|
5
|
+
ruby "3.3.10"
|
|
6
6
|
|
|
7
|
+
gem "featurehub-sdk", path: "../.."
|
|
7
8
|
gem "rack"
|
|
9
|
+
gem "redis"
|
|
8
10
|
gem "sinatra"
|
|
9
|
-
# gem "featurehub-sdk",
|
|
10
|
-
|
|
11
|
-
glob: "featurehub-sdk/*.gemspec"
|
|
11
|
+
# gem "featurehub-sdk", git: "https://github.com/featurehub-io/featurehub-ruby-sdk.git",
|
|
12
|
+
# glob: "featurehub-sdk/*.gemspec"
|
|
12
13
|
|
|
13
14
|
group :development do
|
|
14
15
|
gem "shotgun"
|