launchdarkly-server-sdk 5.6.2 → 5.7.0

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: 0f95b72da90326fccd6fb823407b61ca1c686e53
4
- data.tar.gz: 661768aaa51261feba0018cbc0f3ba793580c3e6
3
+ metadata.gz: 78fa0fd5890ae19f95b19c915694bca33187ef23
4
+ data.tar.gz: 5dae22ebbc4a8c1d2e6deb3e206ff4d27937da02
5
5
  SHA512:
6
- metadata.gz: a409099d6bf0e5fb75b8d64ad05e52983bafb22f645620c8adb7403a0435a0ba9bd14068868b2ef1419f291431d6d517dad74284289f166e7ca21ca7156e0462
7
- data.tar.gz: 72c021d8519a455da47ad3c5d8eacbbb79ef80ee068be1b37a5f200abdf8248e7741638de1f8811cbde03638635bc6a9029d90f90f2b6474b5d5e2fe8aa82b1e
6
+ metadata.gz: 888c327731d50e3d4869b2c6fe72748a9c9bb7f2c80123194eb2d32f1b365a613dbf7bec78a38fcf4f35b16e2ec634725fde25681bad0225dbe9fee41ce674f8
7
+ data.tar.gz: b8fcaa392b4940838daa060d66eea7dc2413d458875de3bb36905e70b194b9e34f6707d1d014c6e297ac81d396123411060c18761fe1a05c6287419cd902ca27
@@ -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
+ ## [5.6.2] - 2020-01-15
6
+ ### Fixed:
7
+ - The SDK now specifies a uniquely identifiable request header when sending events to LaunchDarkly to ensure that events are only processed once, even if the SDK sends them two times due to a failed initial attempt.
8
+
5
9
  ## [5.6.1] - 2020-01-06
6
10
  ### Fixed:
7
11
  - In rare circumstances (depending on the exact data in the flag configuration, the flag's salt value, and the user properties), a percentage rollout could fail and return a default value, logging the error "Data inconsistency in feature flag ... variation/rollout object with no variation or rollout". This would happen if the user's hashed value fell exactly at the end of the last "bucket" (the last variation defined in the rollout). This has been fixed so that the user will get the last variation.
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- launchdarkly-server-sdk (5.6.2)
4
+ launchdarkly-server-sdk (5.7.0)
5
5
  concurrent-ruby (~> 1.0)
6
6
  json (>= 1.8, < 3)
7
- ld-eventsource (= 1.0.1)
7
+ ld-eventsource (= 1.0.2)
8
8
  semantic (~> 1.6)
9
9
 
10
10
  GEM
@@ -23,7 +23,7 @@ GEM
23
23
  aws-sigv4 (1.0.3)
24
24
  codeclimate-test-reporter (0.6.0)
25
25
  simplecov (>= 0.7.1, < 1.0.0)
26
- concurrent-ruby (1.1.5)
26
+ concurrent-ruby (1.1.6)
27
27
  connection_pool (2.2.1)
28
28
  diff-lcs (1.3)
29
29
  diplomat (2.0.2)
@@ -40,7 +40,7 @@ GEM
40
40
  jmespath (1.4.0)
41
41
  json (1.8.6)
42
42
  json (1.8.6-java)
43
- ld-eventsource (1.0.1)
43
+ ld-eventsource (1.0.2)
44
44
  concurrent-ruby (~> 1.0)
45
45
  http_tools (~> 0.4.5)
46
46
  socketry (~> 0.5.1)
@@ -49,7 +49,6 @@ GEM
49
49
  rb-inotify (~> 0.9, >= 0.9.7)
50
50
  ruby_dep (~> 1.2)
51
51
  multipart-post (2.0.0)
52
- rake (10.5.0)
53
52
  rb-fsevent (0.10.3)
54
53
  rb-inotify (0.9.10)
55
54
  ffi (>= 0.5.0, < 2)
@@ -92,7 +91,6 @@ DEPENDENCIES
92
91
  diplomat (>= 2.0.2)
93
92
  launchdarkly-server-sdk!
94
93
  listen (~> 3.0)
95
- rake (~> 10.0)
96
94
  redis (~> 3.3.5)
97
95
  rspec (~> 3.2)
98
96
  rspec_junit_formatter (~> 0.3.0)
@@ -28,7 +28,6 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "diplomat", ">= 2.0.2"
29
29
  spec.add_development_dependency "redis", "~> 3.3.5"
30
30
  spec.add_development_dependency "connection_pool", ">= 2.1.2"
31
- spec.add_development_dependency "rake", "~> 10.0"
32
31
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
33
32
  spec.add_development_dependency "timecop", "~> 0.9.1"
34
33
  spec.add_development_dependency "listen", "~> 3.0" # see file_data_source.rb
@@ -36,5 +35,5 @@ Gem::Specification.new do |spec|
36
35
  spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
37
36
  spec.add_runtime_dependency "semantic", "~> 1.6"
38
37
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
39
- spec.add_runtime_dependency "ld-eventsource", "1.0.1"
38
+ spec.add_runtime_dependency "ld-eventsource", "1.0.2"
40
39
  end
@@ -37,6 +37,10 @@ module LaunchDarkly
37
37
  # @option opts [Object] :data_source See {#data_source}.
38
38
  # @option opts [Object] :update_processor Obsolete synonym for `data_source`.
39
39
  # @option opts [Object] :update_processor_factory Obsolete synonym for `data_source`.
40
+ # @option opts [Boolean] :diagnostic_opt_out (false) See {#diagnostic_opt_out?}.
41
+ # @option opts [Float] :diagnostic_recording_interval (900) See {#diagnostic_recording_interval}.
42
+ # @option opts [String] :wrapper_name See {#wrapper_name}.
43
+ # @option opts [String] :wrapper_version See {#wrapper_version}.
40
44
  #
41
45
  def initialize(opts = {})
42
46
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
@@ -62,6 +66,11 @@ module LaunchDarkly
62
66
  @data_source = opts[:data_source] || opts[:update_processor] || opts[:update_processor_factory]
63
67
  @update_processor = opts[:update_processor]
64
68
  @update_processor_factory = opts[:update_processor_factory]
69
+ @diagnostic_opt_out = opts.has_key?(:diagnostic_opt_out) && opts[:diagnostic_opt_out]
70
+ @diagnostic_recording_interval = opts.has_key?(:diagnostic_recording_interval) && opts[:diagnostic_recording_interval] > Config.minimum_diagnostic_recording_interval ?
71
+ opts[:diagnostic_recording_interval] : Config.default_diagnostic_recording_interval
72
+ @wrapper_name = opts[:wrapper_name]
73
+ @wrapper_version = opts[:wrapper_version]
65
74
  end
66
75
 
67
76
  #
@@ -257,6 +266,45 @@ module LaunchDarkly
257
266
  # @deprecated This is replaced by {#data_source}.
258
267
  attr_reader :update_processor_factory
259
268
 
269
+ #
270
+ # Set to true to opt out of sending diagnostics data.
271
+ #
272
+ # Unless `diagnostic_opt_out` is set to true, the client will send some diagnostics data to the LaunchDarkly servers
273
+ # in order to assist in the development of future SDK improvements. These diagnostics consist of an initial payload
274
+ # containing some details of the SDK in use, the SDK's configuration, and the platform the SDK is being run on, as
275
+ # well as periodic information on irregular occurrences such as dropped events.
276
+ # @return [Boolean]
277
+ #
278
+ def diagnostic_opt_out?
279
+ @diagnostic_opt_out
280
+ end
281
+
282
+ #
283
+ # The interval at which periodic diagnostic data is sent, in seconds.
284
+ #
285
+ # The default is 900 (every 15 minutes) and the minimum value is 60 (every minute).
286
+ # @return [Float]
287
+ #
288
+ attr_reader :diagnostic_recording_interval
289
+
290
+ #
291
+ # For use by wrapper libraries to set an identifying name for the wrapper being used.
292
+ #
293
+ # This will be sent in User-Agent headers during requests to the LaunchDarkly servers to allow recording
294
+ # metrics on the usage of these wrapper libraries.
295
+ # @return [String]
296
+ #
297
+ attr_reader :wrapper_name
298
+
299
+ #
300
+ # For use by wrapper libraries to report the version of the library in use.
301
+ #
302
+ # If `wrapper_name` is not set, this field will be ignored. Otherwise the version string will be included in
303
+ # the User-Agent headers along with the `wrapper_name` during requests to the LaunchDarkly servers.
304
+ # @return [String]
305
+ #
306
+ attr_reader :wrapper_version
307
+
260
308
  #
261
309
  # The default LaunchDarkly client configuration. This configuration sets
262
310
  # reasonable defaults for most users.
@@ -407,5 +455,21 @@ module LaunchDarkly
407
455
  def self.default_user_keys_flush_interval
408
456
  300
409
457
  end
458
+
459
+ #
460
+ # The default value for {#diagnostic_recording_interval}.
461
+ # @return [Float] 900
462
+ #
463
+ def self.default_diagnostic_recording_interval
464
+ 900
465
+ end
466
+
467
+ #
468
+ # The minimum value for {#diagnostic_recording_interval}.
469
+ # @return [Float] 60
470
+ #
471
+ def self.minimum_diagnostic_recording_interval
472
+ 60
473
+ end
410
474
  end
411
475
  end
@@ -1,7 +1,10 @@
1
+ require "ldclient-rb/impl/diagnostic_events"
2
+ require "ldclient-rb/impl/event_sender"
3
+ require "ldclient-rb/impl/util"
4
+
1
5
  require "concurrent"
2
6
  require "concurrent/atomics"
3
7
  require "concurrent/executors"
4
- require "securerandom"
5
8
  require "thread"
6
9
  require "time"
7
10
 
@@ -24,12 +27,10 @@ require "time"
24
27
 
25
28
  module LaunchDarkly
26
29
  MAX_FLUSH_WORKERS = 5
27
- CURRENT_SCHEMA_VERSION = 3
28
30
  USER_ATTRS_TO_STRINGIFY_FOR_EVENTS = [ :key, :secondary, :ip, :country, :email, :firstName, :lastName,
29
31
  :avatar, :name ]
30
32
 
31
33
  private_constant :MAX_FLUSH_WORKERS
32
- private_constant :CURRENT_SCHEMA_VERSION
33
34
  private_constant :USER_ATTRS_TO_STRINGIFY_FOR_EVENTS
34
35
 
35
36
  # @private
@@ -60,6 +61,10 @@ module LaunchDarkly
60
61
  class FlushUsersMessage
61
62
  end
62
63
 
64
+ # @private
65
+ class DiagnosticEventMessage
66
+ end
67
+
63
68
  # @private
64
69
  class SynchronousMessage
65
70
  def initialize
@@ -85,9 +90,9 @@ module LaunchDarkly
85
90
 
86
91
  # @private
87
92
  class EventProcessor
88
- def initialize(sdk_key, config, client = nil)
93
+ def initialize(sdk_key, config, client = nil, diagnostic_accumulator = nil, test_properties = nil)
89
94
  @logger = config.logger
90
- @inbox = SizedQueue.new(config.capacity)
95
+ @inbox = SizedQueue.new(config.capacity < 100 ? 100 : config.capacity)
91
96
  @flush_task = Concurrent::TimerTask.new(execution_interval: config.flush_interval) do
92
97
  post_to_inbox(FlushMessage.new)
93
98
  end
@@ -96,14 +101,29 @@ module LaunchDarkly
96
101
  post_to_inbox(FlushUsersMessage.new)
97
102
  end
98
103
  @users_flush_task.execute
104
+ if !diagnostic_accumulator.nil?
105
+ interval = test_properties && test_properties.has_key?(:diagnostic_recording_interval) ?
106
+ test_properties[:diagnostic_recording_interval] :
107
+ config.diagnostic_recording_interval
108
+ @diagnostic_event_task = Concurrent::TimerTask.new(execution_interval: interval) do
109
+ post_to_inbox(DiagnosticEventMessage.new)
110
+ end
111
+ @diagnostic_event_task.execute
112
+ else
113
+ @diagnostic_event_task = nil
114
+ end
99
115
  @stopped = Concurrent::AtomicBoolean.new(false)
100
116
  @inbox_full = Concurrent::AtomicBoolean.new(false)
101
117
 
102
- EventDispatcher.new(@inbox, sdk_key, config, client)
118
+ event_sender = test_properties && test_properties.has_key?(:event_sender) ?
119
+ test_properties[:event_sender] :
120
+ Impl::EventSender.new(sdk_key, config, client ? client : Util.new_http_client(config.events_uri, config))
121
+
122
+ EventDispatcher.new(@inbox, sdk_key, config, diagnostic_accumulator, event_sender)
103
123
  end
104
124
 
105
125
  def add_event(event)
106
- event[:creationDate] = (Time.now.to_f * 1000).to_i
126
+ event[:creationDate] = Impl::Util.current_time_millis
107
127
  post_to_inbox(EventMessage.new(event))
108
128
  end
109
129
 
@@ -117,6 +137,7 @@ module LaunchDarkly
117
137
  if @stopped.make_true
118
138
  @flush_task.shutdown
119
139
  @users_flush_task.shutdown
140
+ @diagnostic_event_task.shutdown if !@diagnostic_event_task.nil?
120
141
  # Note that here we are not calling post_to_inbox, because we *do* want to wait if the inbox
121
142
  # is full; an orderly shutdown can't happen unless these messages are received.
122
143
  @inbox << FlushMessage.new
@@ -152,34 +173,36 @@ module LaunchDarkly
152
173
 
153
174
  # @private
154
175
  class EventDispatcher
155
- def initialize(inbox, sdk_key, config, client)
176
+ def initialize(inbox, sdk_key, config, diagnostic_accumulator, event_sender)
156
177
  @sdk_key = sdk_key
157
178
  @config = config
158
-
159
- if client
160
- @client = client
161
- else
162
- @client = Util.new_http_client(@config.events_uri, @config)
163
- end
179
+ @diagnostic_accumulator = config.diagnostic_opt_out? ? nil : diagnostic_accumulator
180
+ @event_sender = event_sender
164
181
 
165
182
  @user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
166
183
  @formatter = EventOutputFormatter.new(config)
167
184
  @disabled = Concurrent::AtomicBoolean.new(false)
168
185
  @last_known_past_time = Concurrent::AtomicReference.new(0)
169
-
186
+ @deduplicated_users = 0
187
+ @events_in_last_batch = 0
188
+
170
189
  outbox = EventBuffer.new(config.capacity, config.logger)
171
190
  flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
172
191
 
173
- Thread.new { main_loop(inbox, outbox, flush_workers) }
192
+ if !@diagnostic_accumulator.nil?
193
+ diagnostic_event_workers = NonBlockingThreadPool.new(1)
194
+ init_event = @diagnostic_accumulator.create_init_event(config)
195
+ send_diagnostic_event(init_event, diagnostic_event_workers)
196
+ else
197
+ diagnostic_event_workers = nil
198
+ end
199
+
200
+ Thread.new { main_loop(inbox, outbox, flush_workers, diagnostic_event_workers) }
174
201
  end
175
202
 
176
203
  private
177
204
 
178
- def now_millis()
179
- (Time.now.to_f * 1000).to_i
180
- end
181
-
182
- def main_loop(inbox, outbox, flush_workers)
205
+ def main_loop(inbox, outbox, flush_workers, diagnostic_event_workers)
183
206
  running = true
184
207
  while running do
185
208
  begin
@@ -191,11 +214,13 @@ module LaunchDarkly
191
214
  trigger_flush(outbox, flush_workers)
192
215
  when FlushUsersMessage
193
216
  @user_keys.clear
217
+ when DiagnosticEventMessage
218
+ send_and_reset_diagnostics(outbox, diagnostic_event_workers)
194
219
  when TestSyncMessage
195
- synchronize_for_testing(flush_workers)
220
+ synchronize_for_testing(flush_workers, diagnostic_event_workers)
196
221
  message.completed
197
222
  when StopMessage
198
- do_shutdown(flush_workers)
223
+ do_shutdown(flush_workers, diagnostic_event_workers)
199
224
  running = false
200
225
  message.completed
201
226
  end
@@ -205,18 +230,23 @@ module LaunchDarkly
205
230
  end
206
231
  end
207
232
 
208
- def do_shutdown(flush_workers)
233
+ def do_shutdown(flush_workers, diagnostic_event_workers)
209
234
  flush_workers.shutdown
210
235
  flush_workers.wait_for_termination
236
+ if !diagnostic_event_workers.nil?
237
+ diagnostic_event_workers.shutdown
238
+ diagnostic_event_workers.wait_for_termination
239
+ end
211
240
  begin
212
241
  @client.finish
213
242
  rescue
214
243
  end
215
244
  end
216
245
 
217
- def synchronize_for_testing(flush_workers)
246
+ def synchronize_for_testing(flush_workers, diagnostic_event_workers)
218
247
  # Used only by unit tests. Wait until all active flush workers have finished.
219
248
  flush_workers.wait_all
249
+ diagnostic_event_workers.wait_all if !diagnostic_event_workers.nil?
220
250
  end
221
251
 
222
252
  def dispatch_event(event, outbox)
@@ -260,7 +290,9 @@ module LaunchDarkly
260
290
  if user.nil? || !user.has_key?(:key)
261
291
  true
262
292
  else
263
- @user_keys.add(user[:key].to_s)
293
+ known = @user_keys.add(user[:key].to_s)
294
+ @deduplicated_users += 1 if known
295
+ known
264
296
  end
265
297
  end
266
298
 
@@ -268,7 +300,7 @@ module LaunchDarkly
268
300
  debug_until = event[:debugEventsUntilDate]
269
301
  if !debug_until.nil?
270
302
  last_past = @last_known_past_time.value
271
- debug_until > last_past && debug_until > now_millis
303
+ debug_until > last_past && debug_until > Impl::Util.current_time_millis
272
304
  else
273
305
  false
274
306
  end
@@ -281,34 +313,44 @@ module LaunchDarkly
281
313
 
282
314
  payload = outbox.get_payload
283
315
  if !payload.events.empty? || !payload.summary.counters.empty?
316
+ count = payload.events.length + (payload.summary.counters.empty? ? 0 : 1)
317
+ @events_in_last_batch = count
284
318
  # If all available worker threads are busy, success will be false and no job will be queued.
285
319
  success = flush_workers.post do
286
320
  begin
287
- resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
288
- handle_response(resp) if !resp.nil?
321
+ events_out = @formatter.make_output_events(payload.events, payload.summary)
322
+ result = @event_sender.send_event_data(events_out.to_json, false)
323
+ @disabled.value = true if result.must_shutdown
324
+ if !result.time_from_server.nil?
325
+ @last_known_past_time.value = (result.time_from_server.to_f * 1000).to_i
326
+ end
289
327
  rescue => e
290
328
  Util.log_exception(@config.logger, "Unexpected error in event processor", e)
291
329
  end
292
330
  end
293
331
  outbox.clear if success # Reset our internal state, these events now belong to the flush worker
332
+ else
333
+ @events_in_last_batch = 0
294
334
  end
295
335
  end
296
336
 
297
- def handle_response(res)
298
- status = res.code.to_i
299
- if status >= 400
300
- message = Util.http_error_message(status, "event delivery", "some events were dropped")
301
- @config.logger.error { "[LDClient] #{message}" }
302
- if !Util.http_error_recoverable?(status)
303
- @disabled.value = true
304
- end
305
- else
306
- if !res["date"].nil?
307
- begin
308
- res_time = (Time.httpdate(res["date"]).to_f * 1000).to_i
309
- @last_known_past_time.value = res_time
310
- rescue ArgumentError
311
- end
337
+ def send_and_reset_diagnostics(outbox, diagnostic_event_workers)
338
+ return if @diagnostic_accumulator.nil?
339
+ dropped_count = outbox.get_and_clear_dropped_count
340
+ event = @diagnostic_accumulator.create_periodic_event_and_reset(dropped_count, @deduplicated_users, @events_in_last_batch)
341
+ @deduplicated_users = 0
342
+ @events_in_last_batch = 0
343
+ send_diagnostic_event(event, diagnostic_event_workers)
344
+ end
345
+
346
+ def send_diagnostic_event(event, diagnostic_event_workers)
347
+ return if diagnostic_event_workers.nil?
348
+ uri = URI(@config.events_uri + "/diagnostic")
349
+ diagnostic_event_workers.post do
350
+ begin
351
+ @event_sender.send_event_data(event.to_json, true)
352
+ rescue => e
353
+ Util.log_exception(@config.logger, "Unexpected error in event processor", e)
312
354
  end
313
355
  end
314
356
  end
@@ -323,6 +365,7 @@ module LaunchDarkly
323
365
  @capacity = capacity
324
366
  @logger = logger
325
367
  @capacity_exceeded = false
368
+ @dropped_events = 0
326
369
  @events = []
327
370
  @summarizer = EventSummarizer.new
328
371
  end
@@ -333,6 +376,7 @@ module LaunchDarkly
333
376
  @events.push(event)
334
377
  @capacity_exceeded = false
335
378
  else
379
+ @dropped_events += 1
336
380
  if !@capacity_exceeded
337
381
  @capacity_exceeded = true
338
382
  @logger.warn { "[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events." }
@@ -348,54 +392,18 @@ module LaunchDarkly
348
392
  return FlushPayload.new(@events, @summarizer.snapshot)
349
393
  end
350
394
 
395
+ def get_and_clear_dropped_count
396
+ ret = @dropped_events
397
+ @dropped_events = 0
398
+ ret
399
+ end
400
+
351
401
  def clear
352
402
  @events = []
353
403
  @summarizer.clear
354
404
  end
355
405
  end
356
406
 
357
- # @private
358
- class EventPayloadSendTask
359
- def run(sdk_key, config, client, payload, formatter)
360
- events_out = formatter.make_output_events(payload.events, payload.summary)
361
- res = nil
362
- body = events_out.to_json
363
- payload_id = SecureRandom.uuid
364
- (0..1).each do |attempt|
365
- if attempt > 0
366
- config.logger.warn { "[LDClient] Will retry posting events after 1 second" }
367
- sleep(1)
368
- end
369
- begin
370
- client.start if !client.started?
371
- config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
372
- uri = URI(config.events_uri + "/bulk")
373
- req = Net::HTTP::Post.new(uri)
374
- req.content_type = "application/json"
375
- req.body = body
376
- req["Authorization"] = sdk_key
377
- req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
378
- req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
379
- req["X-LaunchDarkly-Payload-ID"] = payload_id
380
- req["Connection"] = "keep-alive"
381
- res = client.request(req)
382
- rescue StandardError => exn
383
- config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
384
- next
385
- end
386
- status = res.code.to_i
387
- if status < 200 || status >= 300
388
- if Util.http_error_recoverable?(status)
389
- next
390
- end
391
- end
392
- break
393
- end
394
- # used up our retries, return the last response if any
395
- res
396
- end
397
- end
398
-
399
407
  # @private
400
408
  class EventOutputFormatter
401
409
  def initialize(config)