ldclient-rb 3.0.3 → 4.0.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: 7c13b41385d09caf349197da95dc022227f5e143
4
- data.tar.gz: d9e5536ac51a27fef6e1bacbdde3385d04c5d599
3
+ metadata.gz: 65f975d234beca67eed5238b9e693fdfb39de00e
4
+ data.tar.gz: 93e6c8e3de9f9394f1cf9acb64218fda1b873ff6
5
5
  SHA512:
6
- metadata.gz: d9af4fc567f889a50dad1011367e6f2abf460f552645e850bd1364f12fd385da62a8f564d7fcb119e87ae51c135e5f0e174e8ecec956b4fce03392f5f927d953
7
- data.tar.gz: db400282e1500e4b627b8d9b94df8e60b87aa17756f7ceabeb7801346fd64199b038ae8388c3d4c0fe46ffe5517706670ed605b16e13e93a97129a05534cd2d6
6
+ metadata.gz: 1aacb9669181a5cee75fcfdad3170a195bd43bb572203251daa0fd892e94ecf95bd8627ee124fa961884a44d38209271e9f9fbb094a7773b123328d33011e6ab
7
+ data.tar.gz: 12b7baa6f4176b357b11307faf9588a918e34059d1a5bc2fc97e5c6076f25ad1b5d5f13b328b4295d4c1bd4a90dfb3fa02daf75549886948e5311c2b91753bed
@@ -0,0 +1,90 @@
1
+ version: 2
2
+
3
+ workflows:
4
+ version: 2
5
+ test:
6
+ jobs:
7
+ - test-misc-rubies
8
+ - test-2.2
9
+ - test-2.3
10
+ - test-2.4
11
+ - test-jruby-9.1
12
+
13
+ ruby-docker-template: &ruby-docker-template
14
+ steps:
15
+ - checkout
16
+ - run: |
17
+ if [[ $CIRCLE_JOB == test-jruby* ]]; then
18
+ gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
19
+ fi
20
+ - run: gem install bundler
21
+ - run: bundle install
22
+ - run: mkdir ./rspec
23
+ - run: bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
24
+ - store_test_results:
25
+ path: ./rspec
26
+ - store_artifacts:
27
+ path: ./rspec
28
+
29
+ jobs:
30
+ test-2.2:
31
+ <<: *ruby-docker-template
32
+ docker:
33
+ - image: circleci/ruby:2.2.9-jessie
34
+ - image: redis
35
+ test-2.3:
36
+ <<: *ruby-docker-template
37
+ docker:
38
+ - image: circleci/ruby:2.3.6-jessie
39
+ - image: redis
40
+ test-2.4:
41
+ <<: *ruby-docker-template
42
+ docker:
43
+ - image: circleci/ruby:2.4.3-jessie
44
+ - image: redis
45
+ test-jruby-9.1:
46
+ <<: *ruby-docker-template
47
+ docker:
48
+ - image: circleci/jruby:9-jdk
49
+ - image: redis
50
+
51
+ # The following very slow job uses an Ubuntu container to run the Ruby versions that
52
+ # CircleCI doesn't provide Docker images for.
53
+ test-misc-rubies:
54
+ machine:
55
+ image: circleci/classic:latest
56
+ environment:
57
+ - RUBIES: "ruby-2.1.9 ruby-2.0.0 ruby-1.9.3 jruby-9.0.5.0"
58
+ steps:
59
+ - run: sudo apt-get -q update
60
+ - run: sudo apt-get -qy install redis-server
61
+ - checkout
62
+ - run:
63
+ name: install all Ruby versions
64
+ command: "parallel rvm install ::: $RUBIES"
65
+ - run:
66
+ name: bundle install for all versions
67
+ shell: /bin/bash -leo pipefail # need -l in order for "rvm use" to work
68
+ command: |
69
+ set -e;
70
+ for i in $RUBIES;
71
+ do
72
+ rvm use $i;
73
+ if [[ $i == jruby* ]]; then
74
+ gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
75
+ fi
76
+ gem install bundler;
77
+ bundle install;
78
+ mv Gemfile.lock "Gemfile.lock.$i"
79
+ done
80
+ - run:
81
+ name: run tests for all versions
82
+ shell: /bin/bash -leo pipefail
83
+ command: |
84
+ set -e;
85
+ for i in $RUBIES;
86
+ do
87
+ rvm use $i;
88
+ cp "Gemfile.lock.$i" Gemfile.lock;
89
+ bundle exec rspec spec;
90
+ done
@@ -2,6 +2,16 @@
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
+ ## [4.0.0] - 2018-05-10
6
+
7
+ ### Changed:
8
+ - To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `inline_users_in_events`. For more details, see [Analytics Data Stream Reference](https://docs.launchdarkly.com/v2.0/docs/analytics-data-stream-reference).
9
+
10
+ ### Removed:
11
+ - JRuby 1.7 is no longer supported.
12
+ - Greatly reduced the number of indirect gem dependencies by removing `moneta`, which was previously a requirement for the Redis feature store.
13
+
14
+
5
15
  ## [3.0.3] - 2018-03-23
6
16
  ## Fixed
7
17
  - In the Redis feature store, fixed a synchronization problem that could cause a feature flag update to be missed if several of them happened in rapid succession.
data/README.md CHANGED
@@ -4,7 +4,6 @@ LaunchDarkly SDK for Ruby
4
4
  [![Gem Version](https://badge.fury.io/rb/ldclient-rb.svg)](http://badge.fury.io/rb/ldclient-rb)
5
5
 
6
6
  [![Circle CI](https://circleci.com/gh/launchdarkly/ruby-client/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/ruby-client/tree/master)
7
- [![Code Climate](https://codeclimate.com/github/launchdarkly/ruby-client/badges/gpa.svg)](https://codeclimate.com/github/launchdarkly/ruby-client)
8
7
  [![Test Coverage](https://codeclimate.com/github/launchdarkly/ruby-client/badges/coverage.svg)](https://codeclimate.com/github/launchdarkly/ruby-client/coverage)
9
8
  [![security](https://hakiri.io/github/launchdarkly/ruby-client/master.svg)](https://hakiri.io/github/launchdarkly/ruby-client/master)
10
9
 
@@ -22,12 +22,18 @@ Gem::Specification.new do |spec|
22
22
  spec.extensions = 'ext/mkrf_conf.rb'
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.7"
25
- spec.add_development_dependency "rake", "~> 10.0"
26
25
  spec.add_development_dependency "rspec", "~> 3.2"
27
26
  spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
28
27
  spec.add_development_dependency "redis", "~> 3.3.5"
29
28
  spec.add_development_dependency "connection_pool", ">= 2.1.2"
30
- spec.add_development_dependency "moneta", "~> 1.0.0"
29
+ if RUBY_VERSION >= "2.0.0"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
32
+ else
33
+ spec.add_development_dependency "rake", "12.1.0"
34
+ # higher versions of rake fail to install in JRuby 1.7
35
+ end
36
+ spec.add_development_dependency "timecop", "~> 0.9.1"
31
37
 
32
38
  spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
33
39
  if RUBY_VERSION >= "2.1.0"
@@ -2,13 +2,17 @@ require "ldclient-rb/version"
2
2
  require "ldclient-rb/evaluation"
3
3
  require "ldclient-rb/ldclient"
4
4
  require "ldclient-rb/cache_store"
5
+ require "ldclient-rb/expiring_cache"
5
6
  require "ldclient-rb/memoized_value"
6
7
  require "ldclient-rb/in_memory_store"
7
8
  require "ldclient-rb/config"
8
9
  require "ldclient-rb/newrelic"
9
10
  require "ldclient-rb/stream"
10
11
  require "ldclient-rb/polling"
11
- require "ldclient-rb/event_serializer"
12
+ require "ldclient-rb/user_filter"
13
+ require "ldclient-rb/simple_lru_cache"
14
+ require "ldclient-rb/non_blocking_thread_pool"
15
+ require "ldclient-rb/event_summarizer"
12
16
  require "ldclient-rb/events"
13
17
  require "ldclient-rb/redis_store"
14
18
  require "ldclient-rb/requestor"
@@ -54,7 +54,15 @@ module LaunchDarkly
54
54
  # @option opts [Boolean] :send_events (true) Whether or not to send events back to LaunchDarkly.
55
55
  # This differs from `offline` in that it affects only the sending of client-side events, not
56
56
  # streaming or polling for events from the server.
57
- #
57
+ # @option opts [Integer] :user_keys_capacity (1000) The number of user keys that the event processor
58
+ # can remember at any one time, so that duplicate user details will not be sent in analytics events.
59
+ # @option opts [Float] :user_keys_flush_interval (300) The interval in seconds at which the event
60
+ # processor will reset its set of known user keys.
61
+ # @option opts [Boolean] :inline_users_in_events (false) Whether to include full user details in every
62
+ # analytics event. By default, events will only include the user key, except for one "index" event
63
+ # that provides the full details for the user.
64
+ # @option opts [Object] :update_processor An object that will receive feature flag data from LaunchDarkly.
65
+ # Defaults to either the streaming or the polling processor, can be customized for tests.
58
66
  # @return [type] [description]
59
67
  # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
60
68
  def initialize(opts = {})
@@ -76,6 +84,10 @@ module LaunchDarkly
76
84
  @all_attributes_private = opts[:all_attributes_private] || false
77
85
  @private_attribute_names = opts[:private_attribute_names] || []
78
86
  @send_events = opts.has_key?(:send_events) ? opts[:send_events] : Config.default_send_events
87
+ @user_keys_capacity = opts[:user_keys_capacity] || Config.default_user_keys_capacity
88
+ @user_keys_flush_interval = opts[:user_keys_flush_interval] || Config.default_user_keys_flush_interval
89
+ @inline_users_in_events = opts[:inline_users_in_events] || false
90
+ @update_processor = opts[:update_processor]
79
91
  end
80
92
 
81
93
  #
@@ -186,6 +198,26 @@ module LaunchDarkly
186
198
  #
187
199
  attr_reader :send_events
188
200
 
201
+ #
202
+ # The number of user keys that the event processor can remember at any one time, so that
203
+ # duplicate user details will not be sent in analytics events.
204
+ #
205
+ attr_reader :user_keys_capacity
206
+
207
+ #
208
+ # The interval in seconds at which the event processor will reset its set of known user keys.
209
+ #
210
+ attr_reader :user_keys_flush_interval
211
+
212
+ #
213
+ # Whether to include full user details in every
214
+ # analytics event. By default, events will only include the user key, except for one "index" event
215
+ # that provides the full details for the user.
216
+ #
217
+ attr_reader :inline_users_in_events
218
+
219
+ attr_reader :update_processor
220
+
189
221
  #
190
222
  # The default LaunchDarkly client configuration. This configuration sets
191
223
  # reasonable defaults for most users.
@@ -264,5 +296,13 @@ module LaunchDarkly
264
296
  def self.default_send_events
265
297
  true
266
298
  end
299
+
300
+ def self.default_user_keys_capacity
301
+ 1000
302
+ end
303
+
304
+ def self.default_user_keys_flush_interval
305
+ 300
306
+ end
267
307
  end
268
308
  end
@@ -114,7 +114,7 @@ module LaunchDarkly
114
114
  # generated during prerequisite evaluation. Raises EvaluationError if the flag is not well-formed
115
115
  # Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
116
116
  # In that case, the caller should return the default value.
117
- def evaluate(flag, user, store)
117
+ def evaluate(flag, user, store, logger)
118
118
  if flag.nil?
119
119
  raise EvaluationError, "Flag does not exist"
120
120
  end
@@ -126,20 +126,23 @@ module LaunchDarkly
126
126
  events = []
127
127
 
128
128
  if flag[:on]
129
- res = eval_internal(flag, user, store, events)
130
-
131
- return { value: res, events: events } if !res.nil?
129
+ res = eval_internal(flag, user, store, events, logger)
130
+ if !res.nil?
131
+ res[:events] = events
132
+ return res
133
+ end
132
134
  end
133
135
 
134
- if !flag[:offVariation].nil? && flag[:offVariation] < flag[:variations].length
135
- value = flag[:variations][flag[:offVariation]]
136
- return { value: value, events: events }
136
+ offVariation = flag[:offVariation]
137
+ if !offVariation.nil? && offVariation < flag[:variations].length
138
+ value = flag[:variations][offVariation]
139
+ return { variation: offVariation, value: value, events: events }
137
140
  end
138
141
 
139
- { value: nil, events: events }
142
+ { variation: nil, value: nil, events: events }
140
143
  end
141
144
 
142
- def eval_internal(flag, user, store, events)
145
+ def eval_internal(flag, user, store, events, logger)
143
146
  failed_prereq = false
144
147
  # Evaluate prerequisites, if any
145
148
  (flag[:prerequisites] || []).each do |prerequisite|
@@ -149,14 +152,23 @@ module LaunchDarkly
149
152
  failed_prereq = true
150
153
  else
151
154
  begin
152
- prereq_res = eval_internal(prereq_flag, user, store, events)
153
- variation = get_variation(prereq_flag, prerequisite[:variation])
154
- events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
155
- if prereq_res.nil? || prereq_res != variation
155
+ prereq_res = eval_internal(prereq_flag, user, store, events, logger)
156
+ event = {
157
+ kind: "feature",
158
+ key: prereq_flag[:key],
159
+ variation: prereq_res.nil? ? nil : prereq_res[:variation],
160
+ value: prereq_res.nil? ? nil : prereq_res[:value],
161
+ version: prereq_flag[:version],
162
+ prereqOf: flag[:key],
163
+ trackEvents: prereq_flag[:trackEvents],
164
+ debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
165
+ }
166
+ events.push(event)
167
+ if prereq_res.nil? || prereq_res[:variation] != prerequisite[:variation]
156
168
  failed_prereq = true
157
169
  end
158
170
  rescue => exn
159
- @config.logger.error { "[LDClient] Error evaluating prerequisite: #{exn.inspect}" }
171
+ logger.error { "[LDClient] Error evaluating prerequisite: #{exn.inspect}" }
160
172
  failed_prereq = true
161
173
  end
162
174
  end
@@ -175,7 +187,9 @@ module LaunchDarkly
175
187
  # Check user target matches
176
188
  (flag[:targets] || []).each do |target|
177
189
  (target[:values] || []).each do |value|
178
- return get_variation(flag, target[:variation]) if value == user[:key]
190
+ if value == user[:key]
191
+ return { variation: target[:variation], value: get_variation(flag, target[:variation]) }
192
+ end
179
193
  end
180
194
  end
181
195
 
@@ -245,7 +259,7 @@ module LaunchDarkly
245
259
 
246
260
  def variation_for_user(rule, user, flag)
247
261
  if !rule[:variation].nil? # fixed variation
248
- return get_variation(flag, rule[:variation])
262
+ return { variation: rule[:variation], value: get_variation(flag, rule[:variation]) }
249
263
  elsif !rule[:rollout].nil? # percentage rollout
250
264
  rollout = rule[:rollout]
251
265
  bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
@@ -253,7 +267,9 @@ module LaunchDarkly
253
267
  sum = 0;
254
268
  rollout[:variations].each do |variate|
255
269
  sum += variate[:weight].to_f / 100000.0
256
- return get_variation(flag, variate[:variation]) if bucket < sum
270
+ if bucket < sum
271
+ return { variation: variate[:variation], value: get_variation(flag, variate[:variation]) }
272
+ end
257
273
  end
258
274
  nil
259
275
  else # the rule isn't well-formed
@@ -0,0 +1,52 @@
1
+
2
+ module LaunchDarkly
3
+ EventSummary = Struct.new(:start_date, :end_date, :counters)
4
+
5
+ # Manages the state of summarizable information for the EventProcessor, including the
6
+ # event counters and user deduplication. Note that the methods of this class are
7
+ # deliberately not thread-safe; the EventProcessor is responsible for enforcing
8
+ # synchronization across both the summarizer and the event queue.
9
+ class EventSummarizer
10
+ def initialize
11
+ clear
12
+ end
13
+
14
+ # Adds this event to our counters, if it is a type of event we need to count.
15
+ def summarize_event(event)
16
+ if event[:kind] == "feature"
17
+ counter_key = {
18
+ key: event[:key],
19
+ version: event[:version],
20
+ variation: event[:variation]
21
+ }
22
+ c = @counters[counter_key]
23
+ if c.nil?
24
+ @counters[counter_key] = {
25
+ value: event[:value],
26
+ default: event[:default],
27
+ count: 1
28
+ }
29
+ else
30
+ c[:count] = c[:count] + 1
31
+ end
32
+ time = event[:creationDate]
33
+ if !time.nil?
34
+ @start_date = time if @start_date == 0 || time < @start_date
35
+ @end_date = time if time > @end_date
36
+ end
37
+ end
38
+ end
39
+
40
+ # Returns a snapshot of the current summarized event data, and resets this state.
41
+ def snapshot
42
+ ret = EventSummary.new(@start_date, @end_date, @counters)
43
+ ret
44
+ end
45
+
46
+ def clear
47
+ @start_date = 0
48
+ @end_date = 0
49
+ @counters = {}
50
+ end
51
+ end
52
+ end
@@ -1,94 +1,426 @@
1
+ require "concurrent"
1
2
  require "concurrent/atomics"
3
+ require "concurrent/executors"
2
4
  require "thread"
5
+ require "time"
3
6
  require "faraday"
4
7
 
5
8
  module LaunchDarkly
9
+ MAX_FLUSH_WORKERS = 5
10
+ CURRENT_SCHEMA_VERSION = 3
11
+
12
+ class NullEventProcessor
13
+ def add_event(event)
14
+ end
15
+
16
+ def flush
17
+ end
18
+
19
+ def stop
20
+ end
21
+ end
22
+
23
+ class EventMessage
24
+ def initialize(event)
25
+ @event = event
26
+ end
27
+ attr_reader :event
28
+ end
29
+
30
+ class FlushMessage
31
+ end
32
+
33
+ class FlushUsersMessage
34
+ end
35
+
36
+ class SynchronousMessage
37
+ def initialize
38
+ @reply = Concurrent::Semaphore.new(0)
39
+ end
40
+
41
+ def completed
42
+ @reply.release
43
+ end
44
+
45
+ def wait_for_completion
46
+ @reply.acquire
47
+ end
48
+ end
49
+
50
+ class TestSyncMessage < SynchronousMessage
51
+ end
52
+
53
+ class StopMessage < SynchronousMessage
54
+ end
55
+
6
56
  class EventProcessor
7
- def initialize(sdk_key, config)
57
+ def initialize(sdk_key, config, client = nil)
8
58
  @queue = Queue.new
9
- @sdk_key = sdk_key
10
- @config = config
11
- @serializer = EventSerializer.new(config)
12
- @client = Faraday.new
59
+ @flush_task = Concurrent::TimerTask.new(execution_interval: config.flush_interval) do
60
+ @queue << FlushMessage.new
61
+ end
62
+ @flush_task.execute
63
+ @users_flush_task = Concurrent::TimerTask.new(execution_interval: config.user_keys_flush_interval) do
64
+ @queue << FlushUsersMessage.new
65
+ end
66
+ @users_flush_task.execute
13
67
  @stopped = Concurrent::AtomicBoolean.new(false)
14
- @worker = create_worker if @config.send_events
68
+
69
+ EventDispatcher.new(@queue, sdk_key, config, client)
70
+ end
71
+
72
+ def add_event(event)
73
+ event[:creationDate] = (Time.now.to_f * 1000).to_i
74
+ @queue << EventMessage.new(event)
15
75
  end
16
76
 
17
- def alive?
18
- !@stopped.value
77
+ def flush
78
+ # flush is done asynchronously
79
+ @queue << FlushMessage.new
19
80
  end
20
81
 
21
82
  def stop
83
+ # final shutdown, which includes a final flush, is done synchronously
22
84
  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
85
+ @flush_task.shutdown
86
+ @users_flush_task.shutdown
87
+ @queue << FlushMessage.new
88
+ stop_msg = StopMessage.new
89
+ @queue << stop_msg
90
+ stop_msg.wait_for_completion
27
91
  end
28
92
  end
29
93
 
30
- def create_worker
31
- Thread.new do
32
- while !@stopped.value do
33
- begin
34
- flush
35
- sleep(@config.flush_interval)
36
- rescue StandardError => exn
37
- log_exception(__method__.to_s, exn)
94
+ # exposed only for testing
95
+ def wait_until_inactive
96
+ sync_msg = TestSyncMessage.new
97
+ @queue << sync_msg
98
+ sync_msg.wait_for_completion
99
+ end
100
+ end
101
+
102
+ class EventDispatcher
103
+ def initialize(queue, sdk_key, config, client)
104
+ @sdk_key = sdk_key
105
+ @config = config
106
+ @client = client ? client : Faraday.new
107
+ @user_keys = SimpleLRUCacheSet.new(config.user_keys_capacity)
108
+ @formatter = EventOutputFormatter.new(config)
109
+ @disabled = Concurrent::AtomicBoolean.new(false)
110
+ @last_known_past_time = Concurrent::AtomicFixnum.new(0)
111
+
112
+ buffer = EventBuffer.new(config.capacity, config.logger)
113
+ flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
114
+
115
+ Thread.new { main_loop(queue, buffer, flush_workers) }
116
+ end
117
+
118
+ private
119
+
120
+ def now_millis()
121
+ (Time.now.to_f * 1000).to_i
122
+ end
123
+
124
+ def main_loop(queue, buffer, flush_workers)
125
+ running = true
126
+ while running do
127
+ begin
128
+ message = queue.pop
129
+ case message
130
+ when EventMessage
131
+ dispatch_event(message.event, buffer)
132
+ when FlushMessage
133
+ trigger_flush(buffer, flush_workers)
134
+ when FlushUsersMessage
135
+ @user_keys.clear
136
+ when TestSyncMessage
137
+ synchronize_for_testing(flush_workers)
138
+ message.completed
139
+ when StopMessage
140
+ do_shutdown(flush_workers)
141
+ running = false
142
+ message.completed
38
143
  end
144
+ rescue => e
145
+ @config.logger.warn { "[LDClient] Unexpected error in event processor: #{e.inspect}. \nTrace: #{e.backtrace}" }
39
146
  end
40
147
  end
41
148
  end
42
149
 
43
- def post_flushed_events(events)
44
- res = @client.post (@config.events_uri + "/bulk") do |req|
45
- req.headers["Authorization"] = @sdk_key
46
- req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
47
- req.headers["Content-Type"] = "application/json"
48
- req.body = @serializer.serialize_events(events)
49
- req.options.timeout = @config.read_timeout
50
- req.options.open_timeout = @config.connect_timeout
150
+ def do_shutdown(flush_workers)
151
+ flush_workers.shutdown
152
+ flush_workers.wait_for_termination
153
+ # There seems to be no such thing as "close" in Faraday: https://github.com/lostisland/faraday/issues/241
154
+ end
155
+
156
+ def synchronize_for_testing(flush_workers)
157
+ # Used only by unit tests. Wait until all active flush workers have finished.
158
+ flush_workers.wait_all
159
+ end
160
+
161
+ def dispatch_event(event, buffer)
162
+ return if @disabled.value
163
+
164
+ # Always record the event in the summary.
165
+ buffer.add_to_summary(event)
166
+
167
+ # Decide whether to add the event to the payload. Feature events may be added twice, once for
168
+ # the event (if tracked) and once for debugging.
169
+ will_add_full_event = false
170
+ debug_event = nil
171
+ if event[:kind] == "feature"
172
+ will_add_full_event = event[:trackEvents]
173
+ if should_debug_event(event)
174
+ debug_event = event.clone
175
+ debug_event[:debug] = true
176
+ end
177
+ else
178
+ will_add_full_event = true
51
179
  end
52
- if res.status < 200 || res.status >= 300
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
180
+
181
+ # For each user we haven't seen before, we add an index event - unless this is already
182
+ # an identify event for that user.
183
+ if !(will_add_full_event && @config.inline_users_in_events)
184
+ if event.has_key?(:user) && !notice_user(event[:user]) && event[:kind] != "identify"
185
+ buffer.add_event({
186
+ kind: "index",
187
+ creationDate: event[:creationDate],
188
+ user: event[:user]
189
+ })
57
190
  end
58
191
  end
192
+
193
+ buffer.add_event(event) if will_add_full_event
194
+ buffer.add_event(debug_event) if !debug_event.nil?
59
195
  end
60
196
 
61
- def flush
62
- return if @offline || !@config.send_events
63
- events = []
64
- begin
65
- loop do
66
- events << @queue.pop(true)
197
+ # Add to the set of users we've noticed, and return true if the user was already known to us.
198
+ def notice_user(user)
199
+ if user.nil? || !user.has_key?(:key)
200
+ true
201
+ else
202
+ @user_keys.add(user[:key])
203
+ end
204
+ end
205
+
206
+ def should_debug_event(event)
207
+ debug_until = event[:debugEventsUntilDate]
208
+ if !debug_until.nil?
209
+ last_past = @last_known_past_time.value
210
+ debug_until > last_past && debug_until > now_millis
211
+ else
212
+ false
213
+ end
214
+ end
215
+
216
+ def trigger_flush(buffer, flush_workers)
217
+ if @disabled.value
218
+ return
219
+ end
220
+
221
+ payload = buffer.get_payload
222
+ if !payload.events.empty? || !payload.summary.counters.empty?
223
+ # If all available worker threads are busy, success will be false and no job will be queued.
224
+ success = flush_workers.post do
225
+ resp = EventPayloadSendTask.new.run(@sdk_key, @config, @client, payload, @formatter)
226
+ handle_response(resp) if !resp.nil?
67
227
  end
68
- rescue ThreadError
228
+ buffer.clear if success # Reset our internal state, these events now belong to the flush worker
69
229
  end
230
+ end
70
231
 
71
- if !events.empty? && !@stopped.value
72
- post_flushed_events(events)
232
+ def handle_response(res)
233
+ if res.status == 401
234
+ @config.logger.error { "[LDClient] Received 401 error, no further events will be posted since SDK key is invalid" }
235
+ @disabled.value = true
236
+ else
237
+ if !res.headers.nil? && res.headers.has_key?("Date")
238
+ begin
239
+ res_time = (Time.httpdate(res.headers["Date"]).to_f * 1000).to_i
240
+ @last_known_past_time.value = res_time
241
+ rescue ArgumentError
242
+ end
243
+ end
73
244
  end
74
245
  end
246
+ end
247
+
248
+ FlushPayload = Struct.new(:events, :summary)
249
+
250
+ class EventBuffer
251
+ def initialize(capacity, logger)
252
+ @capacity = capacity
253
+ @logger = logger
254
+ @capacity_exceeded = false
255
+ @events = []
256
+ @summarizer = EventSummarizer.new
257
+ end
75
258
 
76
259
  def add_event(event)
77
- return if @offline || !@config.send_events || @stopped.value
260
+ if @events.length < @capacity
261
+ @logger.debug { "[LDClient] Enqueueing event: #{event.to_json}" }
262
+ @events.push(event)
263
+ @capacity_exceeded = false
264
+ else
265
+ if !@capacity_exceeded
266
+ @capacity_exceeded = true
267
+ @logger.warn { "[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events." }
268
+ end
269
+ end
270
+ end
271
+
272
+ def add_to_summary(event)
273
+ @summarizer.summarize_event(event)
274
+ end
78
275
 
79
- if @queue.length < @config.capacity
80
- event[:creationDate] = (Time.now.to_f * 1000).to_i
81
- @config.logger.debug { "[LDClient] Enqueueing event: #{event.to_json}" }
82
- @queue.push(event)
276
+ def get_payload
277
+ return FlushPayload.new(@events, @summarizer.snapshot)
278
+ end
279
+
280
+ def clear
281
+ @events = []
282
+ @summarizer.clear
283
+ end
284
+ end
83
285
 
84
- if !@worker.alive?
85
- @worker = create_worker
286
+ class EventPayloadSendTask
287
+ def run(sdk_key, config, client, payload, formatter)
288
+ events_out = formatter.make_output_events(payload.events, payload.summary)
289
+ res = nil
290
+ body = events_out.to_json
291
+ (0..1).each do |attempt|
292
+ if attempt > 0
293
+ config.logger.warn { "[LDClient] Will retry posting events after 1 second" }
294
+ sleep(1)
86
295
  end
296
+ begin
297
+ config.logger.debug { "[LDClient] sending #{events_out.length} events: #{body}" }
298
+ res = client.post (config.events_uri + "/bulk") do |req|
299
+ req.headers["Authorization"] = sdk_key
300
+ req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
301
+ req.headers["Content-Type"] = "application/json"
302
+ req.headers["X-LaunchDarkly-Event-Schema"] = CURRENT_SCHEMA_VERSION.to_s
303
+ req.body = body
304
+ req.options.timeout = config.read_timeout
305
+ req.options.open_timeout = config.connect_timeout
306
+ end
307
+ rescue StandardError => exn
308
+ config.logger.warn { "[LDClient] Error flushing events: #{exn.inspect}." }
309
+ next
310
+ end
311
+ if res.status < 200 || res.status >= 300
312
+ config.logger.error { "[LDClient] Unexpected status code while processing events: #{res.status}" }
313
+ if res.status >= 500
314
+ next
315
+ end
316
+ end
317
+ break
318
+ end
319
+ # used up our retries, return the last response if any
320
+ res
321
+ end
322
+ end
323
+
324
+ class EventOutputFormatter
325
+ def initialize(config)
326
+ @inline_users = config.inline_users_in_events
327
+ @user_filter = UserFilter.new(config)
328
+ end
329
+
330
+ # Transforms events into the format used for event sending.
331
+ def make_output_events(events, summary)
332
+ events_out = events.map { |e| make_output_event(e) }
333
+ if !summary.counters.empty?
334
+ events_out.push(make_summary_event(summary))
335
+ end
336
+ events_out
337
+ end
338
+
339
+ private
340
+
341
+ def make_output_event(event)
342
+ case event[:kind]
343
+ when "feature"
344
+ is_debug = event[:debug]
345
+ out = {
346
+ kind: is_debug ? "debug" : "feature",
347
+ creationDate: event[:creationDate],
348
+ key: event[:key],
349
+ value: event[:value]
350
+ }
351
+ out[:default] = event[:default] if event.has_key?(:default)
352
+ out[:variation] = event[:variation] if event.has_key?(:variation)
353
+ out[:version] = event[:version] if event.has_key?(:version)
354
+ out[:prereqOf] = event[:prereqOf] if event.has_key?(:prereqOf)
355
+ if @inline_users || is_debug
356
+ out[:user] = @user_filter.transform_user_props(event[:user])
357
+ else
358
+ out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
359
+ end
360
+ out
361
+ when "identify"
362
+ {
363
+ kind: "identify",
364
+ creationDate: event[:creationDate],
365
+ key: event[:user].nil? ? nil : event[:user][:key],
366
+ user: @user_filter.transform_user_props(event[:user])
367
+ }
368
+ when "custom"
369
+ out = {
370
+ kind: "custom",
371
+ creationDate: event[:creationDate],
372
+ key: event[:key]
373
+ }
374
+ out[:data] = event[:data] if event.has_key?(:data)
375
+ if @inline_users
376
+ out[:user] = @user_filter.transform_user_props(event[:user])
377
+ else
378
+ out[:userKey] = event[:user].nil? ? nil : event[:user][:key]
379
+ end
380
+ out
381
+ when "index"
382
+ {
383
+ kind: "index",
384
+ creationDate: event[:creationDate],
385
+ user: @user_filter.transform_user_props(event[:user])
386
+ }
87
387
  else
88
- @config.logger.warn { "[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events." }
388
+ event
89
389
  end
90
390
  end
91
391
 
92
- private :create_worker, :post_flushed_events
392
+ # Transforms the summary data into the format used for event sending.
393
+ def make_summary_event(summary)
394
+ flags = {}
395
+ summary[:counters].each { |ckey, cval|
396
+ flag = flags[ckey[:key]]
397
+ if flag.nil?
398
+ flag = {
399
+ default: cval[:default],
400
+ counters: []
401
+ }
402
+ flags[ckey[:key]] = flag
403
+ end
404
+ c = {
405
+ value: cval[:value],
406
+ count: cval[:count]
407
+ }
408
+ if !ckey[:variation].nil?
409
+ c[:variation] = ckey[:variation]
410
+ end
411
+ if ckey[:version].nil?
412
+ c[:unknown] = true
413
+ else
414
+ c[:version] = ckey[:version]
415
+ end
416
+ flag[:counters].push(c)
417
+ }
418
+ {
419
+ kind: "summary",
420
+ startDate: summary[:start_date],
421
+ endDate: summary[:end_date],
422
+ features: flags
423
+ }
424
+ end
93
425
  end
94
426
  end