Skip to content

feat: add odp to project and user context #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0a7b1ee
add odp to project and user context
andrewleap-optimizely Sep 27, 2022
f47cff6
fix broken tests
andrewleap-optimizely Sep 27, 2022
ce1b1ae
add project/user_context odp tests
andrewleap-optimizely Sep 28, 2022
2bcd343
Merge branch 'master' into aleap/add_odp_to_client_and_user_context
andrewleap-optimizely Sep 28, 2022
22fee39
enable odp for legacy tests
andrewleap-optimizely Sep 29, 2022
503172a
syncronize qualified_for
andrewleap-optimizely Sep 29, 2022
9e4482b
skip segments_cache if odp disabled
andrewleap-optimizely Sep 29, 2022
e3d4621
remove odp_manager from user_context
andrewleap-optimizely Sep 29, 2022
2ff91ff
make fetch_qualified_segments non blocking
andrewleap-optimizely Oct 3, 2022
ee0934b
add sdk_settings
andrewleap-optimizely Oct 3, 2022
784eb8a
rename zaius files
andrewleap-optimizely Oct 5, 2022
19ed8da
rename zaius classes/attributes
andrewleap-optimizely Oct 6, 2022
e58f444
extract odp setup to method
andrewleap-optimizely Oct 6, 2022
de97dc3
add non blocking flag to fetch_qualified_segments
andrewleap-optimizely Oct 6, 2022
80619f9
change odp_state when config updated via setters
andrewleap-optimizely Oct 6, 2022
19dd02d
add support for sdk_settings in optimizely factory
andrewleap-optimizely Oct 6, 2022
94d75fd
remove api_key/api_host setters from odp_config
andrewleap-optimizely Oct 7, 2022
1062469
switch fetch_segments flag param to callback
andrewleap-optimizely Oct 7, 2022
d02b0e8
return status bool from sync fetch_segments
andrewleap-optimizely Oct 10, 2022
a0cd8a9
fix copyright
andrewleap-optimizely Oct 11, 2022
521ca7b
move method validation to init
andrewleap-optimizely Oct 11, 2022
e56d8b2
delay starting event_manager until odp integration
andrewleap-optimizely Oct 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
require_relative 'optimizely/optimizely_user_context'
require_relative 'optimizely/odp/lru_cache'
require_relative 'optimizely/odp/odp_manager'
require_relative 'optimizely/helpers/sdk_settings'

module Optimizely
class Project
Expand All @@ -64,6 +65,8 @@ class Project
# @param config_manager - Optional Responds to 'config' method.
# @param notification_center - Optional Instance of NotificationCenter.
# @param event_processor - Optional Responds to process.
# @param default_decide_options: Optional default decision options.
# @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.

def initialize( # rubocop:disable Metrics/ParameterLists
datafile = nil,
Expand All @@ -77,14 +80,14 @@ def initialize( # rubocop:disable Metrics/ParameterLists
notification_center = nil,
event_processor = nil,
default_decide_options = [],
odp_segments_cache = nil,
sdk_settings = {}
settings = nil
)
@logger = logger || NoOpLogger.new
@error_handler = error_handler || NoOpErrorHandler.new
@event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
@user_profile_service = user_profile_service
@default_decide_options = []
@sdk_settings = settings

if default_decide_options.is_a? Array
@default_decide_options = default_decide_options.clone
Expand All @@ -102,37 +105,50 @@ def initialize( # rubocop:disable Metrics/ParameterLists

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

unless sdk_settings.is_a? Hash
@logger.log(Logger::DEBUG, 'Provided sdk_settings is not a hash.')
sdk_settings = {}
unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
@logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.')
@sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
end

odp_disabled = sdk_settings[:disable_odp] || false
odp_disabled = @sdk_settings.odp_disabled
odp_segments_cache = @sdk_settings.odp_segments_cache
odp_segment_manager = @sdk_settings.odp_segment_manager
odp_event_manager = @sdk_settings.odp_event_manager

unless odp_disabled
@notification_center.add_notification_listener(
NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
-> { update_odp_config_on_datafile_update }
)
end

segments_cache_size = sdk_settings[:segments_cache_size]
segments_cache_timeout_in_secs = sdk_settings[:segments_cache_timeout_in_secs]
if odp_segment_manager && !Helpers::Validator.segment_manager_valid?(odp_segment_manager)
@logger.log(Logger::ERROR, 'Invalid ODP segment manager, reverting to default.')
odp_segment_manager = nil
end

if odp_segments_cache && !Helpers::Validator.segments_cache_valid?(odp_segments_cache)
@logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
odp_segments_cache = nil
end
if odp_event_manager && !Helpers::Validator.event_manager_valid?(odp_event_manager)
@logger.log(Logger::ERROR, 'Invalid ODP event manager, reverting to default.')
odp_event_manager = nil
end

if (segments_cache_size || segments_cache_timeout_in_secs) && !odp_segments_cache
odp_segments_cache = LRUCache.new(
segments_cache_size || Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
segments_cache_timeout_in_secs || Helpers::Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECS]
)
unless odp_segment_manager
if odp_segments_cache && !Helpers::Validator.segments_cache_valid?(odp_segments_cache)
@logger.log(Logger::ERROR, 'Invalid ODP segments cache, reverting to default.')
odp_segments_cache = nil
end

odp_segments_cache ||= LRUCache.new(
@sdk_settings.segments_cache_size,
@sdk_settings.segments_cache_timeout_in_secs
)
end
end

# odp manager must be initialized before config_manager to ensure update of odp_config
@odp_manager = OdpManager.new(
disable: odp_disabled,
segment_manager: odp_segment_manager,
event_manager: odp_event_manager,
segments_cache: odp_segments_cache,
logger: @logger
)
Expand Down Expand Up @@ -921,6 +937,14 @@ def send_odp_event(action:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_
@odp_manager.send_event(type: type, action: action, identifiers: identifiers, data: data)
end

def identify_user(user_id:)
@odp_manager.identify_user(user_id: user_id)
end

def fetch_qualified_segments(user_id:, options: [])
@odp_manager.fetch_qualified_segments(user_id: user_id, options: options)
end

private

def get_variation_with_config(experiment_key, user_id, attributes, config)
Expand Down
51 changes: 51 additions & 0 deletions lib/optimizely/helpers/sdk_settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

#
# Copyright 2020, 2022, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative 'constants'

module Optimizely
module Helpers
class OptimizelySdkSettings
attr_reader :odp_disabled, :segments_cache_size, :segments_cache_timeout_in_secs, :odp_segments_cache, :odp_segment_manager, :odp_event_manager

# Contains configuration used for Optimizely Project initialization.
#
# @param disable_odp - Set this flag to true (default = false) to disable ODP features.
# @param segments_cache_size - The maximum size of audience segments cache (optional. default = 10,000). Set to zero to disable caching.
# @param segments_cache_timeout_in_secs - The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout.
# @param odp_segments_cache - A custom odp segments cache. Required methods include: `save(key, value)`, `lookup(key) -> value`, and `reset()`
# @param odp_segment_manager - A custom odp segment manager. Required method is: `fetch_qualified_segments(user_key, user_value, options)`.
# @param odp_event_manager - A custom odp event manager. Required method is: `send_event(type:, action:, identifiers:, data:)`
def initialize(
disable_odp: false,
segments_cache_size: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_CAPACITY],
segments_cache_timeout_in_secs: Constants::ODP_SEGMENTS_CACHE_CONFIG[:DEFAULT_TIMEOUT_SECONDS],
odp_segments_cache: nil,
odp_segment_manager: nil,
odp_event_manager: nil
)
@odp_disabled = disable_odp
@segments_cache_size = segments_cache_size
@segments_cache_timeout_in_secs = segments_cache_timeout_in_secs
@odp_segments_cache = odp_segments_cache
@odp_segment_manager = odp_segment_manager
@odp_event_manager = odp_event_manager
end
end
end
end
28 changes: 28 additions & 0 deletions lib/optimizely/helpers/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,34 @@ def segments_cache_valid?(segments_cache)
segments_cache.method(:save)&.parameters&.length&.positive?
)
end

def segment_manager_valid?(segment_manager)
# Determines if a given segment_manager is valid.
#
# segment_manager - custom manager to be validated.
#
# Returns boolean depending on whether manager has required methods.
(
segment_manager.respond_to?(:reset) &&
segment_manager.method(:reset)&.parameters&.empty? &&
segment_manager.respond_to?(:fetch_qualified_segments) &&
(segment_manager.method(:fetch_qualified_segments)&.parameters&.length || 0) >= 3
)
end

def event_manager_valid?(event_manager)
# Determines if a given event_manager is valid.
#
# event_manager - custom manager to be validated.
#
# Returns boolean depending on whether manager has required method and parameters.
return false unless event_manager.respond_to?(:send_event)

required_parameters = Set[%i[keyreq type], %i[keyreq action], %i[keyreq identifiers], %i[keyreq data]]
existing_parameters = event_manager.method(:send_event).parameters.to_set

existing_parameters & required_parameters == required_parameters
end
end
end
end
8 changes: 4 additions & 4 deletions lib/optimizely/odp/odp_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def initialize(disable:, segments_cache: nil, segment_manager: nil, event_manage

@event_manager ||= Optimizely::OdpEventManager.new(logger: @logger)

@segment_manager.odp_config = @odp_config
@event_manager.start!(@odp_config)
@segment_manager.odp_config = @odp_config if @segment_manager.respond_to?(:odp_config)
@event_manager.start!(@odp_config) if @event_manager.respond_to?(:start!)
end

def fetch_qualified_segments(user_id:, options:)
Expand Down Expand Up @@ -132,13 +132,13 @@ def update_odp_config(api_key, api_host, segments_to_check)
end

@segment_manager.reset
@event_manager.update_config
@event_manager.update_config if @event_manager.respond_to?(:update_config)
end

def stop!
return unless @enabled

@event_manager.stop!
@event_manager.stop! if @event_manager.respond_to?(:stop!)
end
end
end
20 changes: 13 additions & 7 deletions lib/optimizely/optimizely_user_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def initialize(optimizely_client, user_id, user_attributes)
@forced_decisions = {}
@qualified_segments = nil

@optimizely_client.odp_manager.identify_user(user_id: user_id)
@optimizely_client&.identify_user(user_id: user_id)
end

def clone
Expand Down Expand Up @@ -199,22 +199,28 @@ def qualified_segments=(segments)
# @return true if qualified.

def qualified_for?(segment)
return false if @qualified_segments.nil? || @qualified_segments.empty?
qualified = false
@qualified_segment_mutex.synchronize do
break if @qualified_segments.nil? || @qualified_segments.empty?

@qualified_segment_mutex.synchronize { @qualified_segments.include?(segment) }
qualified = @qualified_segments.include?(segment)
end
qualified
end

# Fetch all qualified segments for the user context.
#
# The segments fetched will be saved in `@qualified_segments` and can be accessed any time.
#
# @param options - A set of options for fetching qualified segments (optional).
# @return On success, returns a non-nil segments array (can be empty). On failure, nil is returned.
# @return a thread handle that may be joined and will returns array of segments or nil upon error

def fetch_qualified_segments(options = [])
segments = @optimizely_client.odp_manager.fetch_qualified_segments(user_id: @user_id, options: options)
self.qualified_segments = segments
segments
Thread.new(options) do |opts|
segments = @optimizely_client&.fetch_qualified_segments(user_id: @user_id, options: opts)
self.qualified_segments = segments
segments
end
end
end
end
3 changes: 2 additions & 1 deletion spec/audience_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:typed_audience_config) { Optimizely::DatafileProjectConfig.new(config_typed_audience_JSON, spy_logger, error_handler) }
let(:integration_config) { Optimizely::DatafileProjectConfig.new(config_integration_JSON, spy_logger, error_handler) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }
after(:example) { project_instance.close }

it 'should return true for user_meets_audience_conditions? when experiment is using no audience' do
# Both Audience Ids and Conditions are Empty
Expand Down
3 changes: 2 additions & 1 deletion spec/decision_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
let(:spy_user_profile_service) { spy('user_profile_service') }
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:decision_service) { Optimizely::DecisionService.new(spy_logger, spy_user_profile_service) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }
after(:example) { project_instance.close }

describe '#get_variation' do
before(:example) do
Expand Down
14 changes: 10 additions & 4 deletions spec/optimizely_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@
let(:error_handler) { Optimizely::NoOpErrorHandler.new }
let(:spy_logger) { spy('logger') }
let(:project_config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
let(:optimizely_config) { project_instance.get_optimizely_config }
let(:project_config_sim_keys) { Optimizely::DatafileProjectConfig.new(similar_exp_keys_JSON, spy_logger, error_handler) }
let(:project_instance_sim_keys) { Optimizely::Project.new(similar_exp_keys_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance_sim_keys) { Optimizely::Project.new(similar_exp_keys_JSON, nil, spy_logger, error_handler) }
let(:optimizely_config_sim_keys) { project_instance_sim_keys.get_optimizely_config }
let(:project_config_typed_audiences) { Optimizely::DatafileProjectConfig.new(typed_audiences_JSON, spy_logger, error_handler) }
let(:project_instance_typed_audiences) { Optimizely::Project.new(typed_audiences_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance_typed_audiences) { Optimizely::Project.new(typed_audiences_JSON, nil, spy_logger, error_handler) }
let(:optimizely_config_typed_audiences) { project_instance_typed_audiences.get_optimizely_config }
let(:project_config_similar_rule_keys) { Optimizely::DatafileProjectConfig.new(similar_rule_key_JSON, spy_logger, error_handler) }
let(:project_instance_similar_rule_keys) { Optimizely::Project.new(similar_rule_key_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], nil, {disable_odp: true}) }
let(:project_instance_similar_rule_keys) { Optimizely::Project.new(similar_rule_key_JSON, nil, spy_logger, error_handler) }
let(:optimizely_config_similar_rule_keys) { project_instance_similar_rule_keys.get_optimizely_config }
after(:example) do
project_instance.close
project_instance_sim_keys.close
project_instance_typed_audiences.close
project_instance_similar_rule_keys.close
end

it 'should return all experiments' do
experiments_map = optimizely_config['experimentsMap']
Expand Down
Loading