featurehub-sdk 2.0.1 → 2.1.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 +1 -0
- data/.dockerignore +12 -0
- data/.rubocop.yml +0 -2
- data/CHANGELOG.md +14 -0
- data/Gemfile +3 -3
- data/Gemfile.lock +38 -55
- data/Makefile +12 -0
- data/README.md +66 -13
- data/examples/sinatra/Dockerfile +16 -16
- data/examples/sinatra/Gemfile +4 -0
- data/examples/sinatra/Gemfile.lock +33 -14
- data/examples/sinatra/README.adoc +18 -2
- data/examples/sinatra/app/application.rb +31 -19
- data/examples/sinatra/conf/nginx.conf +3 -11
- data/examples/sinatra/conf/webapp.conf +16 -0
- data/examples/sinatra/docker-compose.yaml +32 -4
- data/examples/sinatra/start.sh +19 -4
- data/featurehub-sdk.gemspec +3 -2
- data/lib/feature_hub/sdk/context.rb +3 -1
- data/lib/feature_hub/sdk/feature_hub_config.rb +32 -8
- data/lib/feature_hub/sdk/feature_state_holder.rb +11 -0
- data/lib/feature_hub/sdk/memcache_session_store.rb +221 -0
- data/lib/feature_hub/sdk/poll_edge_service.rb +21 -15
- data/lib/feature_hub/sdk/redis_session_store.rb +145 -49
- data/lib/feature_hub/sdk/session_store_helpers.rb +45 -0
- data/lib/feature_hub/sdk/version.rb +1 -1
- data/lib/featurehub-sdk.rb +2 -0
- data/sig/feature_hub/featurehub.rbs +4 -0
- metadata +32 -10
- data/examples/sinatra/sinatra.iml +0 -43
- data/featurehub-ruby-sdk.iml +0 -9
- data/featurehub-sdk.iml +0 -87
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 334643cd1a7037d4a57dd889800927fbb3764840c7debbc789bf27f956af773d
|
|
4
|
+
data.tar.gz: c59e9eff232a2489079ed20c432a6f28af46e278c426049110aa44138ec74833
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1fbf9e70a80ba754703f8744175a4e0794a7fd975ced9e28b19dd0520d21e93cf4f5f772a3dac6e00dcd88af2d6ee2880ab5b557cb5817b57d358d1da032b02
|
|
7
|
+
data.tar.gz: d1ea8f20f72894561fe16e975b73eb3445f8d87ac7817e30ccd732123a56b4a8cb9b057f682e9e80596d234db2c04ab0cf175c2e901f991ba3b6326bbf7afd4b
|
data/.claude/CLAUDE.md
CHANGED
|
@@ -83,3 +83,4 @@ Type definitions live in [sig/feature_hub/featurehub.rbs](sig/feature_hub/featur
|
|
|
83
83
|
- RuboCop: max line length 120, metrics cops disabled, documentation disabled
|
|
84
84
|
- Tests mirror lib structure: `spec/feature_hub/sdk/**/*_spec.rb`
|
|
85
85
|
- Use `instance_double` for mocking, `aggregate_failures` for multiple assertions
|
|
86
|
+
- after making changes run `bundle exec rubocop -a` to autocorrect minor offences and understand what major offences may have been introduced and fix them
|
data/.dockerignore
ADDED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.1.1] - 2026-05-01
|
|
2
|
+
|
|
3
|
+
- expose Feature Properties capability
|
|
4
|
+
- wrap poll in case of transient timeout or similar error to allow it to continue
|
|
5
|
+
|
|
6
|
+
## [2.1.0] - 2026-04-14
|
|
7
|
+
|
|
8
|
+
- Once the Config is closed it won't reopen
|
|
9
|
+
- Added Memcache cache that operates on the same general principles as Redis.
|
|
10
|
+
It requires Dalli to be available in your dependencies at least 4.x.
|
|
11
|
+
- The requirement for faraday 2+ has been relaxed, just faraday is now required in
|
|
12
|
+
the gemspec. It has been tested with 2 and 1.
|
|
13
|
+
- Redis session store has been updated so it only uses two keys
|
|
14
|
+
|
|
1
15
|
## [2.0.1] - 2026-03-27
|
|
2
16
|
|
|
3
17
|
- Remove `FeatureHub::Sdk.default_logger`; logger now defaults to `nil` instead of a stdout DEBUG logger
|
data/Gemfile
CHANGED
|
@@ -9,16 +9,16 @@ gem "rake", "~> 13.0"
|
|
|
9
9
|
|
|
10
10
|
gem "rspec", "~> 3.0"
|
|
11
11
|
|
|
12
|
-
gem "rubocop", "~> 1.
|
|
12
|
+
gem "rubocop", "~> 1.86"
|
|
13
13
|
|
|
14
14
|
gem "simplecov", "~> 0.21"
|
|
15
15
|
|
|
16
16
|
gem "concurrent-ruby", "~> 1.3"
|
|
17
17
|
|
|
18
|
-
gem "faraday"
|
|
18
|
+
gem "faraday"
|
|
19
19
|
|
|
20
20
|
gem "murmurhash3", "~> 0.1.7"
|
|
21
21
|
|
|
22
22
|
gem "sem_version", "~> 2.0.0"
|
|
23
23
|
|
|
24
|
-
gem "ld-eventsource", "~> 2.
|
|
24
|
+
gem "ld-eventsource", "~> 2.6.0"
|
data/Gemfile.lock
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
featurehub-sdk (2.
|
|
4
|
+
featurehub-sdk (2.1.1)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
|
-
faraday
|
|
7
|
-
ld-eventsource (
|
|
6
|
+
faraday
|
|
7
|
+
ld-eventsource (>= 2.5.1, < 2.7.0)
|
|
8
8
|
murmurhash3 (~> 0.1.7)
|
|
9
9
|
sem_version (~> 2.0.0)
|
|
10
10
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
addressable (2.8.8)
|
|
15
|
-
public_suffix (>= 2.0.2, < 8.0)
|
|
16
14
|
ast (2.4.3)
|
|
17
15
|
concurrent-ruby (1.3.6)
|
|
18
16
|
connection_pool (3.0.2)
|
|
17
|
+
dalli (4.3.3)
|
|
18
|
+
logger
|
|
19
19
|
diff-lcs (1.6.2)
|
|
20
20
|
docile (1.4.1)
|
|
21
21
|
domain_name (0.6.20240107)
|
|
@@ -25,39 +25,27 @@ GEM
|
|
|
25
25
|
logger
|
|
26
26
|
faraday-net_http (3.4.2)
|
|
27
27
|
net-http (~> 0.5)
|
|
28
|
-
|
|
29
|
-
ffi (1.17.3-x86_64-darwin)
|
|
30
|
-
ffi (1.17.3-x86_64-linux-gnu)
|
|
31
|
-
ffi-compiler (1.3.2)
|
|
32
|
-
ffi (>= 1.15.5)
|
|
33
|
-
rake
|
|
34
|
-
http (5.3.1)
|
|
35
|
-
addressable (~> 2.8)
|
|
28
|
+
http (6.0.2)
|
|
36
29
|
http-cookie (~> 1.0)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
http-cookie (1.1.0)
|
|
30
|
+
llhttp (~> 0.6.1)
|
|
31
|
+
http-cookie (1.1.4)
|
|
40
32
|
domain_name (~> 0.5)
|
|
41
|
-
|
|
42
|
-
json (2.19.2)
|
|
33
|
+
json (2.19.3)
|
|
43
34
|
language_server-protocol (3.17.0.5)
|
|
44
|
-
ld-eventsource (2.
|
|
35
|
+
ld-eventsource (2.6.0)
|
|
45
36
|
concurrent-ruby (~> 1.0)
|
|
46
|
-
http (>= 4.4.1, <
|
|
37
|
+
http (>= 4.4.1, < 7.0.0)
|
|
47
38
|
lint_roller (1.1.0)
|
|
48
|
-
llhttp
|
|
49
|
-
ffi-compiler (~> 1.0)
|
|
50
|
-
rake (~> 13.0)
|
|
39
|
+
llhttp (0.6.1)
|
|
51
40
|
logger (1.7.0)
|
|
52
41
|
murmurhash3 (0.1.7)
|
|
53
42
|
net-http (0.9.1)
|
|
54
43
|
uri (>= 0.11.1)
|
|
55
|
-
parallel (1.
|
|
56
|
-
parser (3.3.
|
|
44
|
+
parallel (1.28.0)
|
|
45
|
+
parser (3.3.11.1)
|
|
57
46
|
ast (~> 2.4.1)
|
|
58
47
|
racc
|
|
59
|
-
prism (1.
|
|
60
|
-
public_suffix (6.0.2)
|
|
48
|
+
prism (1.9.0)
|
|
61
49
|
racc (1.8.1)
|
|
62
50
|
rainbow (3.1.1)
|
|
63
51
|
rake (13.3.1)
|
|
@@ -65,7 +53,7 @@ GEM
|
|
|
65
53
|
redis-client (>= 0.22.0)
|
|
66
54
|
redis-client (0.28.0)
|
|
67
55
|
connection_pool
|
|
68
|
-
regexp_parser (2.
|
|
56
|
+
regexp_parser (2.12.0)
|
|
69
57
|
rspec (3.13.2)
|
|
70
58
|
rspec-core (~> 3.13.0)
|
|
71
59
|
rspec-expectations (~> 3.13.0)
|
|
@@ -79,20 +67,20 @@ GEM
|
|
|
79
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
80
68
|
rspec-support (~> 3.13.0)
|
|
81
69
|
rspec-support (3.13.6)
|
|
82
|
-
rubocop (1.
|
|
70
|
+
rubocop (1.86.1)
|
|
83
71
|
json (~> 2.3)
|
|
84
72
|
language_server-protocol (~> 3.17.0.2)
|
|
85
73
|
lint_roller (~> 1.1.0)
|
|
86
|
-
parallel (
|
|
74
|
+
parallel (>= 1.10)
|
|
87
75
|
parser (>= 3.3.0.2)
|
|
88
76
|
rainbow (>= 2.2.2, < 4.0)
|
|
89
77
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
90
|
-
rubocop-ast (>= 1.
|
|
78
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
91
79
|
ruby-progressbar (~> 1.7)
|
|
92
80
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
93
|
-
rubocop-ast (1.
|
|
81
|
+
rubocop-ast (1.49.1)
|
|
94
82
|
parser (>= 3.3.7.2)
|
|
95
|
-
prism (~> 1.
|
|
83
|
+
prism (~> 1.7)
|
|
96
84
|
ruby-progressbar (1.13.0)
|
|
97
85
|
sem_version (2.0.1)
|
|
98
86
|
simplecov (0.22.0)
|
|
@@ -113,60 +101,55 @@ PLATFORMS
|
|
|
113
101
|
|
|
114
102
|
DEPENDENCIES
|
|
115
103
|
concurrent-ruby (~> 1.3)
|
|
116
|
-
|
|
104
|
+
dalli (~> 4)
|
|
105
|
+
faraday
|
|
117
106
|
featurehub-sdk!
|
|
118
|
-
ld-eventsource (~> 2.
|
|
107
|
+
ld-eventsource (~> 2.6.0)
|
|
119
108
|
murmurhash3 (~> 0.1.7)
|
|
120
109
|
rake (~> 13.0)
|
|
121
110
|
redis (~> 5)
|
|
122
111
|
rspec (~> 3.0)
|
|
123
|
-
rubocop (~> 1.
|
|
112
|
+
rubocop (~> 1.86)
|
|
124
113
|
sem_version (~> 2.0.0)
|
|
125
114
|
simplecov (~> 0.21)
|
|
126
115
|
|
|
127
116
|
CHECKSUMS
|
|
128
|
-
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
|
|
129
117
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
130
118
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
131
119
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
120
|
+
dalli (4.3.3) sha256=ae58aa3442b0d9e129898f56bc6e3a0f8b6149523e723b3eb124a05ae9a2da0c
|
|
132
121
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
133
122
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
134
123
|
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
|
|
135
124
|
faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd
|
|
136
125
|
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
|
|
137
|
-
featurehub-sdk (2.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ffi-compiler (1.3.2) sha256=a94f3d81d12caf5c5d4ecf13980a70d0aeaa72268f3b9cc13358bcc6509184a0
|
|
142
|
-
http (5.3.1) sha256=c50802d8e9be3926cb84ac3b36d1a31fbbac383bc4cbecdce9053cb604231d7d
|
|
143
|
-
http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
|
|
144
|
-
http-form_data (2.3.0) sha256=cc4eeb1361d9876821e31d7b1cf0b68f1cf874b201d27903480479d86448a5f3
|
|
145
|
-
json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf
|
|
126
|
+
featurehub-sdk (2.1.1)
|
|
127
|
+
http (6.0.2) sha256=be337816fb45cee712eeb0829a16d9300c9d2b87b38b771d177d7a59f77e8b83
|
|
128
|
+
http-cookie (1.1.4) sha256=8dd8011dedcae5f91af2671b7ba878c4a9e89f0f6384790c1f4cdd176f5e3ada
|
|
129
|
+
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
|
146
130
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
147
|
-
ld-eventsource (2.
|
|
131
|
+
ld-eventsource (2.6.0) sha256=e70b34226972d3c729c92787de7f342de08340f7249393aa8b1d4b4e3f0b062d
|
|
148
132
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
149
|
-
llhttp
|
|
133
|
+
llhttp (0.6.1) sha256=9da187ecf6407265465919cc0d691210ef79e38fa6e86e5e45593bdf25b50146
|
|
150
134
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
151
135
|
murmurhash3 (0.1.7) sha256=370a2ce2e9ab0711e51554e530b5f63956927a6554a296855f42a1a4a5ed0936
|
|
152
136
|
net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
|
|
153
|
-
parallel (1.
|
|
154
|
-
parser (3.3.
|
|
155
|
-
prism (1.
|
|
156
|
-
public_suffix (6.0.2) sha256=bfa7cd5108066f8c9602e0d6d4114999a5df5839a63149d3e8b0f9c1d3558394
|
|
137
|
+
parallel (1.28.0) sha256=33e6de1484baf2524792d178b0913fc8eb94c628d6cfe45599ad4458c638c970
|
|
138
|
+
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
|
139
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
157
140
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
158
141
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
159
142
|
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
160
143
|
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
|
161
144
|
redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
|
|
162
|
-
regexp_parser (2.
|
|
145
|
+
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
163
146
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
164
147
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
165
148
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
166
149
|
rspec-mocks (3.13.7) sha256=0979034e64b1d7a838aaaddf12bf065ea4dc40ef3d4c39f01f93ae2c66c62b1c
|
|
167
150
|
rspec-support (3.13.6) sha256=2e8de3702427eab064c9352fe74488cc12a1bfae887ad8b91cba480ec9f8afb2
|
|
168
|
-
rubocop (1.
|
|
169
|
-
rubocop-ast (1.
|
|
151
|
+
rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
|
|
152
|
+
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
|
170
153
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
171
154
|
sem_version (2.0.1) sha256=6d97d4f67e28546ba90b3c290f901d6c8031ddb8e08bce962139739c4d40b183
|
|
172
155
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
data/Makefile
ADDED
data/README.md
CHANGED
|
@@ -295,34 +295,87 @@ The file path defaults to `featurehub-overrides.yaml` or the `FEATUREHUB_LOCAL_Y
|
|
|
295
295
|
|
|
296
296
|
## Caching feature state in Redis
|
|
297
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
|
|
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 periodically re-reads a SHA key so that updates published by other processes are picked up automatically.
|
|
299
299
|
|
|
300
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
301
|
|
|
302
|
+
Multi-process writes are safe: the store uses SHA256-based change detection and Redis `WATCH`/`MULTI`/`EXEC` to atomically update both keys and prevent races between concurrent writers.
|
|
303
|
+
|
|
304
|
+
Pass a `FeatureHubConfig` as the second argument — the store reads `repository` and `environment_id` from it and registers itself as a raw update listener automatically.
|
|
305
|
+
|
|
302
306
|
```ruby
|
|
303
307
|
# Requires the 'redis' gem: gem 'redis', '~> 5'
|
|
304
308
|
store = FeatureHub::Sdk::RedisSessionStore.new(
|
|
305
309
|
"redis://localhost:6379",
|
|
306
|
-
config.repository
|
|
310
|
+
config, # FeatureHubConfig — NOT config.repository
|
|
307
311
|
{
|
|
308
|
-
prefix:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
prefix: "myapp", # Redis key prefix (default: "featurehub")
|
|
313
|
+
db: 0, # Redis DB index (default: 0)
|
|
314
|
+
refresh_timeout: 300, # Seconds between periodic SHA checks (default: 300)
|
|
315
|
+
backoff_timeout: 500, # Milliseconds to wait between WATCH retries (default: 500)
|
|
316
|
+
retry_update_count: 10, # Maximum WATCH retry attempts per write (default: 10)
|
|
317
|
+
logger: my_logger # Optional logger
|
|
313
318
|
}
|
|
314
319
|
)
|
|
315
320
|
|
|
316
|
-
# Register it so it also receives live updates
|
|
317
|
-
config.register_raw_update_listener(store)
|
|
318
|
-
|
|
319
321
|
# Shut down cleanly
|
|
320
322
|
store.close
|
|
321
323
|
```
|
|
322
324
|
|
|
325
|
+
You can also pass an existing Redis client instead of a connection string (e.g. a RedisCluster client or a pre-configured `Redis` instance):
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
redis = Redis.new(url: "redis://localhost:6379", db: 1)
|
|
329
|
+
store = FeatureHub::Sdk::RedisSessionStore.new(redis, config)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
You can also pass a `RedisSessionStoreOptions` object directly:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
opts = FeatureHub::Sdk::RedisSessionStoreOptions.new(prefix: "myapp", db: 2)
|
|
336
|
+
store = FeatureHub::Sdk::RedisSessionStore.new("redis://localhost:6379", config, opts)
|
|
337
|
+
```
|
|
338
|
+
|
|
323
339
|
Redis keys used:
|
|
324
|
-
- `{prefix}
|
|
325
|
-
- `{prefix}_{
|
|
340
|
+
- `{prefix}_{environment_id}` — JSON-encoded array of all feature states
|
|
341
|
+
- `{prefix}_{environment_id}_sha` — SHA256 fingerprint used for cross-process change detection
|
|
342
|
+
|
|
343
|
+
## Caching feature state in Memcache
|
|
344
|
+
|
|
345
|
+
`MemcacheSessionStore` persists feature values from a `FeatureHubRepository` to Memcache. On startup it reads any previously saved features from Memcache and replays them into the repository, then listens for live updates and writes newer versions back. A background timer periodically re-reads a SHA key so that updates published by other processes are picked up automatically.
|
|
346
|
+
|
|
347
|
+
> **Warning:** Do not use `MemcacheSessionStore` with server-evaluated features. Each server-evaluated context resolves to different values; sharing a single Memcache key across processes will cause them to overwrite each other's state.
|
|
348
|
+
|
|
349
|
+
Multi-process writes are safe: the store uses SHA256-based change detection and Dalli's compare-and-set (`cas`) to prevent races between concurrent writers.
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
# Requires the 'dalli' gem: gem 'dalli', '~> 5'
|
|
353
|
+
store = FeatureHub::Sdk::MemcacheSessionStore.new(
|
|
354
|
+
"localhost:11211",
|
|
355
|
+
config,
|
|
356
|
+
{
|
|
357
|
+
prefix: "myapp", # Key prefix (default: "featurehub")
|
|
358
|
+
refresh_timeout: 300, # Seconds between periodic SHA checks (default: 300)
|
|
359
|
+
backoff_timeout: 500, # Milliseconds to wait between CAS retries (default: 500)
|
|
360
|
+
retry_update_count: 10, # Maximum CAS retry attempts per write (default: 10)
|
|
361
|
+
logger: my_logger # Optional logger (default: SDK default logger)
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Shut down cleanly
|
|
366
|
+
store.close
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
You can also pass an existing `Dalli::Client` instead of a connection string:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
dalli = Dalli::Client.new("localhost:11211", serializer: JSON)
|
|
373
|
+
store = FeatureHub::Sdk::MemcacheSessionStore.new(dalli, config)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Memcache keys used:
|
|
377
|
+
- `{prefix}_{environment_id}` — JSON-encoded array of all feature states
|
|
378
|
+
- `{prefix}_{environment_id}_sha` — SHA256 fingerprint used for cross-process change detection
|
|
326
379
|
|
|
327
380
|
## Custom raw update listeners
|
|
328
381
|
|
|
@@ -346,7 +399,7 @@ end
|
|
|
346
399
|
config.register_raw_update_listener(MyAuditListener.new)
|
|
347
400
|
```
|
|
348
401
|
|
|
349
|
-
Callbacks are dispatched asynchronously via `Concurrent::Future`. The `source` parameter will be `"streaming"`, `"polling"`, `"local-yaml"`, `"redis-store"`, or `"unknown"`.
|
|
402
|
+
Callbacks are dispatched asynchronously via `Concurrent::Future`. The `source` parameter will be `"streaming"`, `"polling"`, `"local-yaml"`, `"redis-store"`, `"memcache-store"`, or `"unknown"`.
|
|
350
403
|
|
|
351
404
|
All listeners are closed automatically when `config.close` or `repository.close` is called.
|
|
352
405
|
|
data/examples/sinatra/Dockerfile
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
FROM
|
|
1
|
+
FROM phusion/passenger-ruby33:latest
|
|
2
2
|
|
|
3
3
|
MAINTAINER info@featurehub.io
|
|
4
|
-
ENV BUNDLER_VERSION
|
|
4
|
+
ENV BUNDLER_VERSION 4.0.3
|
|
5
5
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
#ENV BUNDLE_PATH /bundle
|
|
15
|
-
RUN passenger-config build-native-support
|
|
7
|
+
# The purpose of this Dockerfile is to build a test to-do server that can be used against the
|
|
8
|
+
# e2e tests. It *MUST* test the SDK on the current HEAD, so the file is complicated and awkward
|
|
9
|
+
# and not like a typical deploy.
|
|
16
10
|
|
|
17
11
|
RUN echo 'gem: --no-document' >> ~/.gemrc && \
|
|
18
12
|
gem update --system && \
|
|
19
13
|
gem install bundler -v ${BUNDLER_VERSION} --force
|
|
20
14
|
|
|
15
|
+
RUN gem uninstall logger -v 1.6.0
|
|
16
|
+
|
|
21
17
|
# set up nsswitch
|
|
22
18
|
COPY examples/sinatra/conf/nsswitch.conf /etc/nsswitch.conf
|
|
23
19
|
|
|
24
|
-
RUN mkdir -p /app/featurehub
|
|
25
20
|
COPY examples/sinatra/conf/nginx.conf /etc/nginx/nginx.conf
|
|
26
|
-
|
|
21
|
+
ADD . /app/featurehub
|
|
27
22
|
WORKDIR /app/featurehub
|
|
28
|
-
RUN cd /
|
|
29
|
-
|
|
23
|
+
RUN rm Gemfile Gemfile.lock && cd examples/sinatra && bundle config set --local deployment 'true' --without 'development test' path 'vendor/bundle'
|
|
24
|
+
RUN cd /app/featurehub/examples/sinatra && cat .bundle/config && mkdir logs
|
|
25
|
+
RUN cd /app/featurehub/examples/sinatra && bundle install
|
|
26
|
+
RUN chown -R nobody /app
|
|
27
|
+
ENV BUNDLE_PATH=/app/featurehub/examples/sinatra/vendor/bundle
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
USER root
|
|
30
|
+
CMD /usr/sbin/nginx -g 'daemon off;'
|
|
31
|
+
#CMD /usr/bin/bash
|
data/examples/sinatra/Gemfile
CHANGED
|
@@ -4,7 +4,11 @@ source "https://rubygems.org"
|
|
|
4
4
|
|
|
5
5
|
ruby "3.3.10"
|
|
6
6
|
|
|
7
|
+
# 5 prevents us using ruby 3.2 which while EOL is still in our build
|
|
8
|
+
gem "dalli", "~> 4"
|
|
7
9
|
gem "featurehub-sdk", path: "../.."
|
|
10
|
+
# this is just to test 1.x works OK
|
|
11
|
+
gem "faraday", "~> 1"
|
|
8
12
|
gem "rack"
|
|
9
13
|
gem "redis"
|
|
10
14
|
gem "sinatra"
|
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../..
|
|
3
3
|
specs:
|
|
4
|
-
featurehub-sdk (2.
|
|
4
|
+
featurehub-sdk (2.1.1)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
|
-
faraday
|
|
7
|
-
ld-eventsource (
|
|
6
|
+
faraday
|
|
7
|
+
ld-eventsource (>= 2.5.1, < 2.7.0)
|
|
8
8
|
murmurhash3 (~> 0.1.7)
|
|
9
9
|
sem_version (~> 2.0.0)
|
|
10
10
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
addressable (2.
|
|
14
|
+
addressable (2.9.0)
|
|
15
15
|
public_suffix (>= 2.0.2, < 8.0)
|
|
16
16
|
concurrent-ruby (1.3.6)
|
|
17
17
|
connection_pool (3.0.2)
|
|
18
18
|
daemons (1.4.1)
|
|
19
|
+
dalli (4.3.3)
|
|
20
|
+
logger
|
|
19
21
|
domain_name (0.6.20240107)
|
|
20
22
|
eventmachine (1.2.7)
|
|
21
|
-
faraday (
|
|
22
|
-
faraday-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
faraday (1.10.5)
|
|
24
|
+
faraday-em_http (~> 1.0)
|
|
25
|
+
faraday-em_synchrony (~> 1.0)
|
|
26
|
+
faraday-excon (~> 1.1)
|
|
27
|
+
faraday-httpclient (~> 1.0)
|
|
28
|
+
faraday-multipart (~> 1.0)
|
|
29
|
+
faraday-net_http (~> 1.0)
|
|
30
|
+
faraday-net_http_persistent (~> 1.0)
|
|
31
|
+
faraday-patron (~> 1.0)
|
|
32
|
+
faraday-rack (~> 1.0)
|
|
33
|
+
faraday-retry (~> 1.0)
|
|
34
|
+
ruby2_keywords (>= 0.0.4)
|
|
35
|
+
faraday-em_http (1.0.0)
|
|
36
|
+
faraday-em_synchrony (1.0.1)
|
|
37
|
+
faraday-excon (1.1.0)
|
|
38
|
+
faraday-httpclient (1.0.1)
|
|
39
|
+
faraday-multipart (1.2.0)
|
|
40
|
+
multipart-post (~> 2.0)
|
|
41
|
+
faraday-net_http (1.0.2)
|
|
42
|
+
faraday-net_http_persistent (1.2.0)
|
|
43
|
+
faraday-patron (1.0.0)
|
|
44
|
+
faraday-rack (1.0.0)
|
|
45
|
+
faraday-retry (1.0.4)
|
|
27
46
|
ffi (1.17.3-x86_64-darwin)
|
|
47
|
+
ffi (1.17.3-x86_64-linux-gnu)
|
|
28
48
|
ffi-compiler (1.3.2)
|
|
29
49
|
ffi (>= 1.15.5)
|
|
30
50
|
rake
|
|
@@ -36,7 +56,6 @@ GEM
|
|
|
36
56
|
http-cookie (1.1.0)
|
|
37
57
|
domain_name (~> 0.5)
|
|
38
58
|
http-form_data (2.3.0)
|
|
39
|
-
json (2.19.2)
|
|
40
59
|
ld-eventsource (2.5.1)
|
|
41
60
|
concurrent-ruby (~> 1.0)
|
|
42
61
|
http (>= 4.4.1, < 6.0.0)
|
|
@@ -44,11 +63,10 @@ GEM
|
|
|
44
63
|
ffi-compiler (~> 1.0)
|
|
45
64
|
rake (~> 13.0)
|
|
46
65
|
logger (1.7.0)
|
|
66
|
+
multipart-post (2.4.1)
|
|
47
67
|
murmurhash3 (0.1.7)
|
|
48
68
|
mustermann (2.0.2)
|
|
49
69
|
ruby2_keywords (~> 0.0.1)
|
|
50
|
-
net-http (0.9.1)
|
|
51
|
-
uri (>= 0.11.1)
|
|
52
70
|
public_suffix (7.0.5)
|
|
53
71
|
rack (2.2.6.4)
|
|
54
72
|
rack-protection (2.2.3)
|
|
@@ -72,7 +90,6 @@ GEM
|
|
|
72
90
|
eventmachine (~> 1.0, >= 1.0.4)
|
|
73
91
|
rack (>= 1, < 3)
|
|
74
92
|
tilt (2.1.0)
|
|
75
|
-
uri (1.1.1)
|
|
76
93
|
|
|
77
94
|
PLATFORMS
|
|
78
95
|
x86_64-darwin-21
|
|
@@ -80,6 +97,8 @@ PLATFORMS
|
|
|
80
97
|
x86_64-linux
|
|
81
98
|
|
|
82
99
|
DEPENDENCIES
|
|
100
|
+
dalli (~> 4)
|
|
101
|
+
faraday (~> 1)
|
|
83
102
|
featurehub-sdk!
|
|
84
103
|
rack
|
|
85
104
|
redis
|
|
@@ -4,7 +4,23 @@ This is a simple todo server example that we use to run our Cucumber tests again
|
|
|
4
4
|
to ensure that the SDK is operating as expected. Please read the `application.rb`
|
|
5
5
|
for details on how it works, it is fairly simple.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
using `docker_start.sh`
|
|
7
|
+
NOTE: The `Dockerfile` is intended for use in the e2e tests in the pipeline and is not a general use-case sample. It uses Passenger, Sinatra and the FeatureHub SDK on this specific branch. It is, as such, constructed from the root directory (using the `Makefile`).
|
|
9
8
|
|
|
9
|
+
We typically want to run one cache and then two copies of the server. We would normally disconnect the primary server (running on 8099) from edge, leaving the second server running (8100) and writing to the cache.
|
|
10
10
|
|
|
11
|
+
To disconnect from edge, call `curl localhost:8099/health/disconnect`
|
|
12
|
+
|
|
13
|
+
The tests should still pass as the primary server is picking up changes from the cache.
|
|
14
|
+
|
|
15
|
+
== Redis
|
|
16
|
+
|
|
17
|
+
----
|
|
18
|
+
docker run -d -p 6379:6379 --name redis redis
|
|
19
|
+
|
|
20
|
+
----
|
|
21
|
+
|
|
22
|
+
== Memcache
|
|
23
|
+
|
|
24
|
+
----
|
|
25
|
+
docker run -p 11211:11211 --name memcache -d memcached
|
|
26
|
+
----
|