ldclient-rb 2.2.7 → 2.3.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/CHANGELOG.md +16 -0
- data/CODEOWNERS +1 -0
- data/circle.yml +2 -0
- data/ldclient-rb.gemspec +5 -2
- data/lib/ldclient-rb.rb +2 -0
- data/lib/ldclient-rb/config.rb +42 -0
- data/lib/ldclient-rb/event_serializer.rb +51 -0
- data/lib/ldclient-rb/events.rb +26 -5
- data/lib/ldclient-rb/feature_store.rb +4 -0
- data/lib/ldclient-rb/ldclient.rb +23 -4
- data/lib/ldclient-rb/polling.rb +15 -2
- data/lib/ldclient-rb/redis_feature_store.rb +234 -0
- data/lib/ldclient-rb/requestor.rb +4 -1
- data/lib/ldclient-rb/stream.rb +24 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/event_serializer_spec.rb +86 -0
- data/spec/feature_store_spec_base.rb +112 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/ldclient_spec.rb +22 -0
- data/spec/redis_feature_store_spec.rb +43 -0
- metadata +59 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bfb8d22b2b4294ec4096d0c3131e20390bf2634
|
4
|
+
data.tar.gz: 42d06dbed907402e81213b207bbcf4ccce52dd5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3b59e2fdbec17b31cc7c6450e1485afa9a522009990528b998f2f1782711049b86db19f56ba6212d09c931a23a5f92b1078fb70c071fd8adbbd1116990d8601
|
7
|
+
data.tar.gz: 6d2bdc142c97b704ed5ad211bb9eee18f3b0e2bf5deaf5ac2957b3d8fe8475f591e28bdc4ad604665b5154fbf96548fe058a6dbeeed4e98d9ed51320738c7994
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,22 @@
|
|
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
|
+
## [2.3.1] - 2017-11-16
|
6
|
+
|
7
|
+
### Changed
|
8
|
+
- Include source code for changes described in 2.3.0
|
9
|
+
|
10
|
+
|
11
|
+
## [2.3.0] - 2017-11-16
|
12
|
+
## Added
|
13
|
+
- Add `close` method to Ruby client to stop processing events
|
14
|
+
- Add support for Redis feature store
|
15
|
+
- Add support for LDD mode
|
16
|
+
- Allow user to disable outgoing event stream.
|
17
|
+
|
18
|
+
## Changed
|
19
|
+
- Stop retrying on 401 responses (due to bad sdk keys)
|
20
|
+
|
5
21
|
## [2.2.7] - 2017-07-26
|
6
22
|
## Changed
|
7
23
|
- Update Readme to fix instructions on installing gem using command line
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @ashanbrown
|
data/circle.yml
CHANGED
data/ldclient-rb.gemspec
CHANGED
@@ -25,7 +25,10 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
26
|
spec.add_development_dependency "rspec", "~> 3.2"
|
27
27
|
spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
|
28
|
-
|
28
|
+
spec.add_development_dependency "redis", "~> 3.3.5"
|
29
|
+
spec.add_development_dependency "connection_pool", ">= 2.1.2"
|
30
|
+
spec.add_development_dependency "moneta", "~> 1.0.0"
|
31
|
+
|
29
32
|
spec.add_runtime_dependency "json", "~> 1.8"
|
30
33
|
spec.add_runtime_dependency "faraday", "~> 0.9"
|
31
34
|
spec.add_runtime_dependency "faraday-http-cache", "~> 1.3.0"
|
@@ -33,7 +36,7 @@ Gem::Specification.new do |spec|
|
|
33
36
|
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
|
34
37
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.4"
|
35
38
|
spec.add_runtime_dependency "hashdiff", "~> 0.2"
|
36
|
-
spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.
|
39
|
+
spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.11.0"
|
37
40
|
spec.add_runtime_dependency "celluloid", "~> 0.18.0.pre" # transitive dep; specified here for more control
|
38
41
|
|
39
42
|
if RUBY_VERSION >= "2.2.2"
|
data/lib/ldclient-rb.rb
CHANGED
@@ -6,6 +6,8 @@ require "ldclient-rb/config"
|
|
6
6
|
require "ldclient-rb/newrelic"
|
7
7
|
require "ldclient-rb/stream"
|
8
8
|
require "ldclient-rb/polling"
|
9
|
+
require "ldclient-rb/event_serializer"
|
9
10
|
require "ldclient-rb/events"
|
10
11
|
require "ldclient-rb/feature_store"
|
12
|
+
require "ldclient-rb/redis_feature_store"
|
11
13
|
require "ldclient-rb/requestor"
|
data/lib/ldclient-rb/config.rb
CHANGED
@@ -34,12 +34,23 @@ module LaunchDarkly
|
|
34
34
|
# @option opts [Object] :cache_store A cache store for the Faraday HTTP caching
|
35
35
|
# library. Defaults to the Rails cache in a Rails environment, or a
|
36
36
|
# thread-safe in-memory store otherwise.
|
37
|
+
# @option opts [Boolean] :use_ldd (false) Whether you are using the LaunchDarkly relay proxy in
|
38
|
+
# daemon mode. In this configuration, the client will not use a streaming connection to listen
|
39
|
+
# for updates, but instead will get feature state from a Redis instance. The `stream` and
|
40
|
+
# `poll_interval` options will be ignored if this option is set to true.
|
37
41
|
# @option opts [Boolean] :offline (false) Whether the client should be initialized in
|
38
42
|
# offline mode. In offline mode, default values are returned for all flags and no
|
39
43
|
# remote network requests are made.
|
40
44
|
# @option opts [Float] :poll_interval (1) The number of seconds between polls for flag updates
|
41
45
|
# if streaming is off.
|
42
46
|
# @option opts [Boolean] :stream (true) Whether or not the streaming API should be used to receive flag updates.
|
47
|
+
# @option opts [Boolean] all_attributes_private (false) If true, all user attributes (other than the key)
|
48
|
+
# will be private, not just the attributes specified in `private_attribute_names`.
|
49
|
+
# @option opts [Array] :private_attribute_names Marks a set of attribute names private. Any users sent to
|
50
|
+
# LaunchDarkly with this configuration active will have attributes with these names removed.
|
51
|
+
# @option opts [Boolean] :send_events (true) Whether or not to send events back to LaunchDarkly.
|
52
|
+
# This differs from `offline` in that it affects only the sending of client-side events, not
|
53
|
+
# streaming or polling for events from the server.
|
43
54
|
#
|
44
55
|
# @return [type] [description]
|
45
56
|
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
@@ -55,9 +66,13 @@ module LaunchDarkly
|
|
55
66
|
@read_timeout = opts[:read_timeout] || Config.default_read_timeout
|
56
67
|
@feature_store = opts[:feature_store] || Config.default_feature_store
|
57
68
|
@stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
|
69
|
+
@use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
|
58
70
|
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
|
59
71
|
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > 1 ? opts[:poll_interval] : Config.default_poll_interval
|
60
72
|
@proxy = opts[:proxy] || Config.default_proxy
|
73
|
+
@all_attributes_private = opts[:all_attributes_private] || false
|
74
|
+
@private_attribute_names = opts[:private_attribute_names] || []
|
75
|
+
@send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
|
61
76
|
end
|
62
77
|
|
63
78
|
#
|
@@ -87,6 +102,16 @@ module LaunchDarkly
|
|
87
102
|
@stream
|
88
103
|
end
|
89
104
|
|
105
|
+
#
|
106
|
+
# Whether to use the LaunchDarkly relay proxy in daemon mode. In this mode, we do
|
107
|
+
# not use polling or streaming to get feature flag updates from the server, but instead
|
108
|
+
# read them from a Redis instance that is updated by the proxy.
|
109
|
+
#
|
110
|
+
# @return [Boolean] True if using the LaunchDarkly relay proxy in daemon mode
|
111
|
+
def use_ldd?
|
112
|
+
@use_ldd
|
113
|
+
end
|
114
|
+
|
90
115
|
# TODO docs
|
91
116
|
def offline?
|
92
117
|
@offline
|
@@ -150,6 +175,15 @@ module LaunchDarkly
|
|
150
175
|
#
|
151
176
|
attr_reader :proxy
|
152
177
|
|
178
|
+
attr_reader :all_attributes_private
|
179
|
+
|
180
|
+
attr_reader :private_attribute_names
|
181
|
+
|
182
|
+
#
|
183
|
+
# Whether to send events back to LaunchDarkly.
|
184
|
+
#
|
185
|
+
attr_reader :send_events
|
186
|
+
|
153
187
|
#
|
154
188
|
# The default LaunchDarkly client configuration. This configuration sets
|
155
189
|
# reasonable defaults for most users.
|
@@ -209,6 +243,10 @@ module LaunchDarkly
|
|
209
243
|
true
|
210
244
|
end
|
211
245
|
|
246
|
+
def self.default_use_ldd
|
247
|
+
false
|
248
|
+
end
|
249
|
+
|
212
250
|
def self.default_feature_store
|
213
251
|
InMemoryFeatureStore.new
|
214
252
|
end
|
@@ -220,5 +258,9 @@ module LaunchDarkly
|
|
220
258
|
def self.default_poll_interval
|
221
259
|
1
|
222
260
|
end
|
261
|
+
|
262
|
+
def self.default_send_events
|
263
|
+
true
|
264
|
+
end
|
223
265
|
end
|
224
266
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
class EventSerializer
|
5
|
+
def initialize(config)
|
6
|
+
@all_attributes_private = config.all_attributes_private
|
7
|
+
@private_attribute_names = Set.new(config.private_attribute_names.map(&:to_sym))
|
8
|
+
end
|
9
|
+
|
10
|
+
def serialize_events(events)
|
11
|
+
events.map { |event|
|
12
|
+
Hash[event.map { |key, value|
|
13
|
+
[key, (key.to_sym == :user) ? transform_user_props(value) : value]
|
14
|
+
}]
|
15
|
+
}.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
IGNORED_TOP_LEVEL_KEYS = Set.new([:custom, :key, :privateAttributeNames])
|
21
|
+
STRIPPED_TOP_LEVEL_KEYS = Set.new([:privateAttributeNames])
|
22
|
+
|
23
|
+
def filter_values(props, user_private_attrs, ignore=[])
|
24
|
+
removed_keys = Set.new(props.keys.select { |key|
|
25
|
+
!ignore.include?(key) && private_attr?(key, user_private_attrs)
|
26
|
+
})
|
27
|
+
filtered_hash = props.select { |key, value| !removed_keys.include?(key) && !STRIPPED_TOP_LEVEL_KEYS.include?(key) }
|
28
|
+
[filtered_hash, removed_keys]
|
29
|
+
end
|
30
|
+
|
31
|
+
def private_attr?(name, user_private_attrs)
|
32
|
+
@all_attributes_private || @private_attribute_names.include?(name) || user_private_attrs.include?(name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def transform_user_props(user_props)
|
36
|
+
user_private_attrs = Set.new((user_props[:privateAttributeNames] || []).map(&:to_sym))
|
37
|
+
|
38
|
+
filtered_user_props, removed = filter_values(user_props, user_private_attrs, IGNORED_TOP_LEVEL_KEYS)
|
39
|
+
if user_props.has_key?(:custom)
|
40
|
+
filtered_user_props[:custom], removed_custom = filter_values(user_props[:custom], user_private_attrs)
|
41
|
+
removed.merge(removed_custom)
|
42
|
+
end
|
43
|
+
|
44
|
+
unless removed.empty?
|
45
|
+
# note, :privateAttributeNames is what the developer sets; :privateAttrs is what we send to the server
|
46
|
+
filtered_user_props[:privateAttrs] = removed.to_a.sort
|
47
|
+
end
|
48
|
+
return filtered_user_props
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/ldclient-rb/events.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "concurrent/atomics"
|
1
2
|
require "thread"
|
2
3
|
require "faraday"
|
3
4
|
|
@@ -7,13 +8,28 @@ module LaunchDarkly
|
|
7
8
|
@queue = Queue.new
|
8
9
|
@sdk_key = sdk_key
|
9
10
|
@config = config
|
11
|
+
@serializer = EventSerializer.new(config)
|
10
12
|
@client = Faraday.new
|
11
|
-
@
|
13
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
14
|
+
@worker = create_worker if @config.send_events
|
15
|
+
end
|
16
|
+
|
17
|
+
def alive?
|
18
|
+
!@stopped.value
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop
|
22
|
+
if @stopped.make_true
|
23
|
+
# There seems to be no such thing as "close" in Faraday: https://github.com/lostisland/faraday/issues/241
|
24
|
+
if !@worker.nil? && @worker.alive?
|
25
|
+
@worker.raise "shutting down client"
|
26
|
+
end
|
27
|
+
end
|
12
28
|
end
|
13
29
|
|
14
30
|
def create_worker
|
15
31
|
Thread.new do
|
16
|
-
|
32
|
+
while !@stopped.value do
|
17
33
|
begin
|
18
34
|
flush
|
19
35
|
sleep(@config.flush_interval)
|
@@ -29,16 +45,21 @@ module LaunchDarkly
|
|
29
45
|
req.headers["Authorization"] = @sdk_key
|
30
46
|
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
|
31
47
|
req.headers["Content-Type"] = "application/json"
|
32
|
-
req.body = events
|
48
|
+
req.body = @serializer.serialize_events(events)
|
33
49
|
req.options.timeout = @config.read_timeout
|
34
50
|
req.options.open_timeout = @config.connect_timeout
|
35
51
|
end
|
36
52
|
if res.status < 200 || res.status >= 300
|
37
53
|
@config.logger.error("[LDClient] Unexpected status code while processing events: #{res.status}")
|
54
|
+
if res.status == 401
|
55
|
+
@config.logger.error("[LDClient] Received 401 error, no further events will be posted since SDK key is invalid")
|
56
|
+
stop
|
57
|
+
end
|
38
58
|
end
|
39
59
|
end
|
40
60
|
|
41
61
|
def flush
|
62
|
+
return if @offline || !@config.send_events
|
42
63
|
events = []
|
43
64
|
begin
|
44
65
|
loop do
|
@@ -47,13 +68,13 @@ module LaunchDarkly
|
|
47
68
|
rescue ThreadError
|
48
69
|
end
|
49
70
|
|
50
|
-
if !events.empty?
|
71
|
+
if !events.empty? && !@stopped.value
|
51
72
|
post_flushed_events(events)
|
52
73
|
end
|
53
74
|
end
|
54
75
|
|
55
76
|
def add_event(event)
|
56
|
-
return if @offline
|
77
|
+
return if @offline || !@config.send_events || @stopped.value
|
57
78
|
|
58
79
|
if @queue.length < @config.capacity
|
59
80
|
event[:creationDate] = (Time.now.to_f * 1000).to_i
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -27,6 +27,14 @@ module LaunchDarkly
|
|
27
27
|
@sdk_key = sdk_key
|
28
28
|
@config = config
|
29
29
|
@store = config.feature_store
|
30
|
+
|
31
|
+
@event_processor = EventProcessor.new(sdk_key, config)
|
32
|
+
|
33
|
+
if @config.use_ldd?
|
34
|
+
@config.logger.info("[LDClient] Started LaunchDarkly Client in LDD mode")
|
35
|
+
return # requestor and update processor are not used in this mode
|
36
|
+
end
|
37
|
+
|
30
38
|
requestor = Requestor.new(sdk_key, config)
|
31
39
|
|
32
40
|
if !@config.offline?
|
@@ -38,8 +46,6 @@ module LaunchDarkly
|
|
38
46
|
@update_processor.start
|
39
47
|
end
|
40
48
|
|
41
|
-
@event_processor = EventProcessor.new(sdk_key, config)
|
42
|
-
|
43
49
|
if !@config.offline? && wait_for_sec > 0
|
44
50
|
begin
|
45
51
|
WaitUtil.wait_for_condition("LaunchDarkly client initialization", timeout_sec: wait_for_sec, delay_sec: 0.1) do
|
@@ -67,7 +73,7 @@ module LaunchDarkly
|
|
67
73
|
# Returns whether the client has been initialized and is ready to serve feature flag requests
|
68
74
|
# @return [Boolean] true if the client has been initialized
|
69
75
|
def initialized?
|
70
|
-
@update_processor.initialized?
|
76
|
+
@config.offline? || @config.use_ldd? || @update_processor.initialized?
|
71
77
|
end
|
72
78
|
|
73
79
|
#
|
@@ -111,7 +117,7 @@ module LaunchDarkly
|
|
111
117
|
return default
|
112
118
|
end
|
113
119
|
|
114
|
-
if !@update_processor.initialized?
|
120
|
+
if !@update_processor.nil? && !@update_processor.initialized?
|
115
121
|
@config.logger.error("[LDClient] Client has not finished initializing. Returning default value")
|
116
122
|
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
117
123
|
return default
|
@@ -195,6 +201,19 @@ module LaunchDarkly
|
|
195
201
|
end
|
196
202
|
end
|
197
203
|
|
204
|
+
#
|
205
|
+
# Releases all network connections and other resources held by the client, making it no longer usable
|
206
|
+
#
|
207
|
+
# @return [void]
|
208
|
+
def close
|
209
|
+
@config.logger.info("[LDClient] Closing LaunchDarkly client...")
|
210
|
+
if not @config.offline?
|
211
|
+
@update_processor.stop
|
212
|
+
end
|
213
|
+
@event_processor.stop
|
214
|
+
@store.stop
|
215
|
+
end
|
216
|
+
|
198
217
|
def log_exception(caller, exn)
|
199
218
|
error_traceback = "#{exn.inspect} #{exn}\n\t#{exn.backtrace.join("\n\t")}"
|
200
219
|
error = "[LDClient] Unexpected exception in #{caller}: #{error_traceback}"
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -8,6 +8,7 @@ module LaunchDarkly
|
|
8
8
|
@requestor = requestor
|
9
9
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
10
10
|
@started = Concurrent::AtomicBoolean.new(false)
|
11
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
11
12
|
end
|
12
13
|
|
13
14
|
def initialized?
|
@@ -20,6 +21,15 @@ module LaunchDarkly
|
|
20
21
|
create_worker
|
21
22
|
end
|
22
23
|
|
24
|
+
def stop
|
25
|
+
if @stopped.make_true
|
26
|
+
if @worker && @worker.alive?
|
27
|
+
@worker.raise "shutting down client"
|
28
|
+
end
|
29
|
+
@config.logger.info("[LDClient] Polling connection stopped")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
23
33
|
def poll
|
24
34
|
flags = @requestor.request_all_flags
|
25
35
|
if flags
|
@@ -31,9 +41,9 @@ module LaunchDarkly
|
|
31
41
|
end
|
32
42
|
|
33
43
|
def create_worker
|
34
|
-
Thread.new do
|
44
|
+
@worker = Thread.new do
|
35
45
|
@config.logger.debug("[LDClient] Starting polling worker")
|
36
|
-
|
46
|
+
while !@stopped.value do
|
37
47
|
begin
|
38
48
|
started_at = Time.now
|
39
49
|
poll
|
@@ -41,6 +51,9 @@ module LaunchDarkly
|
|
41
51
|
if delta > 0
|
42
52
|
sleep(delta)
|
43
53
|
end
|
54
|
+
rescue InvalidSDKKeyError
|
55
|
+
@config.logger.error("[LDClient] Received 401 error, no further polling requests will be made since SDK key is invalid");
|
56
|
+
stop
|
44
57
|
rescue StandardError => exn
|
45
58
|
@config.logger.error("[LDClient] Exception while polling: #{exn.inspect}")
|
46
59
|
# TODO: log_exception(__method__.to_s, exn)
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "json"
|
3
|
+
require "thread_safe"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
#
|
7
|
+
# An implementation of the LaunchDarkly client's feature store that uses a Redis
|
8
|
+
# instance. Feature data can also be further cached in memory to reduce overhead
|
9
|
+
# of calls to Redis.
|
10
|
+
#
|
11
|
+
# To use this class, you must first have the `redis`, `connection-pool`, and `moneta`
|
12
|
+
# gems installed. Then, create an instance and store it in the `feature_store`
|
13
|
+
# property of your client configuration.
|
14
|
+
#
|
15
|
+
class RedisFeatureStore
|
16
|
+
INIT_KEY = :"$initialized"
|
17
|
+
|
18
|
+
begin
|
19
|
+
require "redis"
|
20
|
+
require "connection_pool"
|
21
|
+
require "moneta"
|
22
|
+
REDIS_ENABLED = true
|
23
|
+
rescue ScriptError, StandardError
|
24
|
+
REDIS_ENABLED = false
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Constructor for a RedisFeatureStore instance.
|
29
|
+
#
|
30
|
+
# @param opts [Hash] the configuration options
|
31
|
+
# @option opts [String] :redis_url URL of the Redis instance (shortcut for omitting redis_opts)
|
32
|
+
# @option opts [Hash] :redis_opts options to pass to the Redis constructor (if you want to specify more than just redis_url)
|
33
|
+
# @option opts [String] :prefix namespace prefix to add to all hash keys used by LaunchDarkly
|
34
|
+
# @option opts [Logger] :logger a `Logger` instance; defaults to `Config.default_logger`
|
35
|
+
# @option opts [Integer] :max_connections size of the Redis connection pool
|
36
|
+
# @option opts [Integer] :expiration expiration time for the in-memory cache, in seconds; 0 for no local caching
|
37
|
+
# @option opts [Integer] :capacity maximum number of feature flags to cache locally
|
38
|
+
# @option opts [Object] :pool custom connection pool, used for testing only
|
39
|
+
#
|
40
|
+
def initialize(opts = {})
|
41
|
+
if !REDIS_ENABLED
|
42
|
+
raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool, moneta")
|
43
|
+
end
|
44
|
+
@redis_opts = opts[:redis_opts] || Hash.new
|
45
|
+
if opts[:redis_url]
|
46
|
+
@redis_opts[:url] = opts[:redis_url]
|
47
|
+
end
|
48
|
+
if !@redis_opts.include?(:url)
|
49
|
+
@redis_opts[:url] = RedisFeatureStore.default_redis_url
|
50
|
+
end
|
51
|
+
max_connections = opts[:max_connections] || 16
|
52
|
+
@pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
|
53
|
+
Redis.new(@redis_opts)
|
54
|
+
end
|
55
|
+
@prefix = opts[:prefix] || RedisFeatureStore.default_prefix
|
56
|
+
@logger = opts[:logger] || Config.default_logger
|
57
|
+
@features_key = @prefix + ':features'
|
58
|
+
|
59
|
+
@expiration_seconds = opts[:expiration] || 15
|
60
|
+
@capacity = opts[:capacity] || 1000
|
61
|
+
# We're using Moneta only to provide expiration behavior for the in-memory cache.
|
62
|
+
# Moneta can also be used as a wrapper for Redis, but it doesn't support the Redis
|
63
|
+
# hash operations that we use.
|
64
|
+
if @expiration_seconds > 0
|
65
|
+
@cache = Moneta.new(:LRUHash, expires: true, threadsafe: true, max_count: @capacity)
|
66
|
+
else
|
67
|
+
@cache = Moneta.new(:Null) # a stub that caches nothing
|
68
|
+
end
|
69
|
+
|
70
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
71
|
+
|
72
|
+
with_connection do |redis|
|
73
|
+
@logger.info("RedisFeatureStore: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} \
|
74
|
+
and prefix: #{@prefix}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
80
|
+
# running at `localhost` with its default port.
|
81
|
+
#
|
82
|
+
def self.default_redis_url
|
83
|
+
'redis://localhost:6379/0'
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Default value for the `prefix` constructor parameter.
|
88
|
+
#
|
89
|
+
def self.default_prefix
|
90
|
+
'launchdarkly'
|
91
|
+
end
|
92
|
+
|
93
|
+
def get(key)
|
94
|
+
f = @cache[key.to_sym]
|
95
|
+
if f.nil?
|
96
|
+
@logger.debug("RedisFeatureStore: no cache hit for #{key}, requesting from Redis")
|
97
|
+
f = with_connection do |redis|
|
98
|
+
begin
|
99
|
+
get_redis(redis,key.to_sym)
|
100
|
+
rescue => e
|
101
|
+
@logger.error("RedisFeatureStore: could not retrieve feature #{key} from Redis, with error: #{e}")
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
if !f.nil?
|
106
|
+
put_cache(key.to_sym, f)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
if f.nil?
|
110
|
+
@logger.warn("RedisFeatureStore: feature #{key} not found")
|
111
|
+
nil
|
112
|
+
elsif f[:deleted]
|
113
|
+
@logger.warn("RedisFeatureStore: feature #{key} was deleted, returning nil")
|
114
|
+
nil
|
115
|
+
else
|
116
|
+
f
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def all
|
121
|
+
fs = {}
|
122
|
+
with_connection do |redis|
|
123
|
+
begin
|
124
|
+
hashfs = redis.hgetall(@features_key)
|
125
|
+
rescue => e
|
126
|
+
@logger.error("RedisFeatureStore: could not retrieve all flags from Redis with error: #{e}; returning none")
|
127
|
+
hashfs = {}
|
128
|
+
end
|
129
|
+
hashfs.each do |k, jsonFeature|
|
130
|
+
f = JSON.parse(jsonFeature, symbolize_names: true)
|
131
|
+
if !f[:deleted]
|
132
|
+
fs[k.to_sym] = f
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
fs
|
137
|
+
end
|
138
|
+
|
139
|
+
def delete(key, version)
|
140
|
+
with_connection do |redis|
|
141
|
+
f = get_redis(redis, key)
|
142
|
+
if f.nil?
|
143
|
+
put_redis_and_cache(redis, key, { deleted: true, version: version })
|
144
|
+
else
|
145
|
+
if f[:version] < version
|
146
|
+
f1 = f.clone
|
147
|
+
f1[:deleted] = true
|
148
|
+
f1[:version] = version
|
149
|
+
put_redis_and_cache(redis, key, f1)
|
150
|
+
else
|
151
|
+
@logger.warn("RedisFeatureStore: attempted to delete flag: #{key} version: #{f[:version]} \
|
152
|
+
with a version that is the same or older: #{version}")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def init(fs)
|
159
|
+
@cache.clear
|
160
|
+
with_connection do |redis|
|
161
|
+
redis.del(@features_key)
|
162
|
+
fs.each { |k, f| put_redis_and_cache(redis, k, f) }
|
163
|
+
end
|
164
|
+
put_cache(INIT_KEY, true)
|
165
|
+
@logger.info("RedisFeatureStore: initialized with #{fs.count} feature flags")
|
166
|
+
end
|
167
|
+
|
168
|
+
def upsert(key, feature)
|
169
|
+
with_connection do |redis|
|
170
|
+
redis.watch(@features_key) do
|
171
|
+
old = get_redis(redis, key)
|
172
|
+
if old.nil? || (old[:version] < feature[:version])
|
173
|
+
put_redis_and_cache(redis, key, feature)
|
174
|
+
end
|
175
|
+
redis.unwatch
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def initialized?
|
181
|
+
if @cache[INIT_KEY].nil?
|
182
|
+
if with_connection { |redis| redis.exists(@features_key) }
|
183
|
+
put_cache(INIT_KEY, true)
|
184
|
+
true
|
185
|
+
else
|
186
|
+
false
|
187
|
+
end
|
188
|
+
else
|
189
|
+
put_cache(INIT_KEY, true) # reset TTL
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def stop
|
194
|
+
if @stopped.make_true
|
195
|
+
@pool.shutdown { |redis| redis.close }
|
196
|
+
@cache.clear
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# exposed for testing
|
201
|
+
def clear_local_cache()
|
202
|
+
@cache.clear
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def with_connection
|
208
|
+
@pool.with { |redis| yield(redis) }
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_redis(redis, key)
|
212
|
+
begin
|
213
|
+
json_feature = redis.hget(@features_key, key)
|
214
|
+
JSON.parse(json_feature, symbolize_names: true) if json_feature
|
215
|
+
rescue => e
|
216
|
+
@logger.error("RedisFeatureStore: could not retrieve feature #{key} from Redis, error: #{e}")
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def put_cache(key, value)
|
222
|
+
@cache.store(key, value, expires: @expiration_seconds)
|
223
|
+
end
|
224
|
+
|
225
|
+
def put_redis_and_cache(redis, key, feature)
|
226
|
+
begin
|
227
|
+
redis.hset(@features_key, key, feature.to_json)
|
228
|
+
rescue => e
|
229
|
+
@logger.error("RedisFeatureStore: could not store #{key} in Redis, error: #{e}")
|
230
|
+
end
|
231
|
+
put_cache(key.to_sym, feature)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -4,6 +4,9 @@ require "faraday/http_cache"
|
|
4
4
|
|
5
5
|
module LaunchDarkly
|
6
6
|
|
7
|
+
class InvalidSDKKeyError < StandardError
|
8
|
+
end
|
9
|
+
|
7
10
|
class Requestor
|
8
11
|
def initialize(sdk_key, config)
|
9
12
|
@sdk_key = sdk_key
|
@@ -39,7 +42,7 @@ module LaunchDarkly
|
|
39
42
|
|
40
43
|
if res.status == 401
|
41
44
|
@config.logger.error("[LDClient] Invalid SDK key")
|
42
|
-
|
45
|
+
raise InvalidSDKKeyError
|
43
46
|
end
|
44
47
|
|
45
48
|
if res.status == 404
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -8,6 +8,7 @@ module LaunchDarkly
|
|
8
8
|
DELETE = :delete
|
9
9
|
INDIRECT_PUT = :'indirect/put'
|
10
10
|
INDIRECT_PATCH = :'indirect/patch'
|
11
|
+
READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
|
11
12
|
|
12
13
|
class StreamProcessor
|
13
14
|
def initialize(sdk_key, config, requestor)
|
@@ -17,6 +18,7 @@ module LaunchDarkly
|
|
17
18
|
@requestor = requestor
|
18
19
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
19
20
|
@started = Concurrent::AtomicBoolean.new(false)
|
21
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
20
22
|
end
|
21
23
|
|
22
24
|
def initialized?
|
@@ -33,13 +35,34 @@ module LaunchDarkly
|
|
33
35
|
'Authorization' => @sdk_key,
|
34
36
|
'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
|
35
37
|
}
|
36
|
-
opts = {:headers => headers, :with_credentials => true, :proxy => @config.proxy}
|
38
|
+
opts = {:headers => headers, :with_credentials => true, :proxy => @config.proxy, :read_timeout => READ_TIMEOUT_SECONDS}
|
37
39
|
@es = Celluloid::EventSource.new(@config.stream_uri + "/flags", opts) do |conn|
|
38
40
|
conn.on(PUT) { |message| process_message(message, PUT) }
|
39
41
|
conn.on(PATCH) { |message| process_message(message, PATCH) }
|
40
42
|
conn.on(DELETE) { |message| process_message(message, DELETE) }
|
41
43
|
conn.on(INDIRECT_PUT) { |message| process_message(message, INDIRECT_PUT) }
|
42
44
|
conn.on(INDIRECT_PATCH) { |message| process_message(message, INDIRECT_PATCH) }
|
45
|
+
conn.on_error { |err|
|
46
|
+
@config.logger.error("[LDClient] Unexpected status code #{err[:status_code]} from streaming connection")
|
47
|
+
if err[:status_code] == 401
|
48
|
+
@config.logger.error("[LDClient] Received 401 error, no further streaming connection will be made since SDK key is invalid")
|
49
|
+
stop
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
if @stopped.make_true
|
57
|
+
@es.close
|
58
|
+
@config.logger.info("[LDClient] Stream connection stopped")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop
|
63
|
+
if @stopped.make_true
|
64
|
+
@es.close
|
65
|
+
@config.logger.info("[LDClient] Stream connection stopped")
|
43
66
|
end
|
44
67
|
end
|
45
68
|
|
data/lib/ldclient-rb/version.rb
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::EventSerializer do
|
4
|
+
subject { LaunchDarkly::EventSerializer }
|
5
|
+
|
6
|
+
let(:base_config) { LaunchDarkly::Config.new }
|
7
|
+
let(:config_with_all_attrs_private) { LaunchDarkly::Config.new({ all_attributes_private: true })}
|
8
|
+
let(:config_with_some_attrs_private) { LaunchDarkly::Config.new({ private_attribute_names: ['firstName', 'bizzle'] })}
|
9
|
+
|
10
|
+
# users to serialize
|
11
|
+
|
12
|
+
let(:user) {
|
13
|
+
{ key: 'abc', firstName: 'Sue', custom: { bizzle: 'def', dizzle: 'ghi' }}
|
14
|
+
}
|
15
|
+
|
16
|
+
let(:user_specifying_own_private_attr) {
|
17
|
+
u = user.clone
|
18
|
+
u[:privateAttributeNames] = [ 'dizzle', 'unused' ]
|
19
|
+
u
|
20
|
+
}
|
21
|
+
|
22
|
+
# expected results from serializing user
|
23
|
+
|
24
|
+
let(:user_with_all_attrs_hidden) {
|
25
|
+
{ key: 'abc', custom: { }, privateAttrs: [ 'bizzle', 'dizzle', 'firstName' ]}
|
26
|
+
}
|
27
|
+
|
28
|
+
let(:user_with_some_attrs_hidden) {
|
29
|
+
{ key: 'abc', custom: { dizzle: 'ghi' }, privateAttrs: [ 'bizzle', 'firstName' ]}
|
30
|
+
}
|
31
|
+
|
32
|
+
let(:user_with_own_specified_attr_hidden) {
|
33
|
+
{ key: 'abc', firstName: 'Sue', custom: { bizzle: 'def' }, privateAttrs: [ 'dizzle' ]}
|
34
|
+
}
|
35
|
+
|
36
|
+
|
37
|
+
def make_event(user)
|
38
|
+
{
|
39
|
+
creationDate: 1000000,
|
40
|
+
key: 'xyz',
|
41
|
+
kind: 'thing',
|
42
|
+
user: user
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_results(js)
|
47
|
+
JSON.parse(js, symbolize_names: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "serialize_events" do
|
51
|
+
it "includes all user attributes by default" do
|
52
|
+
es = LaunchDarkly::EventSerializer.new(base_config)
|
53
|
+
event = make_event(user)
|
54
|
+
j = es.serialize_events([event])
|
55
|
+
expect(parse_results(j)).to eq [event]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "hides all except key if all_attributes_private is true" do
|
59
|
+
es = LaunchDarkly::EventSerializer.new(config_with_all_attrs_private)
|
60
|
+
event = make_event(user)
|
61
|
+
j = es.serialize_events([event])
|
62
|
+
expect(parse_results(j)).to eq [make_event(user_with_all_attrs_hidden)]
|
63
|
+
end
|
64
|
+
|
65
|
+
it "hides some attributes if private_attribute_names is set" do
|
66
|
+
es = LaunchDarkly::EventSerializer.new(config_with_some_attrs_private)
|
67
|
+
event = make_event(user)
|
68
|
+
j = es.serialize_events([event])
|
69
|
+
expect(parse_results(j)).to eq [make_event(user_with_some_attrs_hidden)]
|
70
|
+
end
|
71
|
+
|
72
|
+
it "hides attributes specified in per-user privateAttrs" do
|
73
|
+
es = LaunchDarkly::EventSerializer.new(base_config)
|
74
|
+
event = make_event(user_specifying_own_private_attr)
|
75
|
+
j = es.serialize_events([event])
|
76
|
+
expect(parse_results(j)).to eq [make_event(user_with_own_specified_attr_hidden)]
|
77
|
+
end
|
78
|
+
|
79
|
+
it "looks at both per-user privateAttrs and global config" do
|
80
|
+
es = LaunchDarkly::EventSerializer.new(config_with_some_attrs_private)
|
81
|
+
event = make_event(user_specifying_own_private_attr)
|
82
|
+
j = es.serialize_events([event])
|
83
|
+
expect(parse_results(j)).to eq [make_event(user_with_all_attrs_hidden)]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.shared_examples "feature_store" do |create_store_method|
|
4
|
+
|
5
|
+
let(:feature0) {
|
6
|
+
{
|
7
|
+
key: "test-feature-flag",
|
8
|
+
version: 11,
|
9
|
+
on: true,
|
10
|
+
prerequisites: [],
|
11
|
+
salt: "718ea30a918a4eba8734b57ab1a93227",
|
12
|
+
sel: "fe1244e5378c4f99976c9634e33667c6",
|
13
|
+
targets: [
|
14
|
+
{
|
15
|
+
values: [ "alice" ],
|
16
|
+
variation: 0
|
17
|
+
},
|
18
|
+
{
|
19
|
+
values: [ "bob" ],
|
20
|
+
variation: 1
|
21
|
+
}
|
22
|
+
],
|
23
|
+
rules: [],
|
24
|
+
fallthrough: { variation: 0 },
|
25
|
+
offVariation: 1,
|
26
|
+
variations: [ true, false ],
|
27
|
+
deleted: false
|
28
|
+
}
|
29
|
+
}
|
30
|
+
let(:key0) { feature0[:key].to_sym }
|
31
|
+
|
32
|
+
let!(:store) do
|
33
|
+
s = create_store_method.call()
|
34
|
+
s.init({ key0 => feature0 })
|
35
|
+
s
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_version_plus(f, deltaVersion, attrs = {})
|
39
|
+
f1 = f.clone
|
40
|
+
f1[:version] = f[:version] + deltaVersion
|
41
|
+
f1.update(attrs)
|
42
|
+
f1
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
it "is initialized" do
|
47
|
+
expect(store.initialized?).to eq true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can get existing feature with symbol key" do
|
51
|
+
expect(store.get(key0)).to eq feature0
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can get existing feature with string key" do
|
55
|
+
expect(store.get(key0.to_s)).to eq feature0
|
56
|
+
end
|
57
|
+
|
58
|
+
it "gets nil for nonexisting feature" do
|
59
|
+
expect(store.get('nope')).to be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can get all features" do
|
63
|
+
feature1 = feature0.clone
|
64
|
+
feature1[:key] = "test-feature-flag1"
|
65
|
+
feature1[:version] = 5
|
66
|
+
feature1[:on] = false
|
67
|
+
store.upsert(:"test-feature-flag1", feature1)
|
68
|
+
expect(store.all).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
|
69
|
+
end
|
70
|
+
|
71
|
+
it "can add new feature" do
|
72
|
+
feature1 = feature0.clone
|
73
|
+
feature1[:key] = "test-feature-flag1"
|
74
|
+
feature1[:version] = 5
|
75
|
+
feature1[:on] = false
|
76
|
+
store.upsert(:"test-feature-flag1", feature1)
|
77
|
+
expect(store.get(:"test-feature-flag1")).to eq feature1
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can update feature with newer version" do
|
81
|
+
f1 = new_version_plus(feature0, 1, { on: !feature0[:on] })
|
82
|
+
store.upsert(key0, f1)
|
83
|
+
expect(store.get(key0)).to eq f1
|
84
|
+
end
|
85
|
+
|
86
|
+
it "cannot update feature with same version" do
|
87
|
+
f1 = new_version_plus(feature0, 0, { on: !feature0[:on] })
|
88
|
+
store.upsert(key0, f1)
|
89
|
+
expect(store.get(key0)).to eq feature0
|
90
|
+
end
|
91
|
+
|
92
|
+
it "cannot update feature with older version" do
|
93
|
+
f1 = new_version_plus(feature0, -1, { on: !feature0[:on] })
|
94
|
+
store.upsert(key0, f1)
|
95
|
+
expect(store.get(key0)).to eq feature0
|
96
|
+
end
|
97
|
+
|
98
|
+
it "can delete feature with newer version" do
|
99
|
+
store.delete(key0, feature0[:version] + 1)
|
100
|
+
expect(store.get(key0)).to be_nil
|
101
|
+
end
|
102
|
+
|
103
|
+
it "cannot delete feature with same version" do
|
104
|
+
store.delete(key0, feature0[:version])
|
105
|
+
expect(store.get(key0)).to eq feature0
|
106
|
+
end
|
107
|
+
|
108
|
+
it "cannot delete feature with older version" do
|
109
|
+
store.delete(key0, feature0[:version] - 1)
|
110
|
+
expect(store.get(key0)).to eq feature0
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
{
|
2
|
+
"key":"test-feature-flag1",
|
3
|
+
"version":5,
|
4
|
+
"on":false,
|
5
|
+
"prerequisites":[
|
6
|
+
|
7
|
+
],
|
8
|
+
"salt":"718ea30a918a4eba8734b57ab1a93227",
|
9
|
+
"sel":"fe1244e5378c4f99976c9634e33667c6",
|
10
|
+
"targets":[
|
11
|
+
{
|
12
|
+
"values":[
|
13
|
+
"alice"
|
14
|
+
],
|
15
|
+
"variation":0
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"values":[
|
19
|
+
"bob"
|
20
|
+
],
|
21
|
+
"variation":1
|
22
|
+
}
|
23
|
+
],
|
24
|
+
"rules":[
|
25
|
+
|
26
|
+
],
|
27
|
+
"fallthrough":{
|
28
|
+
"variation":0
|
29
|
+
},
|
30
|
+
"offVariation":1,
|
31
|
+
"variations":[
|
32
|
+
true,
|
33
|
+
false
|
34
|
+
],
|
35
|
+
"deleted":false
|
36
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "feature_store_spec_base"
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
def create_in_memory_store()
|
5
|
+
LaunchDarkly::InMemoryFeatureStore.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe LaunchDarkly::InMemoryFeatureStore do
|
9
|
+
subject { LaunchDarkly::InMemoryFeatureStore }
|
10
|
+
|
11
|
+
include_examples "feature_store", method(:create_in_memory_store)
|
12
|
+
end
|
data/spec/ldclient_spec.rb
CHANGED
@@ -70,4 +70,26 @@ describe LaunchDarkly::LDClient do
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
end
|
73
|
+
|
74
|
+
describe 'with send_events: false' do
|
75
|
+
let(:config) { LaunchDarkly::Config.new({offline: true, send_events: false}) }
|
76
|
+
let(:client) { subject.new("secret", config) }
|
77
|
+
|
78
|
+
let(:queue) { client.instance_variable_get(:@event_processor).instance_variable_get(:@queue) }
|
79
|
+
|
80
|
+
it "does not enqueue a feature event" do
|
81
|
+
client.variation(feature[:key], user, "default")
|
82
|
+
expect(queue.empty?).to be true
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not enqueue a custom event" do
|
86
|
+
client.track("custom_event_name", user, 42)
|
87
|
+
expect(queue.empty?).to be true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "does not enqueue an identify event" do
|
91
|
+
client.identify(user)
|
92
|
+
expect(queue.empty?).to be true
|
93
|
+
end
|
94
|
+
end
|
73
95
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "feature_store_spec_base"
|
2
|
+
require "json"
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
$my_prefix = 'testprefix'
|
8
|
+
$null_log = ::Logger.new($stdout)
|
9
|
+
$null_log.level = ::Logger::FATAL
|
10
|
+
|
11
|
+
|
12
|
+
def create_redis_store()
|
13
|
+
LaunchDarkly::RedisFeatureStore.new(prefix: $my_prefix, logger: $null_log, expiration: 60)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_redis_store_uncached()
|
17
|
+
LaunchDarkly::RedisFeatureStore.new(prefix: $my_prefix, logger: $null_log, expiration: 0)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
describe LaunchDarkly::RedisFeatureStore do
|
22
|
+
subject { LaunchDarkly::RedisFeatureStore }
|
23
|
+
|
24
|
+
let(:feature0_with_higher_version) do
|
25
|
+
f = feature0.clone
|
26
|
+
f[:version] = feature0[:version] + 10
|
27
|
+
f
|
28
|
+
end
|
29
|
+
|
30
|
+
# These tests will all fail if there isn't a Redis instance running on the default port.
|
31
|
+
|
32
|
+
context "real Redis with local cache" do
|
33
|
+
|
34
|
+
include_examples "feature_store", method(:create_redis_store)
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context "real Redis without local cache" do
|
39
|
+
|
40
|
+
include_examples "feature_store", method(:create_redis_store_uncached)
|
41
|
+
|
42
|
+
end
|
43
|
+
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: 2.
|
4
|
+
version: 2.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,48 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.3.5
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.3.5
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: connection_pool
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.1.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.1.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: moneta
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.0.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.0.0
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: json
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -170,14 +212,14 @@ dependencies:
|
|
170
212
|
requirements:
|
171
213
|
- - "~>"
|
172
214
|
- !ruby/object:Gem::Version
|
173
|
-
version: 0.
|
215
|
+
version: 0.11.0
|
174
216
|
type: :runtime
|
175
217
|
prerelease: false
|
176
218
|
version_requirements: !ruby/object:Gem::Requirement
|
177
219
|
requirements:
|
178
220
|
- - "~>"
|
179
221
|
- !ruby/object:Gem::Version
|
180
|
-
version: 0.
|
222
|
+
version: 0.11.0
|
181
223
|
- !ruby/object:Gem::Dependency
|
182
224
|
name: celluloid
|
183
225
|
requirement: !ruby/object:Gem::Requirement
|
@@ -234,6 +276,7 @@ files:
|
|
234
276
|
- ".rubocop.yml"
|
235
277
|
- ".simplecov"
|
236
278
|
- CHANGELOG.md
|
279
|
+
- CODEOWNERS
|
237
280
|
- CONTRIBUTING.md
|
238
281
|
- Gemfile
|
239
282
|
- LICENSE.txt
|
@@ -246,22 +289,29 @@ files:
|
|
246
289
|
- lib/ldclient-rb/cache_store.rb
|
247
290
|
- lib/ldclient-rb/config.rb
|
248
291
|
- lib/ldclient-rb/evaluation.rb
|
292
|
+
- lib/ldclient-rb/event_serializer.rb
|
249
293
|
- lib/ldclient-rb/events.rb
|
250
294
|
- lib/ldclient-rb/feature_store.rb
|
251
295
|
- lib/ldclient-rb/ldclient.rb
|
252
296
|
- lib/ldclient-rb/newrelic.rb
|
253
297
|
- lib/ldclient-rb/polling.rb
|
298
|
+
- lib/ldclient-rb/redis_feature_store.rb
|
254
299
|
- lib/ldclient-rb/requestor.rb
|
255
300
|
- lib/ldclient-rb/stream.rb
|
256
301
|
- lib/ldclient-rb/version.rb
|
257
302
|
- scripts/release.sh
|
258
303
|
- spec/config_spec.rb
|
304
|
+
- spec/event_serializer_spec.rb
|
305
|
+
- spec/feature_store_spec_base.rb
|
259
306
|
- spec/fixtures/feature.json
|
307
|
+
- spec/fixtures/feature1.json
|
260
308
|
- spec/fixtures/numeric_key_user.json
|
261
309
|
- spec/fixtures/sanitized_numeric_key_user.json
|
262
310
|
- spec/fixtures/user.json
|
311
|
+
- spec/in_memory_feature_store_spec.rb
|
263
312
|
- spec/ldclient_spec.rb
|
264
313
|
- spec/newrelic_spec.rb
|
314
|
+
- spec/redis_feature_store_spec.rb
|
265
315
|
- spec/requestor_spec.rb
|
266
316
|
- spec/spec_helper.rb
|
267
317
|
- spec/store_spec.rb
|
@@ -293,12 +343,17 @@ specification_version: 4
|
|
293
343
|
summary: LaunchDarkly SDK for Ruby
|
294
344
|
test_files:
|
295
345
|
- spec/config_spec.rb
|
346
|
+
- spec/event_serializer_spec.rb
|
347
|
+
- spec/feature_store_spec_base.rb
|
296
348
|
- spec/fixtures/feature.json
|
349
|
+
- spec/fixtures/feature1.json
|
297
350
|
- spec/fixtures/numeric_key_user.json
|
298
351
|
- spec/fixtures/sanitized_numeric_key_user.json
|
299
352
|
- spec/fixtures/user.json
|
353
|
+
- spec/in_memory_feature_store_spec.rb
|
300
354
|
- spec/ldclient_spec.rb
|
301
355
|
- spec/newrelic_spec.rb
|
356
|
+
- spec/redis_feature_store_spec.rb
|
302
357
|
- spec/requestor_spec.rb
|
303
358
|
- spec/spec_helper.rb
|
304
359
|
- spec/store_spec.rb
|