launchdarkly-server-sdk 5.5.11 → 5.5.12

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: 3726dd61c5d5366f734b12e9803ea528ec03ccb9
4
- data.tar.gz: ea169915fd5092048cae20bc45494ba8a6a18d82
3
+ metadata.gz: 6711ad265d73300d06cb1a287c76ae1981e9c1e7
4
+ data.tar.gz: 27c72e98745eb679e91a0691766991277490995f
5
5
  SHA512:
6
- metadata.gz: 26cfea25eced467021ecfab5b76390a35ef3d5a3b3fbccc7322e427f452c1e7d13e1fd22abe15bb940b4a9e01dd7a38123e5a9b2ddc41b9a1ec97447986590bc
7
- data.tar.gz: d0c2420bda1218e2785f9c54ab134c4ee870a1883d887f16160fa0af83fa662c9831c20c191c8f0692b9ed6353868052662675f2d3a0dc136a65df2f97b4d0f2
6
+ metadata.gz: 5f0a6c09dd0cff741849392b32d75ac469a08116d918ebaeff2020693d7971bdf5082fee42fe310298fdfb20051da3547e98964fec88c952a3351d1642b5a4c8
7
+ data.tar.gz: 4f4474d63efe13be2db68977b51b1f7d65217415f740823e439568980c28713e0bbdcaab83a7e23c32d0889eed32fcd6cf0778d8dd1e15e0d573994081219157
data/CHANGELOG.md CHANGED
@@ -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.5.12] - 2019-08-05
6
+ ### Fixed:
7
+ - Under conditions where analytics events are being generated at an extremely high rate (for instance, if an application is evaluating a flag repeatedly in a tight loop on many threads), it was possible for the internal event processing logic to fall behind on processing the events, causing them to use more and more memory. The logic has been changed to drop events if necessary so that besides the existing limit on the number of events waiting to be sent to LaunchDarkly (`config.capacity`), the same limit also applies on the number of events that are waiting to be processed by the worker thread that decides whether or not to send them to LaunchDarkly. If that limit is exceeded, this warning message will be logged once: "Events are being produced faster than they can be processed; some events will be dropped". Under normal conditions this should never happen; this change is meant to avoid a concurrency bottleneck in applications that are already so busy that thread starvation is likely.
8
+
5
9
  ## [5.5.11] - 2019-07-24
6
10
  ### Fixed:
7
11
  - `FileDataSource` was using `YAML.load`, which has a known [security vulnerability](https://trailofbits.github.io/rubysec/yaml/index.html). This has been changed to use `YAML.safe_load`, which will refuse to parse any files that contain the `!` directives used in this type of attack. This issue does not affect any applications that do not use `FileDataSource` (which is meant for testing purposes, not production use). ([#139](https://github.com/launchdarkly/ruby-server-sdk/issues/139))
@@ -4,6 +4,23 @@ require "concurrent/executors"
4
4
  require "thread"
5
5
  require "time"
6
6
 
7
+ #
8
+ # Analytics event processing in the SDK involves several components. The purpose of this design is to
9
+ # minimize overhead on the application threads that are generating analytics events.
10
+ #
11
+ # EventProcessor receives an analytics event from the SDK client, on an application thread. It places
12
+ # the event in a bounded queue, the "inbox", and immediately returns.
13
+ #
14
+ # On a separate worker thread, EventDispatcher consumes events from the inbox. These are considered
15
+ # "input events" because they may or may not actually be sent to LaunchDarkly; most flag evaluation
16
+ # events are not sent, but are counted and the counters become part of a single summary event.
17
+ # EventDispatcher updates those counters, creates "index" events for any users that have not been seen
18
+ # recently, and places any events that will be sent to LaunchDarkly into the "outbox" queue.
19
+ #
20
+ # When it is time to flush events to LaunchDarkly, the contents of the outbox are handed off to
21
+ # another worker thread which sends the HTTP request.
22
+ #
23
+
7
24
  module LaunchDarkly
8
25
  MAX_FLUSH_WORKERS = 5
9
26
  CURRENT_SCHEMA_VERSION = 3
@@ -68,28 +85,30 @@ module LaunchDarkly
68
85
  # @private
69
86
  class EventProcessor
70
87
  def initialize(sdk_key, config, client = nil)
71
- @queue = Queue.new
88
+ @logger = config.logger
89
+ @inbox = SizedQueue.new(config.capacity)
72
90
  @flush_task = Concurrent::TimerTask.new(execution_interval: config.flush_interval) do
73
- @queue << FlushMessage.new
91
+ post_to_inbox(FlushMessage.new)
74
92
  end
75
93
  @flush_task.execute
76
94
  @users_flush_task = Concurrent::TimerTask.new(execution_interval: config.user_keys_flush_interval) do
77
- @queue << FlushUsersMessage.new
95
+ post_to_inbox(FlushUsersMessage.new)
78
96
  end
79
97
  @users_flush_task.execute
80
98
  @stopped = Concurrent::AtomicBoolean.new(false)
81
-
82
- EventDispatcher.new(@queue, sdk_key, config, client)
99
+ @inbox_full = Concurrent::AtomicBoolean.new(false)
100
+
101
+ EventDispatcher.new(@inbox, sdk_key, config, client)
83
102
  end
84
103
 
85
104
  def add_event(event)
86
105
  event[:creationDate] = (Time.now.to_f * 1000).to_i
87
- @queue << EventMessage.new(event)
106
+ post_to_inbox(EventMessage.new(event))
88
107
  end
89
108
 
90
109
  def flush
91
110
  # flush is done asynchronously
92
- @queue << FlushMessage.new
111
+ post_to_inbox(FlushMessage.new)
93
112
  end
94
113
 
95
114
  def stop
@@ -97,9 +116,11 @@ module LaunchDarkly
97
116
  if @stopped.make_true
98
117
  @flush_task.shutdown
99
118
  @users_flush_task.shutdown
100
- @queue << FlushMessage.new
119
+ # Note that here we are not calling post_to_inbox, because we *do* want to wait if the inbox
120
+ # is full; an orderly shutdown can't happen unless these messages are received.
121
+ @inbox << FlushMessage.new
101
122
  stop_msg = StopMessage.new
102
- @queue << stop_msg
123
+ @inbox << stop_msg
103
124
  stop_msg.wait_for_completion
104
125
  end
105
126
  end
@@ -107,14 +128,30 @@ module LaunchDarkly
107
128
  # exposed only for testing
108
129
  def wait_until_inactive
109
130
  sync_msg = TestSyncMessage.new
110
- @queue << sync_msg
131
+ @inbox << sync_msg
111
132
  sync_msg.wait_for_completion
112
133
  end
134
+
135
+ private
136
+
137
+ def post_to_inbox(message)
138
+ begin
139
+ @inbox.push(message, non_block=true)
140
+ rescue ThreadError
141
+ # If the inbox is full, it means the EventDispatcher thread is seriously backed up with not-yet-processed
142
+ # events. This is unlikely, but if it happens, it means the application is probably doing a ton of flag
143
+ # evaluations across many threads-- so if we wait for a space in the inbox, we risk a very serious slowdown
144
+ # of the app. To avoid that, we'll just drop the event. The log warning about this will only be shown once.
145
+ if @inbox_full.make_true
146
+ @logger.warn { "[LDClient] Events are being produced faster than they can be processed; some events will be dropped" }
147
+ end
148
+ end
149
+ end
113
150
  end
114
151
 
115
152
  # @private
116
153
  class EventDispatcher
117
- def initialize(queue, sdk_key, config, client)
154
+ def initialize(inbox, sdk_key, config, client)
118
155
  @sdk_key = sdk_key
119
156
  @config = config
120
157
 
@@ -129,10 +166,10 @@ module LaunchDarkly
129
166
  @disabled = Concurrent::AtomicBoolean.new(false)
130
167
  @last_known_past_time = Concurrent::AtomicReference.new(0)
131
168
 
132
- buffer = EventBuffer.new(config.capacity, config.logger)
169
+ outbox = EventBuffer.new(config.capacity, config.logger)
133
170
  flush_workers = NonBlockingThreadPool.new(MAX_FLUSH_WORKERS)
134
171
 
135
- Thread.new { main_loop(queue, buffer, flush_workers) }
172
+ Thread.new { main_loop(inbox, outbox, flush_workers) }
136
173
  end
137
174
 
138
175
  private
@@ -141,16 +178,16 @@ module LaunchDarkly
141
178
  (Time.now.to_f * 1000).to_i
142
179
  end
143
180
 
144
- def main_loop(queue, buffer, flush_workers)
181
+ def main_loop(inbox, outbox, flush_workers)
145
182
  running = true
146
183
  while running do
147
184
  begin
148
- message = queue.pop
185
+ message = inbox.pop
149
186
  case message
150
187
  when EventMessage
151
- dispatch_event(message.event, buffer)
188
+ dispatch_event(message.event, outbox)
152
189
  when FlushMessage
153
- trigger_flush(buffer, flush_workers)
190
+ trigger_flush(outbox, flush_workers)
154
191
  when FlushUsersMessage
155
192
  @user_keys.clear
156
193
  when TestSyncMessage
@@ -181,11 +218,11 @@ module LaunchDarkly
181
218
  flush_workers.wait_all
182
219
  end
183
220
 
184
- def dispatch_event(event, buffer)
221
+ def dispatch_event(event, outbox)
185
222
  return if @disabled.value
186
223
 
187
224
  # Always record the event in the summary.
188
- buffer.add_to_summary(event)
225
+ outbox.add_to_summary(event)
189
226
 
190
227
  # Decide whether to add the event to the payload. Feature events may be added twice, once for
191
228
  # the event (if tracked) and once for debugging.
@@ -205,7 +242,7 @@ module LaunchDarkly
205
242
  # an identify event for that user.
206
243
  if !(will_add_full_event && @config.inline_users_in_events)
207
244
  if event.has_key?(:user) && !notice_user(event[:user]) && event[:kind] != "identify"
208
- buffer.add_event({
245
+ outbox.add_event({
209
246
  kind: "index",
210
247
  creationDate: event[:creationDate],
211
248
  user: event[:user]
@@ -213,8 +250,8 @@ module LaunchDarkly
213
250
  end
214
251
  end
215
252
 
216
- buffer.add_event(event) if will_add_full_event
217
- buffer.add_event(debug_event) if !debug_event.nil?
253
+ outbox.add_event(event) if will_add_full_event
254
+ outbox.add_event(debug_event) if !debug_event.nil?
218
255
  end
219
256
 
220
257
  # Add to the set of users we've noticed, and return true if the user was already known to us.
@@ -236,12 +273,12 @@ module LaunchDarkly
236
273
  end
237
274
  end
238
275
 
239
- def trigger_flush(buffer, flush_workers)
276
+ def trigger_flush(outbox, flush_workers)
240
277
  if @disabled.value
241
278
  return
242
279
  end
243
280
 
244
- payload = buffer.get_payload
281
+ payload = outbox.get_payload
245
282
  if !payload.events.empty? || !payload.summary.counters.empty?
246
283
  # If all available worker threads are busy, success will be false and no job will be queued.
247
284
  success = flush_workers.post do
@@ -252,7 +289,7 @@ module LaunchDarkly
252
289
  Util.log_exception(@config.logger, "Unexpected error in event processor", e)
253
290
  end
254
291
  end
255
- buffer.clear if success # Reset our internal state, these events now belong to the flush worker
292
+ outbox.clear if success # Reset our internal state, these events now belong to the flush worker
256
293
  end
257
294
  end
258
295
 
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "5.5.11"
2
+ VERSION = "5.5.12"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.11
4
+ version: 5.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-25 00:00:00.000000000 Z
11
+ date: 2019-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb