Skip to content

feat: add notification center registry #323

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
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 33 additions & 31 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2016-2022, Optimizely and contributors
# Copyright 2016-2023, 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.
Expand Down Expand Up @@ -36,6 +36,7 @@
require_relative 'optimizely/helpers/variable_type'
require_relative 'optimizely/logger'
require_relative 'optimizely/notification_center'
require_relative 'optimizely/notification_center_registry'
require_relative 'optimizely/optimizely_config'
require_relative 'optimizely/optimizely_user_context'
require_relative 'optimizely/odp/lru_cache'
Expand Down Expand Up @@ -105,19 +106,7 @@ def initialize( # rubocop:disable Metrics/ParameterLists

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

setup_odp!

@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,
fetch_segments_timeout: @sdk_settings.fetch_segments_timeout,
odp_event_timeout: @sdk_settings.odp_event_timeout,
logger: @logger
)

@config_manager = if config_manager.respond_to?(:config)
@config_manager = if config_manager.respond_to?(:config) && config_manager.respond_to?(:sdk_key)
config_manager
elsif sdk_key
HTTPProjectConfigManager.new(
Expand All @@ -132,9 +121,7 @@ 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
setup_odp!(@config_manager.sdk_key)

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

Expand Down Expand Up @@ -1171,7 +1158,7 @@ def project_config
end

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

config = @config_manager&.config
Expand All @@ -1180,19 +1167,12 @@ def update_odp_config_on_datafile_update
@odp_manager.update_odp_config(config.public_key_for_odp, config.host_for_odp, config.all_segments)
end

def setup_odp!
def setup_odp!(sdk_key)
unless @sdk_settings.is_a? Optimizely::Helpers::OptimizelySdkSettings
@logger.log(Logger::DEBUG, 'Provided sdk_settings is not an OptimizelySdkSettings instance.') unless @sdk_settings.nil?
@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
Expand All @@ -1203,17 +1183,39 @@ def setup_odp!
@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
# no need to instantiate a cache if a custom cache or segment manager is provided.
if !@sdk_settings.odp_disabled && @sdk_settings.odp_segment_manager.nil?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd suggest commenting this same as you did in Py:
"no need to instantiate a cache if a custom cache or segment manager is provided."

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

@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,
fetch_segments_timeout: @sdk_settings.fetch_segments_timeout,
odp_event_timeout: @sdk_settings.odp_event_timeout,
logger: @logger
)

return if @sdk_settings.odp_disabled

Optimizely::NotificationCenterRegistry
.get_notification_center(sdk_key, @logger)
&.add_notification_listener(
NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE],
method(:update_odp_config_on_datafile_update)
)

update_odp_config_on_datafile_update
end
end
end
16 changes: 12 additions & 4 deletions lib/optimizely/config_manager/http_project_config_manager.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019-2020, 2022, Optimizely and contributors
# Copyright 2019-2020, 2022-2023, 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.
Expand Down Expand Up @@ -33,12 +33,12 @@ module Optimizely
class HTTPProjectConfigManager < ProjectConfigManager
# Config manager that polls for the datafile and updated ProjectConfig based on an update interval.

attr_reader :stopped
attr_reader :stopped, :sdk_key

# Initialize config manager. One of sdk_key or url has to be set to be able to use.
#
# sdk_key - Optional string uniquely identifying the datafile. It's required unless a URL is passed in.
# datafile: Optional JSON string representing the project.
# sdk_key - Optional string uniquely identifying the datafile. It's required unless a datafile with sdk_key is passed in.
# datafile - Optional JSON string representing the project. If nil, sdk_key is required.
# polling_interval - Optional floating point number representing time interval in seconds
# at which to request datafile and set ProjectConfig.
# blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized.
Expand Down Expand Up @@ -83,6 +83,10 @@ def initialize(
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
@optimizely_config = nil
@config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
@sdk_key = sdk_key || @config&.sdk_key

raise MissingSdkKeyError if @sdk_key.nil?

@mutex = Mutex.new
@resource = ConditionVariable.new
@async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
Expand Down Expand Up @@ -222,6 +226,10 @@ def set_config(config)

@notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])

NotificationCenterRegistry
.get_notification_center(@sdk_key, @logger)
&.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])

@logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \
"Old revision number: #{previous_revision}. New revision number: #{@config.revision}.")
end
Expand Down
3 changes: 2 additions & 1 deletion lib/optimizely/config_manager/project_config_manager.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019, Optimizely and contributors
# Copyright 2019, 2023, 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.
Expand All @@ -20,5 +20,6 @@ class ProjectConfigManager
# Interface for fetching ProjectConfig instance.

def config; end
def sdk_key; end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019-2020, 2022, Optimizely and contributors
# Copyright 2019-2020, 2022-2023, 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.
Expand All @@ -23,7 +23,7 @@
module Optimizely
class StaticProjectConfigManager < ProjectConfigManager
# Implementation of ProjectConfigManager interface.
attr_reader :config
attr_reader :config, :sdk_key

def initialize(datafile, logger, error_handler, skip_json_validation)
# Looks up and sets datafile and config based on response body.
Expand All @@ -41,6 +41,7 @@ def initialize(datafile, logger, error_handler, skip_json_validation)
error_handler,
skip_json_validation
)
@sdk_key = @config&.sdk_key
@optimizely_config = nil
end

Expand Down
9 changes: 8 additions & 1 deletion lib/optimizely/exceptions.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2016-2020, 2022, Optimizely and contributors
# Copyright 2016-2020, 2022-2023, 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.
Expand Down Expand Up @@ -32,6 +32,13 @@ def initialize(msg = 'Provided URI was invalid.')
end
end

class MissingSdkKeyError < Error
# Raised when a provided URI is invalid.
def initialize(msg = 'SDK key not provided/cannot be found in the datafile.')
super
end
end

class InvalidAudienceError < Error
# Raised when an invalid audience is provided

Expand Down
71 changes: 71 additions & 0 deletions lib/optimizely/notification_center_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

#
# Copyright 2023, 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 'notification_center'
require_relative 'exceptions'

module Optimizely
class NotificationCenterRegistry
private_class_method :new
# Class managing internal notification centers.
# @api no-doc
@notification_centers = {}
@mutex = Mutex.new

# Returns an internal notification center for the given sdk_key, creating one
# if none exists yet.
#
# Args:
# sdk_key: A string sdk key to uniquely identify the notification center.
# logger: Optional logger.

# Returns:
# nil or NotificationCenter
def self.get_notification_center(sdk_key, logger)
unless sdk_key
logger&.log(Logger::ERROR, "#{MissingSdkKeyError.new.message} ODP may not work properly without it.")
return nil
end

notification_center = nil

@mutex.synchronize do
if @notification_centers.key?(sdk_key)
notification_center = @notification_centers[sdk_key]
else
notification_center = NotificationCenter.new(logger, nil)
@notification_centers[sdk_key] = notification_center
end
end

notification_center
end

# Remove a previously added notification center and clear all its listeners.

# Args:
# sdk_key: The sdk_key of the notification center to remove.
def self.remove_notification_center(sdk_key)
@mutex.synchronize do
@notification_centers
.delete(sdk_key)
&.clear_all_notification_listeners
end
nil
end
end
end
4 changes: 3 additions & 1 deletion spec/config_manager/http_project_config_manager_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

#
# Copyright 2019-2020, 2022, Optimizely and contributors
# Copyright 2019-2020, 2022-2023, 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.
Expand Down Expand Up @@ -57,6 +57,7 @@
describe '.project_config_manager' do
it 'should get project config when valid url is given' do
@http_project_config_manager = Optimizely::HTTPProjectConfigManager.new(
sdk_key: 'valid_sdk_key',
url: 'https://cdn.optimizely.com/datafiles/valid_sdk_key.json'
)

Expand All @@ -75,6 +76,7 @@
.to_return(status: 200, body: VALID_SDK_KEY_CONFIG_JSON, headers: {})

@http_project_config_manager = Optimizely::HTTPProjectConfigManager.new(
sdk_key: 'valid_sdk_key',
url: 'http://cdn.optimizely.com/datafiles/valid_sdk_key.json'
)

Expand Down
Loading