8000 feat: add odp segment manager by andrewleap-optimizely · Pull Request #310 · optimizely/ruby-sdk · GitHub
[go: up one dir, main page]

Skip to content

feat: add odp segment manager #310

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 5 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
122 changes: 122 additions & 0 deletions lib/optimizely/odp/odp_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# frozen_string_literal: true

#
# Copyright 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 'optimizely/logger'

module Optimizely
class OdpConfig
# Contains configuration used for ODP integration.
#
# @param api_host - The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile.
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile.
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
# @param odp_enabled - A boolean value indicating whether odp is enabled for the project or not.
def initialize(api_key = nil, api_host = nil, segments_to_check = [])
@api_key = api_key
@api_host = api_host
@segments_to_check = segments_to_check
@odp_enabled = !api_key.nil? && !api_host.nil? ? true : false
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll have a similar flag (odpEnabled) in the OdpManager level, which disable all ODP functions including segments and events. This may be conflict with that. We can clean it up here and visit again at the top level.

@mutex = Mutex.new
end

# Replaces the existing configuration
#
# @param api_host - The host URL for the ODP audience segments API (optional). If not provided, SDK will use the default host in datafile.
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional). If not provided, SDK will use the default publicKey in datafile.
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
#
# @return - True if the provided values were different than the existing values.

def update(api_key = nil, api_host = nil, segments_to_check = [])
@mutex.synchronize do
@odp_enabled = !api_key.nil? && !api_host.nil? ? true : false

return false if @api_key == api_key && @api_host == api_host && @segments_to_check == segments_to_check

@api_key = api_key
@api_host = api_host
@segments_to_check = segments_to_check
true
end
end

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

< 8000 /td>
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.

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.

def segments_to_check
@mutex.synchronize { @segments_to_check.clone }
end

# Replace qualified segments with provided segments
#
# @param segments - An array of segment names

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

# Returns True if odp is enabled and ready
#
# @return - bool

def odp_enabled
@mutex.synchronize { @odp_enabled.clone }
end

# Enable/disable the odp integration
#
# @param odp_enabled - bool

def odp_enabled=(odp_enabled)
@mutex.synchronize { @odp_enabled = odp_enabled.clone }
end
end
end
120 changes: 120 additions & 0 deletions lib/optimizely/odp/odp_segment_manager.rb
10000
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

#
# Copyright 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 'optimizely/logger'
require_relative 'zaius_graphql_api_manager'

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

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

def fetch_qualified_segments(segment_request)
odp_api_key = @odp_config&.api_key
odp_api_host = @odp_config&.api_host

unless odp_api_host && odp_api_key
@logger.log(Logger::ERROR, 'api_key/api_host not defined')
segment_request.segments = nil
return
end
segments_to_check = @odp_config&.segments_to_check

unless segments_to_check&.size&.positive?
segment_request.segments = []
return
end

cache_key = make_cache_key(segment_request.user_key, segment_request.user_value)

ignore_cache = segment_request.options.include?(OptimizelySegmentOption::IGNORE_CACHE)
reset_cache = segment_request.options.include?(OptimizelySegmentOption::RESET_CACHE)

reset if reset_cache

unless ignore_cache || reset_cache
segments = @segments_cache.lookup(cache_key)
unless segments.nil?
segment_request.segments = segments
return
end
8000 end

Thread.new do
segments = @zaius_manager.fetch_segments(odp_api_key, odp_api_host, segment_request.user_key, segment_request.user_value, segments_to_check)
@segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache
segment_request.segments = segments
end

nil
end

def reset
@segments_cache.reset
nil
end

private

def make_cache_key(user_key, user_value)
"#{user_key}-$-#{user_value}"
end
end

class OptimizelySegmentOption
IGNORE_CACHE = :IGNORE_CACHE
RESET_CACHE = :RESET_CACHE
end

class OdpSegmentRequest
# Allows asynchronous communication between OptimizelyUserContext and OdpSegmentManger
attr_reader :user_key, :user_value, :options

def initialize(user_key, user_value, options)
@user_key = user_key
@user_value = user_value
@options = options
@queue = Thread::SizedQueue.new(1)
@segments = nil
end

# If this method is called without a corresponding call to segments=, it will wait indefinitely
def wait_for_segments
return @segments if @queue.closed?

@segments = @queue.pop
@queue.close
@segments
end

def segments=(segments)
if @queue.closed?
@segments = segments
return
end
@queue.push(segments, non_block: true)
end
end
end
16 changes: 8 additions & 8 deletions lib/optimizely/odp/zaius_graphql_api_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
require 'json'

module Optimizely
class ZaiusGraphQlApiManager
class ZaiusGraphQLApiManager
# Interface that handles fetching audience segments.

def initialize(logger: nil, proxy_config: nil)
Expand Down Expand Up @@ -52,23 +52,23 @@ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
rescue SocketError, Timeout::Error, Net::ProtocolError, Errno::ECONNRESET => e
@logger.log(Logger::DEBUG, "GraphQL download failed: #{e}")
log_failure('network error')
return []
return nil
rescue Errno::EINVAL, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
log_failure(e)
return []
return nil
end

status = response.code.to_i
if status >= 400
log_failure(status)
return []
return nil
end

begin
response = JSON.parse(response.body)
rescue JSON::ParserError
log_failure('JSON decode error')
return []
return nil
end

if response.include?('errors')
Expand All @@ -78,21 +78,21 @@ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
else
log_failure(error_class)
end
return []
return nil
end

audiences = response.dig('data', 'customer', 'audiences', 'edges')
unless audiences
log_failure('decode error')
return []
return nil
end

audiences.filter_map do |edge|
name = edge.dig('node', 'name')
state = edge.dig('node', 'state')
unless name && state
log_failure('decode error')
return []
return nil
end
state == 'qualified' ? name : nil
end
Expand Down
Loading
0