Skip to content
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

feat: add odp to project and user context #316

Merged
merged 22 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 18 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
91 changes: 89 additions & 2 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
require_relative 'optimizely/notification_center'
require_relative 'optimizely/optimizely_config'
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 @@ -46,7 +49,7 @@ class Project
attr_reader :notification_center
# @api no-doc
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
:event_processor, :logger, :stopped
:event_processor, :logger, :odp_manager, :stopped

# Constructor for Projects.
#
Expand All @@ -62,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 @@ -74,13 +79,15 @@ def initialize( # rubocop:disable Metrics/ParameterLists
config_manager = nil,
notification_center = nil,
event_processor = nil,
default_decide_options = []
default_decide_options = [],
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
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved

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

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

setup_odp!
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved

@odp_manager = OdpManager.new(
disable: @sdk_settings.odp_disabled,
segment_manager: @sdk_settings.odp_segment_manager,
event_manager: @sdk_settings.odp_event_manager,
segments_cache: @sdk_settings.odp_segments_cache,
logger: @logger
)

@config_manager = if config_manager.respond_to?(:config)
config_manager
elsif sdk_key
Expand All @@ -113,6 +130,10 @@ def initialize( # rubocop:disable Metrics/ParameterLists
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
end

# must call this even if it's scheduled as a listener
# in case the config manager was initialized before the listener was added
update_odp_config_on_datafile_update unless @sdk_settings.odp_disabled

@decision_service = DecisionService.new(@logger, @user_profile_service)

@event_processor = if event_processor.respond_to?(:process)
Expand Down Expand Up @@ -816,6 +837,7 @@ def close
@stopped = true
@config_manager.stop! if @config_manager.respond_to?(:stop!)
@event_processor.stop! if @event_processor.respond_to?(:stop!)
@odp_manager.stop! if @odp_manager.respond_to?(:stop!)
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved
end

def get_optimizely_config
Expand Down Expand Up @@ -869,6 +891,25 @@ def get_optimizely_config
end
end

# Send an event to the ODP server.
#
# @param action - the event action name.
# @param type - the event type (default = "fullstack").
# @param identifiers - a hash for identifiers.
# @param data - a hash for associated data. The default event data will be added to this data before sending to the ODP server.

def send_odp_event(action:, type: Helpers::Constants::ODP_MANAGER_CONFIG[:EVENT_TYPE], identifiers: {}, data: {})
@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 Expand Up @@ -1126,5 +1167,51 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl
def project_config
@config_manager.config
end

def update_odp_config_on_datafile_update
# if datafile isn't ready, expects to be called again by the notification_center
return if @config_manager.respond_to?(:ready?) && !@config_manager.ready?

config = @config_manager&.config
return unless config

@odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
end

def setup_odp!
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

return if @sdk_settings.odp_disabled

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

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

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

return if @sdk_settings.odp_segment_manager

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

@sdk_settings.odp_segments_cache ||= LRUCache.new(
@sdk_settings.segments_cache_size,
@sdk_settings.segments_cache_timeout_in_secs
)
end
end
end
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
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved
#
# 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_accessor :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
44 changes: 44 additions & 0 deletions lib/optimizely/helpers/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,50 @@ def odp_data_types_valid?(data)
valid_types = [String, Float, Integer, TrueClass, FalseClass, NilClass]
data.values.all? { |e| valid_types.member? e.class }
end

def segments_cache_valid?(segments_cache)
# Determines if a given segments_cache is valid.
#
# segments_cache - custom cache to be validated.
#
# Returns boolean depending on whether cache has required methods.
(
segments_cache.respond_to?(:reset) &&
segments_cache.method(:reset)&.parameters&.empty? &&
segments_cache.respond_to?(:lookup) &&
segments_cache.method(:lookup)&.parameters&.length&.positive? &&
segments_cache.respond_to?(:save) &&
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
16 changes: 0 additions & 16 deletions lib/optimizely/odp/odp_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ def api_host
@mutex.synchronize { @api_host.clone }
end

# Returns the api host for odp connections
#
# @return - The api host.

def api_host=(api_host)
@mutex.synchronize { @api_host = api_host.clone }
end

# Returns the api key for odp connections
#
# @return - The api key.
Expand All @@ -83,14 +75,6 @@ def api_key
@mutex.synchronize { @api_key.clone }
end

# Replace the api key with the provided string
#
# @param api_key - An api key

def api_key=(api_key)
@mutex.synchronize { @api_key = api_key.clone }
end

# Returns An array of qualified segments for this user
#
# @return - An array of segments names.
Expand Down
9 changes: 5 additions & 4 deletions lib/optimizely/odp/odp_event_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative 'zaius_rest_api_manager'
require_relative 'odp_events_api_manager'
require_relative '../helpers/constants'
require_relative 'odp_event'

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

attr_reader :batch_size, :zaius_manager, :logger
attr_reader :batch_size, :api_manager, :logger
attr_accessor :odp_config

def initialize(
Expand All @@ -46,7 +47,7 @@ def initialize(
# received signal should be sent after adding item to event_queue
@received = ConditionVariable.new
@logger = logger
@zaius_manager = api_manager || ZaiusRestApiManager.new(logger: @logger, proxy_config: proxy_config)
@api_manager = api_manager || OdpEventsApiManager.new(logger: @logger, proxy_config: proxy_config)
@batch_size = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_BATCH_SIZE]
@flush_interval = Helpers::Constants::ODP_EVENT_MANAGER[:DEFAULT_FLUSH_INTERVAL_SECONDS]
@flush_deadline = 0
Expand Down Expand Up @@ -232,7 +233,7 @@ def flush_batch!
i = 0
while i < @retry_count
begin
should_retry = @zaius_manager.send_odp_events(@api_key, @api_host, @current_batch)
should_retry = @api_manager.send_odp_events(@api_key, @api_host, @current_batch)
rescue StandardError => e
should_retry = false
@logger.log(Logger::ERROR, format(Helpers::Constants::ODP_LOGS[:ODP_EVENT_FAILED], "Error: #{e.message} #{@current_batch.to_json}"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
require 'json'

module Optimizely
class ZaiusRestApiManager
class OdpEventsApiManager
# Interface that handles sending ODP events.

def initialize(logger: nil, proxy_config: nil)
Expand Down
10 changes: 5 additions & 5 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)
andrewleap-optimizely marked this conversation as resolved.
Show resolved Hide resolved
@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 close!
def stop!
return unless @enabled

@event_manager.stop!
@event_manager.stop! if @event_manager.respond_to?(:stop!)
end
end
end
8 changes: 4 additions & 4 deletions lib/optimizely/odp/odp_segment_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@
#

require 'optimizely/logger'
require_relative 'zaius_graphql_api_manager'
require_relative 'odp_segments_api_manager'

module Optimizely
class OdpSegmentManager
# Schedules connections to ODP for audience segmentation and caches the results
attr_accessor :odp_config
attr_reader :segments_cache, :zaius_manager, :logger
attr_reader :segments_cache, :api_manager, :logger

def initialize(segments_cache, api_manager = nil, logger = nil, proxy_config = nil)
@odp_config = nil
@logger = logger || NoOpLogger.new
@zaius_manager = api_manager || ZaiusGraphQLApiManager.new(logger: @logger, proxy_config: proxy_config)
@api_manager = api_manager || OdpSegmentsApiManager.new(logger: @logger, proxy_config: proxy_config)
@segments_cache = segments_cache
end

Expand Down Expand Up @@ -72,7 +72,7 @@ def fetch_qualified_segments(user_key, user_value, options)

@logger.log(Logger::DEBUG, 'Making a call to ODP server.')

segments = @zaius_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check)
segments = @api_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check)
@segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache
segments
end
Expand Down
Loading