ldclient-rb 3.0.2 → 3.0.3
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 +4 -0
- data/README.md +11 -1
- data/lib/ldclient-rb/redis_store.rb +58 -44
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/redis_feature_store_spec.rb +50 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c13b41385d09caf349197da95dc022227f5e143
|
4
|
+
data.tar.gz: d9e5536ac51a27fef6e1bacbdde3385d04c5d599
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9af4fc567f889a50dad1011367e6f2abf460f552645e850bd1364f12fd385da62a8f564d7fcb119e87ae51c135e5f0e174e8ecec956b4fce03392f5f927d953
|
7
|
+
data.tar.gz: db400282e1500e4b627b8d9b94df8e60b87aa17756f7ceabeb7801346fd64199b038ae8388c3d4c0fe46ffe5517706670ed605b16e13e93a97129a05534cd2d6
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
+
## [3.0.3] - 2018-03-23
|
6
|
+
## Fixed
|
7
|
+
- In the Redis feature store, fixed a synchronization problem that could cause a feature flag update to be missed if several of them happened in rapid succession.
|
8
|
+
|
5
9
|
## [3.0.2] - 2018-03-06
|
6
10
|
## Fixed
|
7
11
|
- Improved efficiency of logging by not constructing messages that won't be visible at the current log level. (Thanks, [julik](https://github.com/launchdarkly/ruby-client/pull/98)!)
|
data/README.md
CHANGED
@@ -80,7 +80,7 @@ Note that this gem will automatically switch to using the Rails logger it is det
|
|
80
80
|
|
81
81
|
HTTPS proxy
|
82
82
|
------------
|
83
|
-
The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides
|
83
|
+
The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides built-in support for the use of an HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
|
84
84
|
|
85
85
|
How to set the HTTPS_PROXY environment variable on Mac/Linux systems:
|
86
86
|
```
|
@@ -94,6 +94,16 @@ set HTTPS_PROXY=https://web-proxy.domain.com:8080
|
|
94
94
|
```
|
95
95
|
|
96
96
|
|
97
|
+
If your proxy requires authentication then you can prefix the URN with your login information:
|
98
|
+
```
|
99
|
+
export HTTPS_PROXY=http://user:pass@web-proxy.domain.com:8080
|
100
|
+
```
|
101
|
+
or
|
102
|
+
```
|
103
|
+
set HTTPS_PROXY=http://user:pass@web-proxy.domain.com:8080
|
104
|
+
```
|
105
|
+
|
106
|
+
|
97
107
|
Your first feature flag
|
98
108
|
-----------------------
|
99
109
|
|
@@ -103,9 +103,6 @@ and prefix: #{@prefix}")
|
|
103
103
|
nil
|
104
104
|
end
|
105
105
|
end
|
106
|
-
if !f.nil?
|
107
|
-
put_cache(kind, key, f)
|
108
|
-
end
|
109
106
|
end
|
110
107
|
if f.nil?
|
111
108
|
@logger.debug { "RedisFeatureStore: #{key} not found in '#{kind[:namespace]}'" }
|
@@ -138,22 +135,7 @@ and prefix: #{@prefix}")
|
|
138
135
|
end
|
139
136
|
|
140
137
|
def delete(kind, key, version)
|
141
|
-
|
142
|
-
f = get_redis(kind, redis, key)
|
143
|
-
if f.nil?
|
144
|
-
put_redis_and_cache(kind, redis, key, { deleted: true, version: version })
|
145
|
-
else
|
146
|
-
if f[:version] < version
|
147
|
-
f1 = f.clone
|
148
|
-
f1[:deleted] = true
|
149
|
-
f1[:version] = version
|
150
|
-
put_redis_and_cache(kind, redis, key, f1)
|
151
|
-
else
|
152
|
-
@logger.warn("RedisFeatureStore: attempted to delete #{key} version: #{f[:version]} \
|
153
|
-
in '#{kind[:namespace]}' with a version that is the same or older: #{version}")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
138
|
+
update_with_versioning(kind, { key: key, version: version, deleted: true })
|
157
139
|
end
|
158
140
|
|
159
141
|
def init(all_data)
|
@@ -161,11 +143,20 @@ and prefix: #{@prefix}")
|
|
161
143
|
count = 0
|
162
144
|
with_connection do |redis|
|
163
145
|
all_data.each do |kind, items|
|
164
|
-
|
165
|
-
multi
|
166
|
-
|
167
|
-
|
168
|
-
|
146
|
+
begin
|
147
|
+
redis.multi do |multi|
|
148
|
+
multi.del(items_key(kind))
|
149
|
+
count = count + items.count
|
150
|
+
items.each { |key, item|
|
151
|
+
redis.hset(items_key(kind), key, item.to_json)
|
152
|
+
}
|
153
|
+
end
|
154
|
+
items.each { |key, item|
|
155
|
+
put_cache(kind, key.to_sym, item)
|
156
|
+
}
|
157
|
+
rescue => e
|
158
|
+
@logger.error { "RedisFeatureStore: could not initialize '#{kind[:namespace]}' in Redis, error: #{e}" }
|
159
|
+
end
|
169
160
|
end
|
170
161
|
end
|
171
162
|
@inited.set(true)
|
@@ -173,15 +164,7 @@ and prefix: #{@prefix}")
|
|
173
164
|
end
|
174
165
|
|
175
166
|
def upsert(kind, item)
|
176
|
-
|
177
|
-
redis.watch(items_key(kind)) do
|
178
|
-
old = get_redis(kind, redis, item[:key])
|
179
|
-
if old.nil? || (old[:version] < item[:version])
|
180
|
-
put_redis_and_cache(kind, redis, item[:key], item)
|
181
|
-
end
|
182
|
-
redis.unwatch
|
183
|
-
end
|
184
|
-
end
|
167
|
+
update_with_versioning(kind, item)
|
185
168
|
end
|
186
169
|
|
187
170
|
def initialized?
|
@@ -195,13 +178,12 @@ and prefix: #{@prefix}")
|
|
195
178
|
end
|
196
179
|
end
|
197
180
|
|
181
|
+
private
|
182
|
+
|
198
183
|
# exposed for testing
|
199
|
-
def
|
200
|
-
@cache.clear
|
184
|
+
def before_update_transaction(base_key, key)
|
201
185
|
end
|
202
186
|
|
203
|
-
private
|
204
|
-
|
205
187
|
def items_key(kind)
|
206
188
|
@prefix + ":" + kind[:namespace]
|
207
189
|
end
|
@@ -217,7 +199,13 @@ and prefix: #{@prefix}")
|
|
217
199
|
def get_redis(kind, redis, key)
|
218
200
|
begin
|
219
201
|
json_item = redis.hget(items_key(kind), key)
|
220
|
-
|
202
|
+
if json_item
|
203
|
+
item = JSON.parse(json_item, symbolize_names: true)
|
204
|
+
put_cache(kind, key, item)
|
205
|
+
item
|
206
|
+
else
|
207
|
+
nil
|
208
|
+
end
|
221
209
|
rescue => e
|
222
210
|
@logger.error { "RedisFeatureStore: could not retrieve #{key} from Redis, error: #{e}" }
|
223
211
|
nil
|
@@ -228,13 +216,39 @@ and prefix: #{@prefix}")
|
|
228
216
|
@cache.store(cache_key(kind, key), value, expires: @expiration_seconds)
|
229
217
|
end
|
230
218
|
|
231
|
-
def
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
219
|
+
def update_with_versioning(kind, new_item)
|
220
|
+
base_key = items_key(kind)
|
221
|
+
key = new_item[:key]
|
222
|
+
try_again = true
|
223
|
+
while try_again
|
224
|
+
try_again = false
|
225
|
+
with_connection do |redis|
|
226
|
+
redis.watch(base_key) do
|
227
|
+
old_item = get_redis(kind, redis, key)
|
228
|
+
before_update_transaction(base_key, key)
|
229
|
+
if old_item.nil? || old_item[:version] < new_item[:version]
|
230
|
+
begin
|
231
|
+
result = redis.multi do |multi|
|
232
|
+
multi.hset(base_key, key, new_item.to_json)
|
233
|
+
end
|
234
|
+
if result.nil?
|
235
|
+
@logger.debug { "RedisFeatureStore: concurrent modification detected, retrying" }
|
236
|
+
try_again = true
|
237
|
+
else
|
238
|
+
put_cache(kind, key.to_sym, new_item)
|
239
|
+
end
|
240
|
+
rescue => e
|
241
|
+
@logger.error { "RedisFeatureStore: could not store #{key} in Redis, error: #{e}" }
|
242
|
+
end
|
243
|
+
else
|
244
|
+
action = new_item[:deleted] ? "delete" : "update"
|
245
|
+
@logger.warn { "RedisFeatureStore: attempted to #{action} #{key} version: #{old_item[:version]} \
|
246
|
+
in '#{kind[:namespace]}' with a version that is the same or older: #{new_item[:version]}" }
|
247
|
+
end
|
248
|
+
redis.unwatch
|
249
|
+
end
|
250
|
+
end
|
236
251
|
end
|
237
|
-
put_cache(kind, key.to_sym, item)
|
238
252
|
end
|
239
253
|
|
240
254
|
def query_inited
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "feature_store_spec_base"
|
2
2
|
require "json"
|
3
|
+
require "redis"
|
3
4
|
require "spec_helper"
|
4
5
|
|
5
6
|
|
@@ -21,23 +22,63 @@ end
|
|
21
22
|
describe LaunchDarkly::RedisFeatureStore do
|
22
23
|
subject { LaunchDarkly::RedisFeatureStore }
|
23
24
|
|
24
|
-
let(:feature0_with_higher_version) do
|
25
|
-
f = feature0.clone
|
26
|
-
f[:version] = feature0[:version] + 10
|
27
|
-
f
|
28
|
-
end
|
29
|
-
|
30
25
|
# These tests will all fail if there isn't a Redis instance running on the default port.
|
31
26
|
|
32
27
|
context "real Redis with local cache" do
|
33
|
-
|
34
28
|
include_examples "feature_store", method(:create_redis_store)
|
35
|
-
|
36
29
|
end
|
37
30
|
|
38
31
|
context "real Redis without local cache" do
|
39
|
-
|
40
32
|
include_examples "feature_store", method(:create_redis_store_uncached)
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_concurrent_modifier(store, other_client, flag, start_version, end_version)
|
36
|
+
version_counter = start_version
|
37
|
+
expect(store).to receive(:before_update_transaction) { |base_key, key|
|
38
|
+
if version_counter <= end_version
|
39
|
+
new_flag = flag.clone
|
40
|
+
new_flag[:version] = version_counter
|
41
|
+
other_client.hset(base_key, key, new_flag.to_json)
|
42
|
+
version_counter = version_counter + 1
|
43
|
+
end
|
44
|
+
}.at_least(:once)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "handles upsert race condition against external client with lower version" do
|
48
|
+
store = create_redis_store
|
49
|
+
other_client = Redis.new({ url: "redis://localhost:6379" })
|
50
|
+
|
51
|
+
begin
|
52
|
+
flag = { key: "foo", version: 1 }
|
53
|
+
store.init(LaunchDarkly::FEATURES => { flag[:key] => flag })
|
54
|
+
|
55
|
+
add_concurrent_modifier(store, other_client, flag, 2, 4)
|
56
|
+
|
57
|
+
my_ver = { key: "foo", version: 10 }
|
58
|
+
store.upsert(LaunchDarkly::FEATURES, my_ver)
|
59
|
+
result = store.get(LaunchDarkly::FEATURES, flag[:key])
|
60
|
+
expect(result[:version]).to eq 10
|
61
|
+
ensure
|
62
|
+
other_client.close
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "handles upsert race condition against external client with higher version" do
|
67
|
+
store = create_redis_store
|
68
|
+
other_client = Redis.new({ url: "redis://localhost:6379" })
|
69
|
+
|
70
|
+
begin
|
71
|
+
flag = { key: "foo", version: 1 }
|
72
|
+
store.init(LaunchDarkly::FEATURES => { flag[:key] => flag })
|
73
|
+
|
74
|
+
add_concurrent_modifier(store, other_client, flag, 3, 3)
|
41
75
|
|
76
|
+
my_ver = { key: "foo", version: 2 }
|
77
|
+
store.upsert(LaunchDarkly::FEATURES, my_ver)
|
78
|
+
result = store.get(LaunchDarkly::FEATURES, flag[:key])
|
79
|
+
expect(result[:version]).to eq 3
|
80
|
+
ensure
|
81
|
+
other_client.close
|
82
|
+
end
|
42
83
|
end
|
43
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ldclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|