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 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