class Optimizely::HTTPProjectConfigManager

Attributes

optimizely_config[R]

Config manager that polls for the datafile and updated ProjectConfig based on an update interval.

stopped[R]

Config manager that polls for the datafile and updated ProjectConfig based on an update interval.

Public Class Methods

new( sdk_key: nil, url: nil, url_template: nil, polling_interval: nil, blocking_timeout: nil, auto_update: true, start_by_default: true, datafile: nil, logger: nil, error_handler: nil, skip_json_validation: false, notification_center: nil, datafile_access_token: nil, proxy_config: nil ) click to toggle source

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. 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. auto_update - Boolean indicates to run infinitely or only once. start_by_default - Boolean indicates to start by default AsyncScheduler. url - Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key. url_template - Optional string template which in conjunction with sdk_key

determines URL from where to fetch the datafile.

logger - Provides a logger instance. error_handler - Provides a handle_error method to handle exceptions. skip_json_validation - Optional boolean param which allows skipping JSON schema

validation upon object invocation. By default JSON schema validation will be performed.

datafile_access_token - access token used to fetch private datafiles proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.

# File lib/optimizely/config_manager/http_project_config_manager.rb, line 56
def initialize(
  sdk_key: nil,
  url: nil,
  url_template: nil,
  polling_interval: nil,
  blocking_timeout: nil,
  auto_update: true,
  start_by_default: true,
  datafile: nil,
  logger: nil,
  error_handler: nil,
  skip_json_validation: false,
  notification_center: nil,
  datafile_access_token: nil,
  proxy_config: nil
)
  @logger = logger || NoOpLogger.new
  @error_handler = error_handler || NoOpErrorHandler.new
  @access_token = datafile_access_token
  @datafile_url = get_datafile_url(sdk_key, url, url_template)
  @polling_interval = nil
  polling_interval(polling_interval)
  @blocking_timeout = nil
  blocking_timeout(blocking_timeout)
  @last_modified = nil
  @skip_json_validation = skip_json_validation
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
  @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
  @optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
  @mutex = Mutex.new
  @resource = ConditionVariable.new
  @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
  # Start async scheduler in the end to avoid race condition where scheduler executes
  # callback which makes use of variables not yet initialized by the main thread.
  @async_scheduler.start! if start_by_default == true
  @proxy_config = proxy_config
  @stopped = false
end

Public Instance Methods

config() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 120
def config
  # Get Project Config.

  # if stopped is true, then simply return @config.
  # If the background datafile polling thread is running. and config has been initalized,
  # we simply return @config.
  # If it is not, we wait and block maximum for @blocking_timeout.
  # If thread is not running, we fetch the datafile and update config.
  return @config if @stopped

  if @async_scheduler.running
    return @config if ready?

    @mutex.synchronize do
      @resource.wait(@mutex, @blocking_timeout)
      return @config
    end
  end

  fetch_datafile_config
  @config
end
ready?() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 95
def ready?
  !@config.nil?
end
start!() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 99
def start!
  if @stopped
    @logger.log(Logger::WARN, 'Not starting. Already stopped.')
    return
  end

  @async_scheduler.start!
  @stopped = false
end
stop!() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 109
def stop!
  if @stopped
    @logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
    return
  end

  @async_scheduler.stop!
  @config = nil
  @stopped = true
end

Private Instance Methods

blocking_timeout(blocking_timeout) click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 257
def blocking_timeout(blocking_timeout)
  # Sets time in seconds to block the config call until config has been initialized.
  #
  # blocking_timeout - Time in seconds to block the config call.

  # If valid set given timeout, default blocking_timeout otherwise.

  if blocking_timeout.nil?
    @logger.log(
      Logger::DEBUG,
      "Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
    )
    @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
    return
  end

  unless blocking_timeout.is_a? Integer
    @logger.log(
      Logger::ERROR,
      "Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
    )
    @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
    return
  end

  unless blocking_timeout.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
    @logger.log(
      Logger::DEBUG,
      "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
    )
    @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
    return
  end

  @blocking_timeout = blocking_timeout
end
fetch_datafile_config() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 145
def fetch_datafile_config
  # Fetch datafile, handle response and send notification on config update.
  config = request_config
  return unless config

  set_config config
end
get_datafile_url(sdk_key, url, url_template) click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 294
def get_datafile_url(sdk_key, url, url_template)
  # Determines URL from where to fetch the datafile.
  # sdk_key - Key uniquely identifying the datafile.
  # url - String representing URL from which to fetch the datafile.
  # url_template - String representing template which is filled in with
  #               SDK key to determine URL from which to fetch the datafile.
  # Returns String representing URL to fetch datafile from.
  if sdk_key.nil? && url.nil?
    error_msg = 'Must provide at least one of sdk_key or url.'
    @logger.log(Logger::ERROR, error_msg)
    @error_handler.handle_error(InvalidInputsError.new(error_msg))
  end

  unless url
    url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
    begin
      return (url_template % sdk_key)
    rescue
      error_msg = "Invalid url_template #{url_template} provided."
      @logger.log(Logger::ERROR, error_msg)
      @error_handler.handle_error(InvalidInputsError.new(error_msg))
    end
  end

  url
end
polling_interval(polling_interval) click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 220
def polling_interval(polling_interval)
  # Sets frequency at which datafile has to be polled and ProjectConfig updated.
  #
  # polling_interval - Time in seconds after which to update datafile.

  # If valid set given polling interval, default update interval otherwise.

  if polling_interval.nil?
    @logger.log(
      Logger::DEBUG,
      "Polling interval is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
    )
    @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
    return
  end

  unless polling_interval.is_a? Numeric
    @logger.log(
      Logger::ERROR,
      "Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
    )
    @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
    return
  end

  unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
    @logger.log(
      Logger::DEBUG,
      "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
    )
    @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
    return
  end

  @polling_interval = polling_interval
end
request_config() click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 153
def request_config
  @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
  headers = {}
  headers['Content-Type'] = 'application/json'
  headers['If-Modified-Since'] = @last_modified if @last_modified
  headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?

  # Cleaning headers before logging to avoid exposing authorization token
  cleansed_headers = {}
  headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
  @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")

  begin
    response = Helpers::HttpUtils.make_request(
      @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
    )
  rescue StandardError => e
    @logger.log(
      Logger::ERROR,
      "Fetching datafile from #{@datafile_url} failed. Error: #{e}"
    )
    return nil
  end

  response_code = response.code.to_i
  @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")

  # Leave datafile and config unchanged if it has not been modified.
  if response.code == '304'
    @logger.log(
      Logger::DEBUG,
      "Not updating config as datafile has not updated since #{@last_modified}."
    )
    return
  end

  if response_code >= 200 && response_code < 400
    @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
    config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
    @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
    @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
  else
    @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
  end

  config
end
set_config(config) click to toggle source
# File lib/optimizely/config_manager/http_project_config_manager.rb, line 201
def set_config(config)
  # Send notification if project config is updated.
  previous_revision = @config.revision if @config
  return if previous_revision == config.revision

  unless ready?
    @config = config
    @mutex.synchronize { @resource.signal }
  end

  @config = config
  @optimizely_config = OptimizelyConfig.new(config).config

  @notification_center.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