ldclient-rb 3.0.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 41869ab263a2b79a395f296f5bdc0be3ff7c6be5
4
- data.tar.gz: 180e6a5ca71016e6776a1b3f484824a604b3cbfb
3
+ metadata.gz: 7c13b41385d09caf349197da95dc022227f5e143
4
+ data.tar.gz: d9e5536ac51a27fef6e1bacbdde3385d04c5d599
5
5
  SHA512:
6
- metadata.gz: b9301b40618e1fd75e1edefcafc2207da147af8ad0ca03e2b945c2e623675cc88c1ad1d62672730aaf1a4f7f8a25a48e0577b8a6037d18cde486d4289b418c69
7
- data.tar.gz: 94a9e852962fcdaf98f2a90b257c06ef3107fd59882d8e92e1ae44fc4239b4f1e37a8875ebaa7c14bafa0e33d453c118b8dca4aa734164496ac608e3f21f1258
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 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.
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
- 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 })
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
- 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
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
- 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)
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 clear_local_cache()
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
- 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
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 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
236
251
  end
237
- put_cache(kind, key.to_sym, item)
238
252
  end
239
253
 
240
254
  def query_inited
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "3.0.2"
2
+ VERSION = "3.0.3"
3
3
  end
@@ -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.2
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-06 00:00:00.000000000 Z
11
+ date: 2018-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler