launchdarkly-server-sdk 5.5.7

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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,55 @@
1
+
2
+ module LaunchDarkly
3
+ # @private
4
+ EventSummary = Struct.new(:start_date, :end_date, :counters)
5
+
6
+ # Manages the state of summarizable information for the EventProcessor, including the
7
+ # event counters and user deduplication. Note that the methods of this class are
8
+ # deliberately not thread-safe; the EventProcessor is responsible for enforcing
9
+ # synchronization across both the summarizer and the event queue.
10
+ #
11
+ # @private
12
+ class EventSummarizer
13
+ def initialize
14
+ clear
15
+ end
16
+
17
+ # Adds this event to our counters, if it is a type of event we need to count.
18
+ def summarize_event(event)
19
+ if event[:kind] == "feature"
20
+ counter_key = {
21
+ key: event[:key],
22
+ version: event[:version],
23
+ variation: event[:variation]
24
+ }
25
+ c = @counters[counter_key]
26
+ if c.nil?
27
+ @counters[counter_key] = {
28
+ value: event[:value],
29
+ default: event[:default],
30
+ count: 1
31
+ }
32
+ else
33
+ c[:count] = c[:count] + 1
34
+ end
35
+ time = event[:creationDate]
36
+ if !time.nil?
37
+ @start_date = time if @start_date == 0 || time < @start_date
38
+ @end_date = time if time > @end_date
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns a snapshot of the current summarized event data, and resets this state.
44
+ def snapshot
45
+ ret = EventSummary.new(@start_date, @end_date, @counters)
46
+ ret
47
+ end
48
+
49
+ def clear
50
+ @start_date = 0
51
+ @end_date = 0
52
+ @counters = {}
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,468 @@
1
+ require "concurrent"
2
+ require "concurrent/atomics"
3
+ require "concurrent/executors"
4
+ require "thread"
5
+ require "time"
6
+
7
+ module LaunchDarkly
8
+ MAX_FLUSH_WORKERS = 5
9
+ CURRENT_SCHEMA_VERSION = 3
10
+ USER_ATTRS_TO_STRINGIFY_FOR_EVENTS = [ :key, :secondary, :ip, :country, :email, :firstName, :lastName,
11
+ :avatar, :name ]
12
+
13
+ private_constant :MAX_FLUSH_WORKERS
14
+ private_constant :CURRENT_SCHEMA_VERSION
15
+ private_constant :USER_ATTRS_TO_STRINGIFY_FOR_EVENTS
16
+
17
+ # @private
18
+ class NullEventProcessor
19
+ def add_event(event)
20
+ end
21
+
22
+ def flush
23
+ end
24
+
25
+ def stop
26
+ end
27
+ end
28
+
29
+ # @private
30
+ class EventMessage
31
+ def initialize(event)
32
+ @event = event
33
+ end
34
+ attr_reader :event
35
+ end
36
+
37
+ # @private
38
+ class FlushMessage
39
+ end
40
+
41
+ # @private
42
+ class FlushUsersMessage
43
+ end
44
+
45
+ # @private
46
+ class SynchronousMessage
47
+ def initialize
48
+ @reply = Concurrent::Semaphore.new(0)
49
+ end
50
+
51
+ def completed
52
+ @reply.release
53
+ end
54
+
55
+ def wait_for_completion
56
+ @reply.acquire
57
+ end
58
+ end
59
+
60
+ # @private
61
+ class TestSyncMessage < SynchronousMessage
62
+ end
63
+
64
+ # @private
65
+ class StopMessage < SynchronousMessage
66
+ end
67
+
68
+ # @private
69
+ class EventProcessor
70
+ def initialize(sdk_key, config, client = nil)
71
+ @queue = Queue.new
72
+ @flush_task = Concurrent::TimerTask.new(execution_interval: config.flush_interval) do
73
+ @queue << FlushMessage.new
74
+ end
75
+ @flush_task.execute
76
+ @users_flush_task = Concurrent::TimerTask.new(execution_interval: config.user_keys_flush_interval) do
77
+ @queue << FlushUsersMessage.new
78
+ end
79
+ @users_flush_task.execute
80
+ @stopped = Concurrent::AtomicBoolean.new(false)
81
+
82
+ EventDispatcher.new(@queue, sdk_key, config, client)
83
+ end
84
+
85
+ def add_event(event)
86
+ event[:creationDate] = (Time.now.to_f * 1000).to_i
87
+ @queue << EventMessage.new(event)
88
+ end
89
+
90
+ def flush
91
+ # flush is done asynchronously
92
+ @queue << FlushMessage.new
93
+ end
94
+
95
+ def stop
96
+ # final shutdown, which includes a final flush, is done synchronously
97
+ if @stopped.make_true
98
+ @flush_task.shutdown
99
+ @users_flush_task.shutdown
100
+ @queue << FlushMessage.new
101
+ stop_msg = StopMessage.new
102
+ @queue << stop_msg
103
+ stop_msg.wait_for_completion
104
+ end
105
+ end
106
+
107
+ # exposed only for testing
108
+ def wait_until_inactive
109
+ sync_msg = TestSyncMessage.new
110
+ @queue << sync_msg
111
+ sync_msg.wait_for_completion
112
+ end
113
+ end
114
+
115
+ # @private
116
+ class EventDispatcher
117
+ def initialize(queue, sdk_key, config, client)
118
+ @sdk_key = sdk_key
119
+ @config = config
120
+
121
+ if client
122
+ @client = client
123
+ else
124
+ @client = Util.new_http_client(@config.events_uri, @config)
125
+ end
126
+
127
+ @user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
128
+ @formatter = EventOutputFormatter.new(config)
129
+ @disabled = Concurrent::AtomicBoolean.new(false)
130
+ @last_known_past_time = Concurrent::AtomicReference.new(0)
131
+
132
+ buffer = EventBuffer.new(config.capacity, config.logger)
133
+ flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
134
+
135
+ Thread.new { main_loop(queue, buffer, flush_workers) }
136
+ end
137
+
138
+ private
139
+
140
+ def now_millis()
141
+ (Time.now.to_f * 1000).to_i
142
+ end
143
+
144
+ def main_loop(queue, buffer, flush_workers)
145
+ running = true
146
+ while running do
147
+ begin
148
+ message = queue.pop
149
+ case message
150
+ when EventMessage
151
+ dispatch_event(message.event, buffer)
152
+ when FlushMessage
153
+ trigger_flush(buffer, flush_workers)
154
+ when FlushUsersMessage
155
+ @user_keys.clear
156
+ when TestSyncMessage
157
+ synchronize_for_testing(flush_workers)
158
+ message.completed
159
+ when StopMessage
160
+ do_shutdown(flush_workers)
161
+ running = false
162
+ message.completed
163
+ end
164
+ rescue => e
165
+ Util.log_exception(@config.logger, "Unexpected error in event processor", e)
166
+ end
167
+ end
168
+ end
169
+
170
+ def do_shutdown(flush_workers)
171
+ flush_workers.shutdown
172
+ flush_workers.wait_for_termination
173
+ begin
174
+ @client.finish
175
+ rescue
176
+ end
177
+ end
178
+
179
+ def synchronize_for_testing(flush_workers)
180
+ # Used only by unit tests. Wait until all active flush workers have finished.
181
+ flush_workers.wait_all
182
+ end
183
+
184
+ def dispatch_event(event, buffer)
185
+ return if @disabled.value
186
+
187
+ # Always record the event in the summary.
188
+ buffer.add_to_summary(event)
189
+
190
+ # Decide whether to add the event to the payload. Feature events may be added twice, once for
191
+ # the event (if tracked) and once for debugging.
192
+ will_add_full_event = false
193
+ debug_event = nil
194
+ if event[:kind] == "feature"
195
+ will_add_full_event = event[:trackEvents]
196
+ if should_debug_event(event)
197
+ debug_event = event.clone
198
+ debug_event[:debug] = true
199
+ end
200
+ else
201
+ will_add_full_event = true
202
+ end
203
+
204
+ # For each user we haven't seen before, we add an index event - unless this is already
205
+ # an identify event for that user.
206
+ if !(will_add_full_event && @config.inline_users_in_events)
207
+ if event.has_key?(:user) && !notice_user(event[:user]) && event[:kind] != "identify"
208
+ buffer.add_event({
209
+ kind: "index",
210
+ creationDate: event[:creationDate],
211
+ user: event[:user]
212
+ })
213
+ end
214
+ end
215
+
216
+ buffer.add_event(event) if will_add_full_event
217
+ buffer.add_event(debug_event) if !debug_event.nil?
218
+ end
219
+
220
+ # Add to the set of users we've noticed, and return true if the user was already known to us.
221
+ def notice_user(user)
222
+ if user.nil? || !user.has_key?(:key)
223
+ true
224
+ else
225
+ @user_keys.add(user[:key].to_s)
226
+ end
227
+ end
228
+
229
+ def should_debug_event(event)
230
+ debug_until = event[:debugEventsUntilDate]
231
+ if !debug_until.nil?
232
+ last_past = @last_known_past_time.value
233
+ debug_until > last_past && debug_until > now_millis
234
+ else
235
+ false
236
+ end
237
+ end
238
+
239
+ def trigger_flush(buffer, flush_workers)
240
+ if @disabled.value
241
+ return
242
+ end
243
+
244
+ payload = buffer.get_payload
245
+ if !payload.events.empty? || !payload.summary.counters.empty?
246
+ # If all available worker threads are busy, success will be false and no job will be queued.
247
+ success = flush_workers.post do
248
+ begin
249
+ resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
250
+ handle_response(resp) if !resp.nil?
251
+ rescue => e
252
+ Util.log_exception(@config.logger, "Unexpected error in event processor", e)
253
+ end
254
+ end
255
+ buffer.clear if success # Reset our internal state, these events now belong to the flush worker
256
+ end
257
+ end
258
+
259
+ def handle_response(res)
260
+ status = res.code.to_i
261
+ if status >= 400
262
+ message = Util.http_error_message(status, "event delivery", "some events were dropped")
263
+ @config.logger.error { "[LDClient] #{message}" }
264
+ if !Util.http_error_recoverable?(status)
265
+ @disabled.value = true
266
+ end
267
+ else
268
+ if !res["date"].nil?
269
+ begin
270
+ res_time = (Time.httpdate(res["date"]).to_f * 1000).to_i
271
+ @last_known_past_time.value = res_time
272
+ rescue ArgumentError
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ # @private
280
+ FlushPayload = Struct.new(:events, :summary)
281
+
282
+ # @private
283
+ class EventBuffer
284
+ def initialize(capacity, logger)
285
+ @capacity = capacity
286
+ @logger = logger
287
+ @capacity_exceeded = false
288
+ @events = []
289
+ @summarizer = EventSummarizer.new
290
+ end
291
+
292
+ def add_event(event)
293
+ if @events.length < @capacity
294
+ @logger.debug { "[LDClient] Enqueueing event: #{event.to_json}" }
295
+ @events.push(event)
296
+ @capacity_exceeded = false
297
+ else
298
+ if !@capacity_exceeded
299
+ @capacity_exceeded = true
300
+ @logger.warn { "[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events." }
301
+ end
302
+ end
303
+ end
304
+
305
+ def add_to_summary(event)
306
+ @summarizer.summarize_event(event)
307
+ end
308
+
309
+ def get_payload
310
+ return FlushPayload.new(@events, @summarizer.snapshot)
311
+ end
312
+
313
+ def clear
314
+ @events = []
315
+ @summarizer.clear
316
+ end
317
+ end
318
+
319
+ # @private
320
+ class EventPayloadSendTask
321
+ def run(sdk_key, config, client, payload, formatter)
322
+ events_out = formatter.make_output_events(payload.events, payload.summary)
323
+ res = nil
324
+ body = events_out.to_json
325
+ (0..1).each do |attempt|
326
+ if attempt > 0
327
+ config.logger.warn { "[LDClient] Will retry posting events after 1 second" }
328
+ sleep(1)
329
+ end
330
+ begin
331
+ client.start if !client.started?
332
+ config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
333
+ uri = URI(config.events_uri + "/bulk")
334
+ req = Net::HTTP::Post.new(uri)
335
+ req.content_type = "application/json"
336
+ req.body = body
337
+ req["Authorization"] = sdk_key
338
+ req["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
339
+ req["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
340
+ req["Connection"] = "keep-alive"
341
+ res = client.request(req)
342
+ rescue StandardError => exn
343
+ config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
344
+ next
345
+ end
346
+ status = res.code.to_i
347
+ if status < 200 || status >= 300
348
+ if Util.http_error_recoverable?(status)
349
+ next
350
+ end
351
+ end
352
+ break
353
+ end
354
+ # used up our retries, return the last response if any
355
+ res
356
+ end
357
+ end
358
+
359
+ # @private
360
+ class EventOutputFormatter
361
+ def initialize(config)
362
+ @inline_users = config.inline_users_in_events
363
+ @user_filter = UserFilter.new(config)
364
+ end
365
+
366
+ # Transforms events into the format used for event sending.
367
+ def make_output_events(events, summary)
368
+ events_out = events.map { |e| make_output_event(e) }
369
+ if !summary.counters.empty?
370
+ events_out.push(make_summary_event(summary))
371
+ end
372
+ events_out
373
+ end
374
+
375
+ private
376
+
377
+ def process_user(event)
378
+ filtered = @user_filter.transform_user_props(event[:user])
379
+ Util.stringify_attrs(filtered, USER_ATTRS_TO_STRINGIFY_FOR_EVENTS)
380
+ end
381
+
382
+ def make_output_event(event)
383
+ case event[:kind]
384
+ when "feature"
385
+ is_debug = event[:debug]
386
+ out = {
387
+ kind: is_debug ? "debug" : "feature",
388
+ creationDate: event[:creationDate],
389
+ key: event[:key],
390
+ value: event[:value]
391
+ }
392
+ out[:default] = event[:default] if event.has_key?(:default)
393
+ out[:variation] = event[:variation] if event.has_key?(:variation)
394
+ out[:version] = event[:version] if event.has_key?(:version)
395
+ out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf)
396
+ if @inline_users || is_debug
397
+ out[:user] = process_user(event)
398
+ else
399
+ out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
400
+ end
401
+ out[:reason] = event[:reason] if !event[:reason].nil?
402
+ out
403
+ when "identify"
404
+ {
405
+ kind: "identify",
406
+ creationDate: event[:creationDate],
407
+ key: event[:user].nil? ? nil : event[:user][:key].to_s,
408
+ user: process_user(event)
409
+ }
410
+ when "custom"
411
+ out = {
412
+ kind: "custom",
413
+ creationDate: event[:creationDate],
414
+ key: event[:key]
415
+ }
416
+ out[:data] = event[:data] if event.has_key?(:data)
417
+ if @inline_users
418
+ out[:user] = process_user(event)
419
+ else
420
+ out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
421
+ end
422
+ out
423
+ when "index"
424
+ {
425
+ kind: "index",
426
+ creationDate: event[:creationDate],
427
+ user: process_user(event)
428
+ }
429
+ else
430
+ event
431
+ end
432
+ end
433
+
434
+ # Transforms the summary data into the format used for event sending.
435
+ def make_summary_event(summary)
436
+ flags = {}
437
+ summary[:counters].each { |ckey, cval|
438
+ flag = flags[ckey[:key]]
439
+ if flag.nil?
440
+ flag = {
441
+ default: cval[:default],
442
+ counters: []
443
+ }
444
+ flags[ckey[:key]] = flag
445
+ end
446
+ c = {
447
+ value: cval[:value],
448
+ count: cval[:count]
449
+ }
450
+ if !ckey[:variation].nil?
451
+ c[:variation] = ckey[:variation]
452
+ end
453
+ if ckey[:version].nil?
454
+ c[:unknown] = true
455
+ else
456
+ c[:version] = ckey[:version]
457
+ end
458
+ flag[:counters].push(c)
459
+ }
460
+ {
461
+ kind: "summary",
462
+ startDate: summary[:start_date],
463
+ endDate: summary[:end_date],
464
+ features: flags
465
+ }
466
+ end
467
+ end
468
+ end