optimizely-sdk 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +70 -27
- data/lib/optimizely/config_manager/http_project_config_manager.rb +30 -13
- data/lib/optimizely/event/batch_event_processor.rb +225 -0
- data/lib/optimizely/event/entity/conversion_event.rb +43 -0
- data/lib/optimizely/event/entity/decision.rb +36 -0
- data/lib/optimizely/event/entity/event_batch.rb +86 -0
- data/lib/optimizely/event/entity/event_context.rb +50 -0
- data/lib/optimizely/event/entity/impression_event.rb +45 -0
- data/lib/optimizely/event/entity/snapshot.rb +33 -0
- data/lib/optimizely/event/entity/snapshot_event.rb +48 -0
- data/lib/optimizely/event/entity/user_event.rb +22 -0
- data/lib/optimizely/event/entity/visitor.rb +35 -0
- data/lib/optimizely/event/entity/visitor_attribute.rb +37 -0
- data/lib/optimizely/event/event_factory.rb +154 -0
- data/lib/optimizely/event/event_processor.rb +25 -0
- data/lib/optimizely/event/forwarding_event_processor.rb +43 -0
- data/lib/optimizely/event/user_event_factory.rb +87 -0
- data/lib/optimizely/helpers/constants.rb +1 -1
- data/lib/optimizely/helpers/date_time_utils.rb +30 -0
- data/lib/optimizely/helpers/validator.rb +9 -0
- data/lib/optimizely/notification_center.rb +5 -0
- data/lib/optimizely/optimizely_factory.rb +62 -3
- data/lib/optimizely/version.rb +1 -1
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6172194b87939a24b453c42267ee0431b7e86f4
|
4
|
+
data.tar.gz: 26fabaaa1ff0a7d6ea4aa2d6728b75dbe85d646c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7deb33a0386fa18352dc926a9920cd1a7a56373dfb5e7f1a03f1161013b867add78d13cdf051d3cde9762710bb9ee9adbc77669dee0a08ca9b9bda461df8f5e
|
7
|
+
data.tar.gz: 2c17a1ca3bef13c7c44e53c1a50ff32991f7abd966d7b9cd7af57708f395ea95a69ecab24090af936488ea21c8810b83c826ba0c01481058bd61a6f5da3d7fbd
|
data/lib/optimizely.rb
CHANGED
@@ -22,6 +22,9 @@ require_relative 'optimizely/config_manager/static_project_config_manager'
|
|
22
22
|
require_relative 'optimizely/decision_service'
|
23
23
|
require_relative 'optimizely/error_handler'
|
24
24
|
require_relative 'optimizely/event_builder'
|
25
|
+
require_relative 'optimizely/event/forwarding_event_processor'
|
26
|
+
require_relative 'optimizely/event/event_factory'
|
27
|
+
require_relative 'optimizely/event/user_event_factory'
|
25
28
|
require_relative 'optimizely/event_dispatcher'
|
26
29
|
require_relative 'optimizely/exceptions'
|
27
30
|
require_relative 'optimizely/helpers/constants'
|
@@ -35,8 +38,8 @@ module Optimizely
|
|
35
38
|
class Project
|
36
39
|
attr_reader :notification_center
|
37
40
|
# @api no-doc
|
38
|
-
attr_reader :config_manager, :decision_service, :error_handler,
|
39
|
-
:
|
41
|
+
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
|
42
|
+
:event_processor, :logger, :stopped
|
40
43
|
|
41
44
|
# Constructor for Projects.
|
42
45
|
#
|
@@ -49,8 +52,9 @@ module Optimizely
|
|
49
52
|
# @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
|
50
53
|
# @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
|
51
54
|
# Must provide at least one of datafile or sdk_key.
|
52
|
-
# @param config_manager - Optional Responds to
|
55
|
+
# @param config_manager - Optional Responds to 'config' method.
|
53
56
|
# @param notification_center - Optional Instance of NotificationCenter.
|
57
|
+
# @param event_processor - Optional Responds to process.
|
54
58
|
|
55
59
|
def initialize(
|
56
60
|
datafile = nil,
|
@@ -61,7 +65,8 @@ module Optimizely
|
|
61
65
|
user_profile_service = nil,
|
62
66
|
sdk_key = nil,
|
63
67
|
config_manager = nil,
|
64
|
-
notification_center = nil
|
68
|
+
notification_center = nil,
|
69
|
+
event_processor = nil
|
65
70
|
)
|
66
71
|
@logger = logger || NoOpLogger.new
|
67
72
|
@error_handler = error_handler || NoOpErrorHandler.new
|
@@ -77,7 +82,7 @@ module Optimizely
|
|
77
82
|
|
78
83
|
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
79
84
|
|
80
|
-
@config_manager = if config_manager.respond_to?(:
|
85
|
+
@config_manager = if config_manager.respond_to?(:config)
|
81
86
|
config_manager
|
82
87
|
elsif sdk_key
|
83
88
|
HTTPProjectConfigManager.new(
|
@@ -91,8 +96,14 @@ module Optimizely
|
|
91
96
|
else
|
92
97
|
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
|
93
98
|
end
|
99
|
+
|
94
100
|
@decision_service = DecisionService.new(@logger, @user_profile_service)
|
95
|
-
|
101
|
+
|
102
|
+
@event_processor = if event_processor.respond_to?(:process)
|
103
|
+
event_processor
|
104
|
+
else
|
105
|
+
ForwardingEventProcessor.new(@event_dispatcher, @logger, @notification_center)
|
106
|
+
end
|
96
107
|
end
|
97
108
|
|
98
109
|
# Buckets visitor and sends impression event to Optimizely.
|
@@ -243,20 +254,17 @@ module Optimizely
|
|
243
254
|
return nil
|
244
255
|
end
|
245
256
|
|
246
|
-
|
257
|
+
user_event = UserEventFactory.create_conversion_event(config, event, user_id, attributes, event_tags)
|
258
|
+
@event_processor.process(user_event)
|
247
259
|
@logger.log(Logger::INFO, "Tracking event '#{event_key}' for user '#{user_id}'.")
|
248
|
-
@logger.log(Logger::INFO,
|
249
|
-
"Dispatching conversion event to URL #{conversion_event.url} with params #{conversion_event.params}.")
|
250
|
-
begin
|
251
|
-
@event_dispatcher.dispatch_event(conversion_event)
|
252
|
-
rescue => e
|
253
|
-
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
|
254
|
-
end
|
255
260
|
|
256
|
-
@notification_center.
|
257
|
-
|
258
|
-
|
259
|
-
|
261
|
+
if @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:TRACK]).positive?
|
262
|
+
log_event = EventFactory.create_log_event(user_event, @logger)
|
263
|
+
@notification_center.send_notifications(
|
264
|
+
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
|
265
|
+
event_key, user_id, attributes, event_tags, log_event
|
266
|
+
)
|
267
|
+
end
|
260
268
|
nil
|
261
269
|
end
|
262
270
|
|
@@ -369,6 +377,32 @@ module Optimizely
|
|
369
377
|
enabled_features
|
370
378
|
end
|
371
379
|
|
380
|
+
# Get the value of the specified variable in the feature flag.
|
381
|
+
#
|
382
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
383
|
+
# @param variable_key - String key of variable for which we are getting the value
|
384
|
+
# @param user_id - String user ID
|
385
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
386
|
+
#
|
387
|
+
# @return [*] the type-casted variable value.
|
388
|
+
# @return [nil] if the feature flag or variable are not found.
|
389
|
+
|
390
|
+
def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
|
391
|
+
unless is_valid
|
392
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
|
393
|
+
return nil
|
394
|
+
end
|
395
|
+
variable_value = get_feature_variable_for_type(
|
396
|
+
feature_flag_key,
|
397
|
+
variable_key,
|
398
|
+
nil,
|
399
|
+
user_id,
|
400
|
+
attributes
|
401
|
+
)
|
402
|
+
|
403
|
+
variable_value
|
404
|
+
end
|
405
|
+
|
372
406
|
# Get the String value of the specified variable in the feature flag.
|
373
407
|
#
|
374
408
|
# @param feature_flag_key - String key of feature flag the variable belongs to
|
@@ -481,6 +515,14 @@ module Optimizely
|
|
481
515
|
config.is_a?(Optimizely::ProjectConfig)
|
482
516
|
end
|
483
517
|
|
518
|
+
def close
|
519
|
+
return if @stopped
|
520
|
+
|
521
|
+
@stopped = true
|
522
|
+
@config_manager.stop! if @config_manager.respond_to?(:stop!)
|
523
|
+
@event_processor.stop! if @event_processor.respond_to?(:stop!)
|
524
|
+
end
|
525
|
+
|
484
526
|
private
|
485
527
|
|
486
528
|
def get_variation_with_config(experiment_key, user_id, attributes, config)
|
@@ -556,6 +598,9 @@ module Optimizely
|
|
556
598
|
return nil if variable.nil?
|
557
599
|
|
558
600
|
feature_enabled = false
|
601
|
+
|
602
|
+
# If variable_type is nil, set it equal to variable['type']
|
603
|
+
variable_type ||= variable['type']
|
559
604
|
# Returns nil if type differs
|
560
605
|
if variable['type'] != variable_type
|
561
606
|
@logger.log(Logger::WARN,
|
@@ -663,18 +708,16 @@ module Optimizely
|
|
663
708
|
def send_impression(config, experiment, variation_key, user_id, attributes = nil)
|
664
709
|
experiment_key = experiment['key']
|
665
710
|
variation_id = config.get_variation_id_from_key(experiment_key, variation_key)
|
666
|
-
|
667
|
-
@
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
rescue => e
|
672
|
-
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
|
673
|
-
end
|
711
|
+
user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, user_id, attributes)
|
712
|
+
@event_processor.process(user_event)
|
713
|
+
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
|
714
|
+
|
715
|
+
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
|
674
716
|
variation = config.get_variation_from_id(experiment_key, variation_id)
|
717
|
+
log_event = EventFactory.create_log_event(user_event, @logger)
|
675
718
|
@notification_center.send_notifications(
|
676
719
|
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
|
677
|
-
experiment, user_id, attributes, variation,
|
720
|
+
experiment, user_id, attributes, variation, log_event
|
678
721
|
)
|
679
722
|
end
|
680
723
|
|
@@ -30,7 +30,7 @@ module Optimizely
|
|
30
30
|
class HTTPProjectConfigManager < ProjectConfigManager
|
31
31
|
# Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
|
32
32
|
|
33
|
-
attr_reader :
|
33
|
+
attr_reader :stopped
|
34
34
|
|
35
35
|
# Initialize config manager. One of sdk_key or url has to be set to be able to use.
|
36
36
|
#
|
@@ -38,9 +38,9 @@ module Optimizely
|
|
38
38
|
# datafile: Optional JSON string representing the project.
|
39
39
|
# polling_interval - Optional floating point number representing time interval in seconds
|
40
40
|
# at which to request datafile and set ProjectConfig.
|
41
|
-
# blocking_timeout -
|
42
|
-
# auto_update -
|
43
|
-
# start_by_default -
|
41
|
+
# blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized.
|
42
|
+
# auto_update - Boolean indicates to run infinitely or only once.
|
43
|
+
# start_by_default - Boolean indicates to start by default AsyncScheduler.
|
44
44
|
# url - Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
|
45
45
|
# url_template - Optional string template which in conjunction with sdk_key
|
46
46
|
# determines URL from where to fetch the datafile.
|
@@ -72,6 +72,7 @@ module Optimizely
|
|
72
72
|
@last_modified = nil
|
73
73
|
@async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
|
74
74
|
@async_scheduler.start! if start_by_default == true
|
75
|
+
@stopped = false
|
75
76
|
@skip_json_validation = skip_json_validation
|
76
77
|
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
77
78
|
@config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
|
@@ -84,20 +85,36 @@ module Optimizely
|
|
84
85
|
end
|
85
86
|
|
86
87
|
def start!
|
88
|
+
if @stopped
|
89
|
+
@logger.log(Logger::WARN, 'Not starting. Already stopped.')
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
87
93
|
@async_scheduler.start!
|
94
|
+
@stopped = false
|
88
95
|
end
|
89
96
|
|
90
97
|
def stop!
|
98
|
+
if @stopped
|
99
|
+
@logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
91
103
|
@async_scheduler.stop!
|
104
|
+
@config = nil
|
105
|
+
@stopped = true
|
92
106
|
end
|
93
107
|
|
94
|
-
def
|
108
|
+
def config
|
95
109
|
# Get Project Config.
|
96
110
|
|
111
|
+
# if stopped is true, then simply return @config.
|
97
112
|
# If the background datafile polling thread is running. and config has been initalized,
|
98
|
-
# we simply return config.
|
113
|
+
# we simply return @config.
|
99
114
|
# If it is not, we wait and block maximum for @blocking_timeout.
|
100
115
|
# If thread is not running, we fetch the datafile and update config.
|
116
|
+
return @config if @stopped
|
117
|
+
|
101
118
|
if @async_scheduler.running
|
102
119
|
return @config if ready?
|
103
120
|
|
@@ -196,7 +213,7 @@ module Optimizely
|
|
196
213
|
return
|
197
214
|
end
|
198
215
|
|
199
|
-
unless polling_interval.is_a?
|
216
|
+
unless polling_interval.is_a? Numeric
|
200
217
|
@logger.log(
|
201
218
|
Logger::ERROR,
|
202
219
|
"Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
|
@@ -205,7 +222,7 @@ module Optimizely
|
|
205
222
|
return
|
206
223
|
end
|
207
224
|
|
208
|
-
unless polling_interval.
|
225
|
+
unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
|
209
226
|
@logger.log(
|
210
227
|
Logger::DEBUG,
|
211
228
|
"Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
|
@@ -218,9 +235,9 @@ module Optimizely
|
|
218
235
|
end
|
219
236
|
|
220
237
|
def blocking_timeout(blocking_timeout)
|
221
|
-
# Sets time in seconds to block the
|
238
|
+
# Sets time in seconds to block the config call until config has been initialized.
|
222
239
|
#
|
223
|
-
# blocking_timeout - Time in seconds
|
240
|
+
# blocking_timeout - Time in seconds to block the config call.
|
224
241
|
|
225
242
|
# If valid set given timeout, default blocking_timeout otherwise.
|
226
243
|
|
@@ -229,7 +246,7 @@ module Optimizely
|
|
229
246
|
Logger::DEBUG,
|
230
247
|
"Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
|
231
248
|
)
|
232
|
-
@
|
249
|
+
@blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
|
233
250
|
return
|
234
251
|
end
|
235
252
|
|
@@ -238,7 +255,7 @@ module Optimizely
|
|
238
255
|
Logger::ERROR,
|
239
256
|
"Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
|
240
257
|
)
|
241
|
-
@
|
258
|
+
@blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
|
242
259
|
return
|
243
260
|
end
|
244
261
|
|
@@ -247,7 +264,7 @@ module Optimizely
|
|
247
264
|
Logger::DEBUG,
|
248
265
|
"Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
|
249
266
|
)
|
250
|
-
@
|
267
|
+
@blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
|
251
268
|
return
|
252
269
|
end
|
253
270
|
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2019, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require_relative 'event_processor'
|
19
|
+
require_relative '../helpers/validator'
|
20
|
+
module Optimizely
|
21
|
+
class BatchEventProcessor < EventProcessor
|
22
|
+
# BatchEventProcessor is a batched implementation of the Interface EventProcessor.
|
23
|
+
# Events passed to the BatchEventProcessor are immediately added to a EventQueue.
|
24
|
+
# The BatchEventProcessor maintains a single consumer thread that pulls events off of
|
25
|
+
# the BlockingQueue and buffers them for either a configured batch size or for a
|
26
|
+
# maximum duration before the resulting LogEvent is sent to the NotificationCenter.
|
27
|
+
|
28
|
+
attr_reader :event_queue, :event_dispatcher, :current_batch, :started, :batch_size, :flush_interval
|
29
|
+
|
30
|
+
DEFAULT_BATCH_SIZE = 10
|
31
|
+
DEFAULT_BATCH_INTERVAL = 30_000 # interval in milliseconds
|
32
|
+
DEFAULT_QUEUE_CAPACITY = 1000
|
33
|
+
|
34
|
+
FLUSH_SIGNAL = 'FLUSH_SIGNAL'
|
35
|
+
SHUTDOWN_SIGNAL = 'SHUTDOWN_SIGNAL'
|
36
|
+
|
37
|
+
def initialize(
|
38
|
+
event_queue: SizedQueue.new(DEFAULT_QUEUE_CAPACITY),
|
39
|
+
event_dispatcher: Optimizely::EventDispatcher.new,
|
40
|
+
batch_size: DEFAULT_BATCH_SIZE,
|
41
|
+
flush_interval: DEFAULT_BATCH_INTERVAL,
|
42
|
+
logger: NoOpLogger.new,
|
43
|
+
notification_center: nil
|
44
|
+
)
|
45
|
+
@event_queue = event_queue
|
46
|
+
@logger = logger
|
47
|
+
@event_dispatcher = event_dispatcher
|
48
|
+
@batch_size = if (batch_size.is_a? Integer) && positive_number?(batch_size)
|
49
|
+
batch_size
|
50
|
+
else
|
51
|
+
@logger.log(Logger::DEBUG, "Setting to default batch_size: #{DEFAULT_BATCH_SIZE}.")
|
52
|
+
DEFAULT_BATCH_SIZE
|
53
|
+
end
|
54
|
+
@flush_interval = if positive_number?(flush_interval)
|
55
|
+
flush_interval
|
56
|
+
else
|
57
|
+
@logger.log(Logger::DEBUG, "Setting to default flush_interval: #{DEFAULT_BATCH_INTERVAL} ms.")
|
58
|
+
DEFAULT_BATCH_INTERVAL
|
59
|
+
end
|
60
|
+
@notification_center = notification_center
|
61
|
+
@mutex = Mutex.new
|
62
|
+
@received = ConditionVariable.new
|
63
|
+
@current_batch = []
|
64
|
+
@started = false
|
65
|
+
start!
|
66
|
+
end
|
67
|
+
|
68
|
+
def start!
|
69
|
+
if @started == true
|
70
|
+
@logger.log(Logger::WARN, 'Service already started.')
|
71
|
+
return
|
72
|
+
end
|
73
|
+
@flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
|
74
|
+
@thread = Thread.new { run }
|
75
|
+
@started = true
|
76
|
+
end
|
77
|
+
|
78
|
+
def flush
|
79
|
+
@mutex.synchronize do
|
80
|
+
@event_queue << FLUSH_SIGNAL
|
81
|
+
@received.signal
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def process(user_event)
|
86
|
+
@logger.log(Logger::DEBUG, "Received userEvent: #{user_event}")
|
87
|
+
|
88
|
+
if !@started || !@thread.alive?
|
89
|
+
@logger.log(Logger::WARN, 'Executor shutdown, not accepting tasks.')
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
@mutex.synchronize do
|
94
|
+
begin
|
95
|
+
@event_queue << user_event
|
96
|
+
@received.signal
|
97
|
+
rescue Exception
|
98
|
+
@logger.log(Logger::WARN, 'Payload not accepted by the queue.')
|
99
|
+
return
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def stop!
|
105
|
+
return unless @started
|
106
|
+
|
107
|
+
@mutex.synchronize do
|
108
|
+
@event_queue << SHUTDOWN_SIGNAL
|
109
|
+
@received.signal
|
110
|
+
end
|
111
|
+
|
112
|
+
@started = false
|
113
|
+
@logger.log(Logger::WARN, 'Stopping scheduler.')
|
114
|
+
@thread.exit
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def run
|
120
|
+
loop do
|
121
|
+
if Helpers::DateTimeUtils.create_timestamp > @flushing_interval_deadline
|
122
|
+
@logger.log(
|
123
|
+
Logger::DEBUG,
|
124
|
+
'Deadline exceeded flushing current batch.'
|
125
|
+
)
|
126
|
+
flush_queue!
|
127
|
+
end
|
128
|
+
|
129
|
+
item = nil
|
130
|
+
|
131
|
+
@mutex.synchronize do
|
132
|
+
@received.wait(@mutex, 0.05)
|
133
|
+
item = @event_queue.pop if @event_queue.length.positive?
|
134
|
+
end
|
135
|
+
|
136
|
+
if item.nil?
|
137
|
+
sleep(0.05)
|
138
|
+
next
|
139
|
+
end
|
140
|
+
|
141
|
+
if item == SHUTDOWN_SIGNAL
|
142
|
+
@logger.log(Logger::INFO, 'Received shutdown signal.')
|
143
|
+
break
|
144
|
+
end
|
145
|
+
|
146
|
+
if item == FLUSH_SIGNAL
|
147
|
+
@logger.log(Logger::DEBUG, 'Received flush signal.')
|
148
|
+
flush_queue!
|
149
|
+
next
|
150
|
+
end
|
151
|
+
|
152
|
+
add_to_batch(item) if item.is_a? Optimizely::UserEvent
|
153
|
+
end
|
154
|
+
rescue SignalException
|
155
|
+
@logger.log(Logger::INFO, 'Interrupted while processing buffer.')
|
156
|
+
rescue Exception => e
|
157
|
+
@logger.log(Logger::ERROR, "Uncaught exception processing buffer. #{e.message}")
|
158
|
+
ensure
|
159
|
+
@logger.log(
|
160
|
+
Logger::INFO,
|
161
|
+
'Exiting processing loop. Attempting to flush pending events.'
|
162
|
+
)
|
163
|
+
flush_queue!
|
164
|
+
end
|
165
|
+
|
166
|
+
def flush_queue!
|
167
|
+
return if @current_batch.empty?
|
168
|
+
|
169
|
+
log_event = Optimizely::EventFactory.create_log_event(@current_batch, @logger)
|
170
|
+
begin
|
171
|
+
@event_dispatcher.dispatch_event(log_event)
|
172
|
+
@notification_center&.send_notifications(
|
173
|
+
NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT],
|
174
|
+
log_event
|
175
|
+
)
|
176
|
+
rescue StandardError => e
|
177
|
+
@logger.log(Logger::ERROR, "Error dispatching event: #{log_event} #{e.message}.")
|
178
|
+
end
|
179
|
+
@current_batch = []
|
180
|
+
end
|
181
|
+
|
182
|
+
def add_to_batch(user_event)
|
183
|
+
if should_split?(user_event)
|
184
|
+
flush_queue!
|
185
|
+
@current_batch = []
|
186
|
+
end
|
187
|
+
|
188
|
+
# Reset the deadline if starting a new batch.
|
189
|
+
@flushing_interval_deadline = (Helpers::DateTimeUtils.create_timestamp + @flush_interval) if @current_batch.empty?
|
190
|
+
|
191
|
+
@logger.log(Logger::DEBUG, "Adding user event: #{user_event} to batch.")
|
192
|
+
@current_batch << user_event
|
193
|
+
return unless @current_batch.length >= @batch_size
|
194
|
+
|
195
|
+
@logger.log(Logger::DEBUG, 'Flushing on max batch size!')
|
196
|
+
flush_queue!
|
197
|
+
end
|
198
|
+
|
199
|
+
def should_split?(user_event)
|
200
|
+
return false if @current_batch.empty?
|
201
|
+
|
202
|
+
current_context = @current_batch.last.event_context
|
203
|
+
new_context = user_event.event_context
|
204
|
+
|
205
|
+
# Revisions should match
|
206
|
+
unless current_context[:revision] == new_context[:revision]
|
207
|
+
@logger.log(Logger::DEBUG, 'Revisions mismatched: Flushing current batch.')
|
208
|
+
return true
|
209
|
+
end
|
210
|
+
|
211
|
+
# Projects should match
|
212
|
+
unless current_context[:project_id] == new_context[:project_id]
|
213
|
+
@logger.log(Logger::DEBUG, 'Project Ids mismatched: Flushing current batch.')
|
214
|
+
return true
|
215
|
+
end
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
def positive_number?(value)
|
220
|
+
# Returns true if the given value is positive finite number.
|
221
|
+
# false otherwise.
|
222
|
+
Helpers::Validator.finite_number?(value) && value.positive?
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|