Skip to content

Commit 554bb91

Browse files
feat: add odp to project and user context (#316)
* add odp to project, optimizely factory and user context * rename zaius classes/attributes * remove api_key/api_host setters from odp_config
1 parent bbe9465 commit 554bb91

25 files changed

+951
-169
lines changed

lib/optimizely.rb

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
require_relative 'optimizely/notification_center'
3939
require_relative 'optimizely/optimizely_config'
4040
require_relative 'optimizely/optimizely_user_context'
41+
require_relative 'optimizely/odp/lru_cache'
42+
require_relative 'optimizely/odp/odp_manager'
43+
require_relative 'optimizely/helpers/sdk_settings'
4144

4245
module Optimizely
4346
class Project
@@ -46,7 +49,7 @@ class Project
4649
attr_reader :notification_center
4750
# @api no-doc
4851
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
49-
:event_processor, :logger, :stopped
52+
:event_processor, :logger, :odp_manager, :stopped
5053

5154
# Constructor for Projects.
5255
#
@@ -62,6 +65,8 @@ class Project
6265
# @param config_manager - Optional Responds to 'config' method.
6366
# @param notification_center - Optional Instance of NotificationCenter.
6467
# @param event_processor - Optional Responds to process.
68+
# @param default_decide_options: Optional default decision options.
69+
# @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
6570

6671
def initialize( # rubocop:disable Metrics/ParameterLists
6772
datafile = nil,
@@ -74,13 +79,15 @@ def initialize( # rubocop:disable Metrics/ParameterLists
7479
config_manager = nil,
7580
notification_center = nil,
7681
event_processor = nil,
77-
default_decide_options = []
82+
default_decide_options = [],
83+
settings = nil
7884
)
7985
@logger = logger || NoOpLogger.new
8086
@error_handler = error_handler || NoOpErrorHandler.new
8187
@event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
8288
@user_profile_service = user_profile_service
8389
@default_decide_options = []
90+
@sdk_settings = settings
8491

8592
if default_decide_options.is_a? Array
8693
@default_decide_options = default_decide_options.clone
@@ -98,6 +105,16 @@ def initialize( # rubocop:disable Metrics/ParameterLists
98105

99106
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
100107

108+
setup_odp!
109+
110+
@odp_manager = OdpManager.new(
111+
disable: @sdk_settings.odp_disabled,
112+
segment_manager: @sdk_settings.odp_segment_manager,
113+
event_manager: @sdk_settings.odp_event_manager,
114+
segments_cache: @sdk_settings.odp_segments_cache,
115+
logger: @logger
116+
)
117+
101118
@config_manager = if config_manager.respond_to?(:config)
102119
config_manager
103120
elsif sdk_key
@@ -113,6 +130,10 @@ def initialize( # rubocop:disable Metrics/ParameterLists
113130
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
114131
end
115132

133+
# must call this even if it's scheduled as a listener
134+
# in case the config manager was initialized before the listener was added
135+
update_odp_config_on_datafile_update unless @sdk_settings.odp_disabled
136+
116137
@decision_service = DecisionService.new(@logger, @user_profile_service)
117138

118139
@event_processor = if event_processor.respond_to?(:process)
@@ -816,6 +837,7 @@ def close
816837
@stopped = true
817838
@config_manager.stop! if @config_manager.respond_to?(:stop!)
818839
@event_processor.stop! if @event_processor.respond_to?(:stop!)
840+
@odp_manager.stop!
819841
end
820842

821843
def get_optimizely_config
@@ -869,6 +891,25 @@ def get_optimizely_config
869891
end
870892
end
871893

894+
# Send an event to the ODP server.
895+
#
896+
# @param action - the event action name.
897+
# @param type - the event type (default = "fullstack").
898+
# @param identifiers - a hash for identifiers.
899+
# @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.
900+
901+
def send_odp_event(action:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], identifiers: {}, data: {})
902+
@odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
903+
end
904+
905+
def identify_user(user_id:)
906+
@odp_manager.identify_user(user_id: user_id)
907+
end
908+
909+
def fetch_qualified_segments(user_id:, options: [])
910+
@odp_manager.fetch_qualified_segments(user_id: user_id, options: options)
911+
end
912+
872913
private
873914

874915
def get_variation_with_config(experiment_key, user_id, attributes, config)
@@ -1126,5 +1167,51 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
11261167
def project_config
11271168
@config_manager.config
11281169
end
1170+
1171+
def update_odp_config_on_datafile_update
1172+
# if datafile isn't ready, expects to be called again by the notification_center
1173+
return if @config_manager.respond_to?(:ready?) && !@config_manager.ready?
1174+
1175+
config = @config_manager&.config
1176+
return unless config
1177+
1178+
@odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
1179+
end
1180+
1181+
def setup_odp!
1182+
unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
1183+
@logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil?
1184+
@sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
1185+
end
1186+
1187+
return if @sdk_settings.odp_disabled
1188+
1189+
@notification_center.add_notification_listener(
1190+
NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
1191+
-> { update_odp_config_on_datafile_update }
1192+
)
1193+
1194+
if !@sdk_settings.odp_segment_manager.nil? && !Helpers::Validator.segment_manager_valid?(@sdk_settings.odp_segment_manager)
1195+
@logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.')
1196+
@sdk_settings.odp_segment_manager = nil
1197+
end
1198+
1199+
if !@sdk_settings.odp_event_manager.nil? && !Helpers::Validator.event_manager_valid?(@sdk_settings.odp_event_manager)
1200+
@logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.')
1201+
@sdk_settings.odp_event_manager = nil
1202+
end
1203+
1204+
return if @sdk_settings.odp_segment_manager
1205+
1206+
if !@sdk_settings.odp_segments_cache.nil? && !Helpers::Validator.segments_cache_valid?(@sdk_settings.odp_segments_cache)
1207+
@logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
1208+
@sdk_settings.odp_segments_cache = nil
1209+
end
1210+
1211+
@sdk_settings.odp_segments_cache ||= LRUCache.new(
1212+
@sdk_settings.segments_cache_size,
1213+
@sdk_settings.segments_cache_timeout_in_secs
1214+
)
1215+
end
11291216
end
11301217
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
# Copyright 2022, 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+
19+
require_relative 'constants'
20+
21+
module Optimizely
22+
module Helpers
23+
class OptimizelySdkSettings
24+
attr_accessor :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager, :odp_event_manager
25+
26+
# Contains configuration used for Optimizely Project initialization.
27+
#
28+
# @param disable_odp - Set this flag to true (default = false) to disable ODP features.
29+
# @param segments_cache_size - The maximum size of audience segments cache (optional. default = 10,000). Set to zero to disable caching.
30+
# @param segments_cache_timeout_in_secs - The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout.
31+
# @param odp_segments_cache - A custom odp segments cache. Required methods include: `save(key, value)`, `lookup(key) -> value`, and `reset()`
32+
# @param odp_segment_manager - A custom odp segment manager. Required method is: `fetch_qualified_segments(user_key, user_value, options)`.
33+
# @param odp_event_manager - A custom odp event manager. Required method is: `send_event(type:, action:, identifiers:, data:)`
34+
def initialize(
35+
disable_odp: false,
36+
segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
37+
segments_cache_timeout_in_secs: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECONDS],
38+
odp_segments_cache: nil,
39+
odp_segment_manager: nil,
40+
odp_event_manager: nil
41+
)
42+
@odp_disabled = disable_odp
43+
@segments_cache_size = segments_cache_size
44+
@segments_cache_timeout_in_secs = segments_cache_timeout_in_secs
45+
@odp_segments_cache = odp_segments_cache
46+
@odp_segment_manager = odp_segment_manager
47+
@odp_event_manager = odp_event_manager
48+
end
49+
end
50+
end
51+
end

lib/optimizely/helpers/validator.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,56 @@ def odp_data_types_valid?(data)
183183
valid_types = [String, Float, Integer, TrueClass, FalseClass, NilClass]
184184
data.values.all? { |e| valid_types.member? e.class }
185185
end
186+
187+
def segments_cache_valid?(segments_cache)
188+
# Determines if a given segments_cache is valid.
189+
#
190+
# segments_cache - custom cache to be validated.
191+
#
192+
# Returns boolean depending on whether cache has required methods.
193+
(
194+
segments_cache.respond_to?(:reset) &&
195+
segments_cache.method(:reset)&.parameters&.empty? &&
196+
segments_cache.respond_to?(:lookup) &&
197+
segments_cache.method(:lookup)&.parameters&.length&.positive? &&
198+
segments_cache.respond_to?(:save) &&
199+
segments_cache.method(:save)&.parameters&.length&.positive?
200+
)
201+
end
202+
203+
def segment_manager_valid?(segment_manager)
204+
# Determines if a given segment_manager is valid.
205+
#
206+
# segment_manager - custom manager to be validated.
207+
#
208+
# Returns boolean depending on whether manager has required methods.
209+
(
210+
segment_manager.respond_to?(:odp_config) &&
211+
segment_manager.respond_to?(:reset) &&
212+
segment_manager.method(:reset)&.parameters&.empty? &&
213+
segment_manager.respond_to?(:fetch_qualified_segments) &&
214+
(segment_manager.method(:fetch_qualified_segments)&.parameters&.length || 0) >= 3
215+
)
216+
end
217+
218+
def event_manager_valid?(event_manager)
219+
# Determines if a given event_manager is valid.
220+
#
221+
# event_manager - custom manager to be validated.
222+
#
223+
# Returns boolean depending on whether manager has required method and parameters.
224+
return false unless
225+
event_manager.respond_to?(:send_event) &&
226+
event_manager.respond_to?(:start!) &&
227+
(event_manager.method(:start!)&.parameters&.length || 0) >= 1 &&
228+
event_manager.respond_to?(:update_config) &&
229+
event_manager.respond_to?(:stop!)
230+
231+
required_parameters = Set[%i[keyreq type], %i[keyreq action], %i[keyreq identifiers], %i[keyreq data]]
232+
existing_parameters = event_manager.method(:send_event).parameters.to_set
233+
234+
existing_parameters & required_parameters == required_parameters
235+
end
186236
end
187237
end
188238
end

lib/optimizely/odp/odp_config.rb

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@ def api_host
6767
@mutex.synchronize { @api_host.clone }
6868
end
6969

70-
# Returns the api host for odp connections
71-
#
72-
# @return - The api host.
73-
74-
def api_host=(api_host)
75-
@mutex.synchronize { @api_host = api_host.clone }
76-
end
77-
7870
# Returns the api key for odp connections
7971
#
8072
# @return - The api key.
@@ -83,14 +75,6 @@ def api_key
8375
@mutex.synchronize { @api_key.clone }
8476
end
8577

86-
# Replace the api key with the provided string
87-
#
88-
# @param api_key - An api key
89-
90-
def api_key=(api_key)
91-
@mutex.synchronize { @api_key = api_key.clone }
92-
end
93-
9478
# Returns An array of qualified segments for this user
9579
#
9680
# @return - An array of segments names.

lib/optimizely/odp/odp_event_manager.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
1717
#
18-
require_relative 'zaius_rest_api_manager'
18+
require_relative 'odp_events_api_manager'
1919
require_relative '../helpers/constants'
20+
require_relative 'odp_event'
2021

2122
module Optimizely
2223
class OdpEventManager
@@ -26,7 +27,7 @@ class OdpEventManager
2627
# the BlockingQueue and buffers them for either a configured batch size or for a
2728
# maximum duration before the resulting LogEvent is sent to the NotificationCenter.
2829

29-
attr_reader :batch_size, :zaius_manager, :logger
30+
attr_reader :batch_size, :api_manager, :logger
3031
attr_accessor :odp_config
3132

3233
def initialize(
@@ -46,7 +47,7 @@ def initialize(
4647
# received signal should be sent after adding item to event_queue
4748
@received = ConditionVariable.new
4849
@logger = logger
49-
@zaius_manager = api_manager || ZaiusRestApiManager.new(logger: @logger, proxy_config: proxy_config)
50+
@api_manager = api_manager || OdpEventsApiManager.new(logger: @logger, proxy_config: proxy_config)
5051
@batch_size = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_BATCH_SIZE]
5152
@flush_interval = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_FLUSH_INTERVAL_SECONDS]
5253
@flush_deadline = 0
@@ -232,7 +233,7 @@ def flush_batch!
232233
i = 0
233234
while i < @retry_count
234235
begin
235-
should_retry = @zaius_manager.send_odp_events(@api_key, @api_host, @current_batch)
236+
should_retry = @api_manager.send_odp_events(@api_key, @api_host, @current_batch)
236237
rescue StandardError => e
237238
should_retry = false
238239
@logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], "Error: #{e.message} #{@current_batch.to_json}"))

lib/optimizely/odp/zaius_rest_api_manager.rb renamed to lib/optimizely/odp/odp_events_api_manager.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
require 'json'
2020

2121
module Optimizely
22-
class ZaiusRestApiManager
22+
class OdpEventsApiManager
2323
# Interface that handles sending ODP events.
2424

2525
def initialize(logger: nil, proxy_config: nil)

lib/optimizely/odp/odp_manager.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class OdpManager
3131
ODP_MANAGER_CONFIG = Helpers::Constants::ODP_MANAGER_CONFIG
3232
ODP_CONFIG_STATE = Helpers::Constants::ODP_CONFIG_STATE
3333

34+
# update_odp_config must be called to complete initialization
3435
def initialize(disable:, segments_cache: nil, segment_manager: nil, event_manager: nil, logger: nil)
3536
@enabled = !disable
3637
@segment_manager = segment_manager
@@ -54,7 +55,6 @@ def initialize(disable:, segments_cache: nil, segment_manager: nil, event_manage
5455
@event_manager ||= Optimizely::OdpEventManager.new(logger: @logger)
5556

5657
@segment_manager.odp_config = @odp_config
57-
@event_manager.start!(@odp_config)
5858
end
5959

6060
def fetch_qualified_segments(user_id:, options:)
@@ -123,6 +123,7 @@ def send_event(type:, action:, identifiers:, data:)
123123

124124
def update_odp_config(api_key, api_host, segments_to_check)
125125
# Update the odp config, reset the cache and send signal to the event processor to update its config.
126+
# Start the event manager if odp is integrated.
126127
return unless @enabled
127128

128129
config_changed = @odp_config.update(api_key, api_host, segments_to_check)
@@ -132,10 +133,15 @@ def update_odp_config(api_key, api_host, segments_to_check)
132133
end
133134

134135
@segment_manager.reset
135-
@event_manager.update_config
136+
137+
if @event_manager.running?
138+
@event_manager.update_config
139+
elsif @odp_config.odp_state == ODP_CONFIG_STATE[:INTEGRATED]
140+
@event_manager.start!(@odp_config)
141+
end
136142
end
137143

138-
def close!
144+
def stop!
139145
return unless @enabled
140146

141147
@event_manager.stop!

0 commit comments

Comments
 (0)