Skip to content

Commit 52239b9

Browse files
authored
Merge pull request #99 from launchdarkly/3.0.3
prepare 3.0.3 release
2 parents c62d796 + 9725a39 commit 52239b9

File tree

3 files changed

+119
-54
lines changed

3 files changed

+119
-54
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Note that this gem will automatically switch to using the Rails logger it is det
8080

8181
HTTPS proxy
8282
------------
83-
The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides a built-in HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
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.
8484

8585
How to set the HTTPS_PROXY environment variable on Mac/Linux systems:
8686
```
@@ -94,6 +94,16 @@ set HTTPS_PROXY=https://web-proxy.domain.com:8080
9494
```
9595

9696

97+
If your proxy requires authentication then you can prefix the URN with your login information:
98+
```
99+
export HTTPS_PROXY=http://user:[email protected]:8080
100+
```
101+
or
102+
```
103+
set HTTPS_PROXY=http://user:[email protected]:8080
104+
```
105+
106+
97107
Your first feature flag
98108
-----------------------
99109

lib/ldclient-rb/redis_store.rb

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ def get(kind, key)
103103
nil
104104
end
105105
end
106-
if !f.nil?
107-
put_cache(kind, key, f)
108-
end
109106
end
110107
if f.nil?
111108
@logger.debug { "RedisFeatureStore: #{key} not found in '#{kind[:namespace]}'" }
@@ -138,50 +135,36 @@ def all(kind)
138135
end
139136

140137
def delete(kind, key, version)
141-
with_connection do |redis|
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 })
157139
end
158140

159141
def init(all_data)
160142
@cache.clear
161143
count = 0
162144
with_connection do |redis|
163145
all_data.each do |kind, items|
164-
redis.multi do |multi|
165-
multi.del(items_key(kind))
166-
count = count + items.count
167-
items.each { |k, v| put_redis_and_cache(kind, multi, k, v) }
168-
end
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
169160
end
170161
end
171162
@inited.set(true)
172163
@logger.info { "RedisFeatureStore: initialized with #{count} items" }
173164
end
174165

175166
def upsert(kind, item)
176-
with_connection do |redis|
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)
185168
end
186169

187170
def initialized?
@@ -195,13 +178,12 @@ def stop
195178
end
196179
end
197180

181+
private
182+
198183
# exposed for testing
199-
def clear_local_cache()
200-
@cache.clear
184+
def before_update_transaction(base_key, key)
201185
end
202186

203-
private
204-
205187
def items_key(kind)
206188
@prefix + ":" + kind[:namespace]
207189
end
@@ -217,7 +199,13 @@ def with_connection
217199
def get_redis(kind, redis, key)
218200
begin
219201
json_item = redis.hget(items_key(kind), key)
220-
JSON.parse(json_item, symbolize_names: true) if json_item
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
221209
rescue => e
222210
@logger.error { "RedisFeatureStore: could not retrieve #{key} from Redis, error: #{e}" }
223211
nil
@@ -228,13 +216,39 @@ def put_cache(kind, key, value)
228216
@cache.store(cache_key(kind, key), value, expires: @expiration_seconds)
229217
end
230218

231-
def put_redis_and_cache(kind, redis, key, item)
232-
begin
233-
redis.hset(items_key(kind), key, item.to_json)
234-
rescue => e
235-
@logger.error { "RedisFeatureStore: could not store #{key} in Redis, error: #{e}" }
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
236251
end
237-
put_cache(kind, key.to_sym, item)
238252
end
239253

240254
def query_inited

spec/redis_feature_store_spec.rb

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "feature_store_spec_base"
22
require "json"
3+
require "redis"
34
require "spec_helper"
45

56

@@ -21,23 +22,63 @@ def create_redis_store_uncached()
2122
describe LaunchDarkly::RedisFeatureStore do
2223
subject { LaunchDarkly::RedisFeatureStore }
2324

24-
let(:feature0_with_higher_version) do
25-
f = feature0.clone
26-
f[:version] = feature0[:version] + 10
27-
f
28-
end
29-
3025
# These tests will all fail if there isn't a Redis instance running on the default port.
3126

3227
context "real Redis with local cache" do
33-
3428
include_examples "feature_store", method(:create_redis_store)
35-
3629
end
3730

3831
context "real Redis without local cache" do
39-
4032
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)
4175

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
4283
end
4384
end

0 commit comments

Comments
 (0)