ldclient-rb 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,19 +1,16 @@
1
1
  module LaunchDarkly
2
-
3
2
  class LDNewRelic
4
3
  begin
5
- require 'newrelic_rpm'
4
+ require "newrelic_rpm"
6
5
  NR_ENABLED = defined?(::NewRelic::Agent.add_custom_parameters)
7
- rescue Exception
6
+ rescue ScriptError, StandardError
8
7
  NR_ENABLED = false
9
8
  end
10
9
 
11
10
  def self.annotate_transaction(key, value)
12
11
  if NR_ENABLED
13
- ::NewRelic::Agent.add_custom_parameters({key.to_s => value.to_s})
12
+ ::NewRelic::Agent.add_custom_parameters(key.to_s => value.to_s)
14
13
  end
15
14
  end
16
15
  end
17
-
18
-
19
- end
16
+ end
@@ -1,37 +1,36 @@
1
- require 'thread_safe'
1
+ require "thread_safe"
2
2
 
3
3
  module LaunchDarkly
4
-
5
4
  # A thread-safe in-memory store suitable for use
6
5
  # with the Faraday caching HTTP client. Uses the
7
- # Threadsafe gem as the underlying cache.
8
- #
6
+ # Threadsafe gem as the underlying cache.
7
+ #
9
8
  # @see https://github.com/plataformatec/faraday-http-cache
10
9
  # @see https://github.com/ruby-concurrency/thread_safe
11
- #
10
+ #
12
11
  class ThreadSafeMemoryStore
13
- #
12
+ #
14
13
  # Default constructor
15
- #
14
+ #
16
15
  # @return [ThreadSafeMemoryStore] a new store
17
16
  def initialize
18
17
  @cache = ThreadSafe::Cache.new
19
18
  end
20
19
 
21
- #
20
+ #
22
21
  # Read a value from the cache
23
22
  # @param key [Object] the cache key
24
- #
23
+ #
25
24
  # @return [Object] the cache value
26
25
  def read(key)
27
26
  @cache[key]
28
27
  end
29
28
 
30
- #
29
+ #
31
30
  # Store a value in the cache
32
31
  # @param key [Object] the cache key
33
32
  # @param value [Object] the value to associate with the key
34
- #
33
+ #
35
34
  # @return [Object] the value
36
35
  def write(key, value)
37
36
  @cache[key] = value
@@ -1,67 +1,66 @@
1
- require 'concurrent/atomics'
2
- require 'json'
3
- require 'ld-em-eventsource'
1
+ require "concurrent/atomics"
2
+ require "json"
3
+ require "ld-em-eventsource"
4
4
 
5
5
  module LaunchDarkly
6
-
7
6
  PUT = "put"
8
7
  PATCH = "patch"
9
8
  DELETE = "delete"
10
9
 
11
10
  class InMemoryFeatureStore
12
- def initialize()
11
+ def initialize
13
12
  @features = Hash.new
14
13
  @lock = Concurrent::ReadWriteLock.new
15
14
  @initialized = Concurrent::AtomicBoolean.new(false)
16
15
  end
17
16
 
18
17
  def get(key)
19
- @lock.with_read_lock {
18
+ @lock.with_read_lock do
20
19
  f = @features[key.to_sym]
21
20
  (f.nil? || f[:deleted]) ? nil : f
22
- }
21
+ end
23
22
  end
24
23
 
25
- def all()
26
- @lock.with_read_lock {
27
- @features.select {|k,f| not f[:deleted]}
28
- }
24
+ def all
25
+ @lock.with_read_lock do
26
+ @features.select { |_k, f| not f[:deleted] }
27
+ end
29
28
  end
30
29
 
31
30
  def delete(key, version)
32
- @lock.with_write_lock {
31
+ @lock.with_write_lock do
33
32
  old = @features[key.to_sym]
34
33
 
35
- if old != nil and old[:version] < version
34
+ if !old.nil? && old[:version] < version
36
35
  old[:deleted] = true
37
36
  old[:version] = version
38
37
  @features[key.to_sym] = old
39
38
  elsif old.nil?
40
- @features[key.to_sym] = {:deleted => true, :version => version}
39
+ @features[key.to_sym] = { deleted: true, version: version }
41
40
  end
42
- }
41
+ end
43
42
  end
44
43
 
45
44
  def init(fs)
46
- @lock.with_write_lock {
45
+ @lock.with_write_lock do
47
46
  @features.replace(fs)
48
47
  @initialized.make_true
49
- }
48
+ end
50
49
  end
51
50
 
52
51
  def upsert(key, feature)
53
- @lock.with_write_lock {
52
+ @lock.with_write_lock do
54
53
  old = @features[key.to_sym]
55
54
 
56
- if old.nil? or old[:version] < feature[:version]
55
+ if old.nil? || old[:version] < feature[:version]
57
56
  @features[key.to_sym] = feature
58
57
  end
59
- }
58
+ end
60
59
  end
61
60
 
62
- def initialized?()
61
+ def initialized?
63
62
  @initialized.value
64
- end
63
+ end
65
64
  end
66
65
 
67
66
  class StreamProcessor
@@ -73,11 +72,11 @@ module LaunchDarkly
73
72
  @started = Concurrent::AtomicBoolean.new(false)
74
73
  end
75
74
 
76
- def initialized?()
75
+ def initialized?
77
76
  @store.initialized?
78
77
  end
79
78
 
80
- def started?()
79
+ def started?
81
80
  @started.value
82
81
  end
83
82
 
@@ -88,9 +87,9 @@ module LaunchDarkly
88
87
  @store.get(key)
89
88
  end
90
89
 
91
- def start_reactor()
90
+ def start_reactor
92
91
  if defined?(Thin)
93
- @config.logger.debug("Running in a Thin environment-- not starting EventMachine")
92
+ @config.logger.debug("Running in a Thin environment-- not starting EventMachine")
94
93
  elsif EM.reactor_running?
95
94
  @config.logger.debug("EventMachine already running")
96
95
  else
@@ -101,66 +100,67 @@ module LaunchDarkly
101
100
  EM.reactor_running?
102
101
  end
103
102
 
104
- def start()
103
+ def start
105
104
  # Try to start the reactor. If it's not started, we shouldn't start
106
105
  # the stream processor
107
- if not start_reactor
108
- return
109
- end
106
+ return if not start_reactor
110
107
 
111
108
  # If someone else booted the stream processor connection, just return
112
- if not @started.make_true
113
- return
114
- end
109
+ return unless @started.make_true
115
110
 
116
111
  # If we're the first and only thread to set started, boot
117
112
  # the stream processor connection
118
113
  EM.defer do
119
- source = EM::EventSource.new(@config.stream_uri + "/features",
120
- {},
121
- {'Accept' => 'text/event-stream',
122
- 'Authorization' => 'api_key ' + @api_key,
123
- 'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION})
124
- source.on PUT do |message|
125
- features = JSON.parse(message, :symbolize_names => true)
126
- @store.init(features)
127
- set_connected
128
- end
129
- source.on PATCH do |message|
130
- json = JSON.parse(message, :symbolize_names => true)
131
- @store.upsert(json[:path][1..-1], json[:data])
132
- set_connected
133
- end
134
- source.on DELETE do |message|
135
- json = JSON.parse(message, :symbolize_names => true)
136
- @store.delete(json[:path][1..-1], json[:version])
137
- set_connected
138
- end
139
- source.error do |error|
140
- @config.logger.error("[LDClient] Error subscribing to stream API: #{error}")
141
- set_disconnected
142
- end
143
- source.inactivity_timeout = 0
144
- source.start
114
+ boot_event_manager
115
+ end
116
+ end
117
+
118
+ def boot_event_manager
119
+ source = EM::EventSource.new(@config.stream_uri + "/features",
120
+ {},
121
+ "Accept" => "text/event-stream",
122
+ "Authorization" => "api_key " + @api_key,
123
+ "User-Agent" => "RubyClient/" + LaunchDarkly::VERSION)
124
+ source.on(PUT) { |message| process_message(message, PUT) }
125
+ source.on(PATCH) { |message| process_message(message, PATCH) }
126
+ source.on(DELETE) { |message| process_message(message, DELETE) }
127
+ source.error do |error|
128
+ @config.logger.info("[LDClient] Error subscribing to stream API: #{error}")
129
+ set_disconnected
145
130
  end
131
+ source.inactivity_timeout = 0
132
+ source.start
133
+ source
146
134
  end
147
135
 
148
- def set_disconnected()
136
+ def process_message(message, method)
137
+ message = JSON.parse(message, symbolize_names: true)
138
+ if method == PUT
139
+ @store.init(message)
140
+ elsif method == PATCH
141
+ @store.upsert(message[:path][1..-1], message[:data])
142
+ elsif method == DELETE
143
+ @store.delete(message[:path][1..-1], message[:version])
144
+ else
145
+ @config.logger.error("[LDClient] Unknown message received: #{method}")
146
+ end
147
+ set_connected
148
+ end
149
+
150
+ def set_disconnected
149
151
  @disconnected.set(Time.now)
150
152
  end
151
153
 
152
- def set_connected()
154
+ def set_connected
153
155
  @disconnected.set(nil)
154
156
  end
155
157
 
156
- def should_fallback_update()
158
+ def should_fallback_update
157
159
  disc = @disconnected.get
158
- disc != nil and disc < (Time.now - 120)
160
+ !disc.nil? && disc < (Time.now - 120)
159
161
  end
160
162
 
161
163
  # TODO mark private methods
162
- private :set_connected, :set_disconnected, :start_reactor
163
-
164
+ private :boot_event_manager, :process_message, :set_connected, :set_disconnected, :start_reactor
164
165
  end
165
-
166
- end
166
+ end
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::Config do
4
+ subject { LaunchDarkly::Config }
5
+ describe ".initialize" do
6
+ it "can be initialized with default settings" do
7
+ expect(subject).to receive(:default_capacity).and_return 1234
8
+ expect(subject.new.capacity).to eq 1234
9
+ end
10
+ it "accepts custom arguments" do
11
+ expect(subject).to_not receive(:default_capacity)
12
+ expect(subject.new(capacity: 50).capacity).to eq 50
13
+ end
14
+ it "will chomp base_url and stream_uri" do
15
+ uri = "https://test.launchdarkly.com"
16
+ config = subject.new(base_uri: uri + "/")
17
+ expect(config.base_uri).to eq uri
18
+ end
19
+ end
20
+ describe "@base_uri" do
21
+ it "can be read" do
22
+ expect(subject.new.base_uri).to eq subject.default_base_uri
23
+ end
24
+ end
25
+ describe ".default_store" do
26
+ it "uses Rails cache if it is available" do
27
+ rails = instance_double("Rails", cache: :cache)
28
+ stub_const("Rails", rails)
29
+ expect(subject.default_store).to eq :cache
30
+ end
31
+ it "uses memory store if Rails is not available" do
32
+ expect(subject.default_store).to be_an_instance_of LaunchDarkly::ThreadSafeMemoryStore
33
+ end
34
+ end
35
+ describe ".default_logger" do
36
+ it "uses Rails logger if it is available" do
37
+ rails = instance_double("Rails", logger: :logger)
38
+ stub_const("Rails", rails)
39
+ expect(subject.default_logger).to eq :logger
40
+ end
41
+ it "Uses logger if Rails is not available" do
42
+ expect(subject.default_logger).to be_an_instance_of Logger
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,67 @@
1
+ {
2
+ "name":"New recommendations engine",
3
+ "key":"engine.enable",
4
+ "kind":"flag",
5
+ "salt":"ZW5naW5lLmVuYWJsZQ==",
6
+ "on":true,
7
+ "variations":[
8
+ {
9
+ "value":true,
10
+ "weight":31,
11
+ "targets":[
12
+ {
13
+ "attribute":"groups",
14
+ "op":"in",
15
+ "values":[
16
+ "Microsoft"
17
+ ]
18
+ }
19
+ ],
20
+ "userTarget":{
21
+ "attribute":"key",
22
+ "op":"in",
23
+ "values":[
24
+ "foo@bar.com",
25
+ "Abbey.Orkwis@example.com",
26
+ "Abbie.Stolte@example.com",
27
+ "44d2781c-5985-4d89-b07a-0dffbd24126f",
28
+ "0ffe4f0c-7aa9-4621-a87c-abe1c051abd8",
29
+ "f52d99be-6a40-4cdd-a7b4-0548834fcbe5",
30
+ "Vernetta.Belden@example.com",
31
+ "c9d591bd-ea1f-465f-86d2-239ea41d0f0f",
32
+ "870745092"
33
+ ]
34
+ }
35
+ },
36
+ {
37
+ "value":false,
38
+ "weight":69,
39
+ "targets":[
40
+ {
41
+ "attribute":"key",
42
+ "op":"in",
43
+ "values":[
44
+ "Alida.Caples@example.com"
45
+ ]
46
+ },
47
+ {
48
+ "attribute":"groups",
49
+ "op":"in",
50
+ "values":[
51
+
52
+ ]
53
+ }
54
+ ],
55
+ "userTarget":{
56
+ "attribute":"key",
57
+ "op":"in",
58
+ "values":[
59
+ "Alida.Caples@example.com"
60
+ ]
61
+ }
62
+ }
63
+ ],
64
+ "ttl":0,
65
+ "commitDate":"2015-05-10T06:06:45.381Z",
66
+ "creationDate":"2014-09-02T20:39:18.61Z"
67
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "key":"user@test.com",
3
+ "custom":{
4
+ "groups":[
5
+ "microsoft",
6
+ "google"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,226 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::LDClient do
4
+ subject { LaunchDarkly::LDClient }
5
+ let(:client) do
6
+ expect_any_instance_of(LaunchDarkly::LDClient).to receive :create_worker
7
+ subject.new("api_key")
8
+ end
9
+ let(:feature) do
10
+ data = File.read(File.join("spec", "fixtures", "feature.json"))
11
+ JSON.parse(data, symbolize_names: true)
12
+ end
13
+ let(:user) do
14
+ data = File.read(File.join("spec", "fixtures", "user.json"))
15
+ JSON.parse(data, symbolize_names: true)
16
+ end
17
+
18
+ describe '#flush' do
19
+ it "will flush and post all events" do
20
+ client.instance_variable_get(:@queue).push "asdf"
21
+ client.instance_variable_get(:@queue).push "asdf"
22
+ expect(client).to receive(:post_flushed_events)
23
+ client.flush
24
+ expect(client.instance_variable_get(:@queue).length).to eq 0
25
+ end
26
+ it "will not do anything if there are no events" do
27
+ expect(client).to_not receive(:post_flushed_events)
28
+ expect(client.instance_variable_get(:@config).logger).to_not receive :error
29
+ client.flush
30
+ end
31
+ end
32
+
33
+ describe '#post_flushed_events' do
34
+ let(:events) { ["event"] }
35
+ it "will flush and post all events" do
36
+ result = double("result", status: 200)
37
+ expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
38
+ expect(client.instance_variable_get(:@config).logger).to_not receive :error
39
+ client.send(:post_flushed_events, events)
40
+ expect(client.instance_variable_get(:@queue).length).to eq 0
41
+ end
42
+ it "will allow any 2XX response" do
43
+ result = double("result", status: 202)
44
+ expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
45
+ expect(client.instance_variable_get(:@config).logger).to_not receive :error
46
+ client.send(:post_flushed_events, events)
47
+ end
48
+ it "will work with unexpected post results" do
49
+ result = double("result", status: 418)
50
+ expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
51
+ expect(client.instance_variable_get(:@config).logger).to receive :error
52
+ client.send(:post_flushed_events, events)
53
+ expect(client.instance_variable_get(:@queue).length).to eq 0
54
+ end
55
+ end
56
+
57
+ describe '#toggle?' do
58
+ it "will not fail" do
59
+ expect(client.instance_variable_get(:@config)).to receive(:stream?).and_raise RuntimeError
60
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
61
+ result = client.toggle?(feature[:key], user, "default")
62
+ expect(result).to eq "default"
63
+ end
64
+ it "requires user" do
65
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
66
+ result = client.toggle?(feature[:key], nil, "default")
67
+ expect(result).to eq "default"
68
+ end
69
+ it "returns value from streamed flag if available" do
70
+ expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(true).twice
71
+ expect(client.instance_variable_get(:@stream_processor)).to receive(:started?).and_return true
72
+ expect(client.instance_variable_get(:@stream_processor)).to receive(:initialized?).and_return true
73
+ expect(client).to receive(:add_event)
74
+ expect(client).to receive(:get_streamed_flag).and_return feature
75
+ result = client.toggle?(feature[:key], user, "default")
76
+ expect(result).to eq false
77
+ end
78
+ it "returns value from normal request if streamed flag is not available" do
79
+ expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(false).twice
80
+ expect(client).to receive(:add_event)
81
+ expect(client).to receive(:get_flag_int).and_return feature
82
+ result = client.toggle?(feature[:key], user, "default")
83
+ expect(result).to eq false
84
+ end
85
+ end
86
+
87
+ describe '#get_streamed_flag' do
88
+ it "will not check the polled flag normally" do
89
+ expect(client).to receive(:get_flag_stream).and_return true
90
+ expect(client).to_not receive(:get_flag_int)
91
+ expect(client.send(:get_streamed_flag, "key")).to eq true
92
+ end
93
+ context "debug stream" do
94
+ it "will log an error if the streamed and polled flag do not match" do
95
+ expect(client.instance_variable_get(:@config)).to receive(:debug_stream?).and_return true
96
+ expect(client).to receive(:get_flag_stream).and_return true
97
+ expect(client).to receive(:get_flag_int).and_return false
98
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
99
+ expect(client.send(:get_streamed_flag, "key")).to eq true
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#get_features' do
105
+ it "will parse and return the features list" do
106
+ result = double("Faraday::Response", status: 200, body: '{"items": ["asdf"]}')
107
+ expect(client).to receive(:make_request).with("/api/features").and_return(result)
108
+ data = client.send(:get_features)
109
+ expect(data).to eq ["asdf"]
110
+ end
111
+ it "will log errors" do
112
+ result = double("Faraday::Response", status: 418)
113
+ expect(client).to receive(:make_request).with("/api/features").and_return(result)
114
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
115
+ client.send(:get_features)
116
+ end
117
+ end
118
+
119
+ describe '#get_flag_int' do
120
+ it "will return the parsed flag" do
121
+ result = double("Faraday::Response", status: 200, body: '{"asdf":"qwer"}')
122
+ expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
123
+ data = client.send(:get_flag_int, "key")
124
+ expect(data).to eq(asdf: "qwer")
125
+ end
126
+ it "will accept 401 statuses" do
127
+ result = double("Faraday::Response", status: 401)
128
+ expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
129
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
130
+ data = client.send(:get_flag_int, "key")
131
+ expect(data).to be_nil
132
+ end
133
+ it "will accept 404 statuses" do
134
+ result = double("Faraday::Response", status: 404)
135
+ expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
136
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
137
+ data = client.send(:get_flag_int, "key")
138
+ expect(data).to be_nil
139
+ end
140
+ it "will accept non-standard statuses" do
141
+ result = double("Faraday::Response", status: 418)
142
+ expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
143
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
144
+ data = client.send(:get_flag_int, "key")
145
+ expect(data).to be_nil
146
+ end
147
+ end
148
+
149
+ describe '#make_request' do
150
+ it "will make a proper request" do
151
+ expect(client.instance_variable_get :@client).to receive(:get)
152
+ client.send(:make_request, "/asdf")
153
+ end
154
+ end
155
+
156
+ describe '#param_for_user' do
157
+ it "will return a consistent hash of a user key, feature key, and feature salt" do
158
+ param = client.send(:param_for_user, feature, user)
159
+ expect(param).to be_between(0.0, 1.0).inclusive
160
+ end
161
+ end
162
+
163
+ describe '#evaluate' do
164
+ it "will return nil if there is no feature" do
165
+ expect(client.send(:evaluate, nil, user)).to eq nil
166
+ end
167
+ it "will return nil unless the feature is on" do
168
+ feature[:on] = false
169
+ expect(client.send(:evaluate, feature, user)).to eq nil
170
+ end
171
+ it "will return value if it matches the user" do
172
+ user = { key: "Alida.Caples@example.com" }
173
+ expect(client.send(:evaluate, feature, user)).to eq false
174
+ user = { key: "foo@bar.com" }
175
+ expect(client.send(:evaluate, feature, user)).to eq true
176
+ end
177
+ it "will return value if the target matches" do
178
+ user = { key: "asdf@asdf.com", custom: { groups: "Microsoft" } }
179
+ expect(client.send(:evaluate, feature, user)).to eq true
180
+ end
181
+ it "will return value if the weight matches" do
182
+ expect(client).to receive(:param_for_user).and_return 0.1
183
+ expect(client.send(:evaluate, feature, user)).to eq true
184
+ expect(client).to receive(:param_for_user).and_return 0.9
185
+ expect(client.send(:evaluate, feature, user)).to eq false
186
+ end
187
+ end
188
+
189
+ describe '#log_timings' do
190
+ let(:block) { lambda { "result" } }
191
+ let(:label) { "label" }
192
+ it "will not measure if not configured to do so" do
193
+ expect(Benchmark).to_not receive(:measure)
194
+ client.send(:log_timings, label, &block)
195
+ end
196
+ context "logging enabled" do
197
+ before do
198
+ expect(client.instance_variable_get(:@config)).to receive(:log_timings?).and_return true
199
+ expect(client.instance_variable_get(:@config).logger).to receive(:debug?).and_return true
200
+ end
201
+ it "will benchmark timings and return result" do
202
+ expect(Benchmark).to receive(:measure).and_call_original
203
+ expect(client.instance_variable_get(:@config).logger).to receive(:debug)
204
+ result = client.send(:log_timings, label, &block)
205
+ expect(result).to eq "result"
206
+ end
207
+ it "will raise exceptions if the block has them" do
208
+ block = lambda { raise RuntimeError }
209
+ expect(Benchmark).to receive(:measure).and_call_original
210
+ expect(client.instance_variable_get(:@config).logger).to receive(:debug)
211
+ expect { client.send(:log_timings, label, &block) }.to raise_error RuntimeError
212
+ end
213
+ end
214
+ end
215
+
216
+ describe '#log_exception' do
217
+ it "log error data" do
218
+ expect(client.instance_variable_get(:@config).logger).to receive(:error)
219
+ begin
220
+ raise StandardError.new 'asdf'
221
+ rescue StandardError => exn
222
+ client.send(:log_exception, 'caller', exn)
223
+ end
224
+ end
225
+ end
226
+ end