optimizely-sdk 3.2.0 → 3.3.0
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.
- 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
|