From: Imre Farkas ifarkas@redhat.com
--- src/lib/provider_selection.rb | 20 ++++ src/lib/provider_selection/base.rb | 121 +++++++++++++++++++++++ src/lib/provider_selection/chainable_strategy.rb | 44 +++++++++ src/lib/provider_selection/match.rb | 50 ++++++++++ src/lib/provider_selection/priority_group.rb | 71 +++++++++++++ src/lib/provider_selection/rank.rb | 54 ++++++++++ src/lib/provider_selection/strategy.rb | 35 +++++++ 7 files changed, 395 insertions(+) create mode 100644 src/lib/provider_selection.rb create mode 100644 src/lib/provider_selection/base.rb create mode 100644 src/lib/provider_selection/chainable_strategy.rb create mode 100644 src/lib/provider_selection/match.rb create mode 100644 src/lib/provider_selection/priority_group.rb create mode 100644 src/lib/provider_selection/rank.rb create mode 100644 src/lib/provider_selection/strategy.rb
diff --git a/src/lib/provider_selection.rb b/src/lib/provider_selection.rb new file mode 100644 index 0000000..375c9dc --- /dev/null +++ b/src/lib/provider_selection.rb @@ -0,0 +1,20 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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 files in lib dir +['match', 'priority_group', 'rank', 'strategy', 'base', 'chainable_strategy'].each do |file_name| + require File.join(File.dirname(__FILE__), 'provider_selection', file_name) +end diff --git a/src/lib/provider_selection/base.rb b/src/lib/provider_selection/base.rb new file mode 100644 index 0000000..84a42fa --- /dev/null +++ b/src/lib/provider_selection/base.rb @@ -0,0 +1,121 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + class Base + + @strategies = [] + @view_paths = [] + + class << self + attr_reader :strategies + attr_reader :view_paths + + def register_strategy(name, lib_path, options = {}) + # TODO: this require call should be revised in the future considering security aspects + require lib_path + new_klass = "provider_selection/strategies/#{name.to_s}".classify.constantize + @strategies << Strategy.new(name, new_klass) + end + + def find_strategy_by_name(name) + @strategies.detect{ |strategy| strategy.name == name.to_s } + end + + def add_view_path(view_path) + @view_paths << view_path + end + end + + attr_reader :instances + + def initialize(instances) + @instances = instances + @strategy_chain = self + end + + def calculate + pool = @instances.first.pool + rank = Rank.new(pool) + + provider_accounts = find_common_provider_accounts.map do |provider_account| + provider_account if provider_account.quota.can_start?(@instances) + end.uniq.compact + + # Adding a priority of 100000 to the default priority group which contains + # all the available provider accounts. + # The user defined priority groups has a score between -100 and +100. + default_priority_group = PriorityGroup.new(100000) + rank.default_priority_group = default_priority_group + + provider_accounts.uniq.each do |provider_account| + # Rescale to provider account priority to the [-100, 100] interval + score = provider_account.priority + if score.present? && score > Match::UPPER_LIMIT + score = Match::UPPER_LIMIT + elsif score.present? && score < Match::LOWER_LIMIT + score = Match::LOWER_LIMIT + end + + default_priority_group.matches << Match.new(provider_account, score) + end + + rank + end + + def next_match + rank = @strategy_chain.calculate + rank.ordered_priority_groups.each do |priority_group| + random_match = priority_group.get_random_match + return random_match if random_match.present? + end + + nil + end + + def chain_strategy(name, options = {}) + @strategy = self.class.find_strategy_by_name(name) + @strategy_chain = @strategy.klass.new(@strategy_chain, options) + end + + private + + def find_common_provider_accounts + instance_matches_grouped_by_instances = @instances.map do |instance| + matches, errors = instance.matches + matches + end + + result = [] + if instance_matches_grouped_by_instances.any? + result += instance_matches_grouped_by_instances.first.map(&:provider_account) + end + + instance_matches_grouped_by_instances.each_with_index do |instance_matches, index| + if index == 0 + result += instance_matches.map(&:provider_account) + else + result &= instance_matches.map(&:provider_account) + end + end + + result + end + + end + +end diff --git a/src/lib/provider_selection/chainable_strategy.rb b/src/lib/provider_selection/chainable_strategy.rb new file mode 100644 index 0000000..9f3b974 --- /dev/null +++ b/src/lib/provider_selection/chainable_strategy.rb @@ -0,0 +1,44 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + module ChainableStrategy + + module InstanceMethods + + def initialize(strategies, options) + @strategies = strategies + @options = options + end + + def method_missing(method, *args) + args.empty? ? @strategies.send(method) : @strategies.send(method, args) + end + + end + + module ClassMethods + + def properties + @properties || {} + end + + end + + end + +end diff --git a/src/lib/provider_selection/match.rb b/src/lib/provider_selection/match.rb new file mode 100644 index 0000000..485bc3a --- /dev/null +++ b/src/lib/provider_selection/match.rb @@ -0,0 +1,50 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + class Match + + UPPER_LIMIT = 100 + LOWER_LIMIT = -100 + + attr_reader :score + attr_accessor :provider_account + + def initialize(provider_account, score = nil) + @provider_account = provider_account + @score = score + end + + def score=(val) + raise ArgumentError.new("score cannot be higher than #{UPPER_LIMIT}") if val > UPPER_LIMIT + raise ArgumentError.new("score cannot be lower than #{LOWER_LIMIT}") if val < LOWER_LIMIT + + @score = val + end + + # nil values should value more then any other value + def calculated_score + if @score.nil? + UPPER_LIMIT + 1 + else + @score + end + end + + end + +end \ No newline at end of file diff --git a/src/lib/provider_selection/priority_group.rb b/src/lib/provider_selection/priority_group.rb new file mode 100644 index 0000000..c2a0c0d --- /dev/null +++ b/src/lib/provider_selection/priority_group.rb @@ -0,0 +1,71 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + class PriorityGroup + + attr_accessor :matches + attr_accessor :score + + def initialize(score = nil) + @matches = [] + @score = score + end + + def self.create_from_active_record(obj, allowed_matches) + priority_group = PriorityGroup.new(obj.score) + + possible_provider_accounts = obj.all_provider_accounts + allowed_provider_accounts = allowed_matches.map do |match| + match.provider_account + end.uniq + + possible_provider_accounts &= allowed_provider_accounts + + return nil if possible_provider_accounts.empty? + + possible_provider_accounts.each do |provider_account| + priority_group.matches << Match.new(provider_account) + end + + priority_group + end + + def get_random_match + match_sum_score = @matches.sum do |match| + (Match::UPPER_LIMIT + 1) - match.calculated_score + end + + # In order to work with 'nil' scores adding the size of the @matches array + match_sum_score += @matches.length + random_value = rand(match_sum_score) + + sum = match_sum_score + matches.each do |match| + difference = (Match::UPPER_LIMIT + 1) - match.calculated_score + sum -= (difference + 1) + if random_value >= sum + return match + end + end + + nil + end + + end + +end diff --git a/src/lib/provider_selection/rank.rb b/src/lib/provider_selection/rank.rb new file mode 100644 index 0000000..7479ddf --- /dev/null +++ b/src/lib/provider_selection/rank.rb @@ -0,0 +1,54 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + class Rank + + attr_accessor :priority_groups + attr_reader :pool + + def initialize(pool) + @priority_groups = [] + @pool = pool + end + + def ordered_priority_groups + @priority_groups.sort do |a, b| + if a.score.nil? && b.score.nil? + 0 + elsif a.score.nil? + 1 + elsif b.score.nil? + -1 + else + a.score <=> b.score + end + end + end + + def default_priority_group + @default_priority_group + end + + def default_priority_group=(val) + @default_priority_group = val + priority_groups << val + end + + end + +end \ No newline at end of file diff --git a/src/lib/provider_selection/strategy.rb b/src/lib/provider_selection/strategy.rb new file mode 100644 index 0000000..2706f92 --- /dev/null +++ b/src/lib/provider_selection/strategy.rb @@ -0,0 +1,35 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# 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. +# + +module ProviderSelection + + class Strategy + + attr_reader :name + attr_reader :klass + + def initialize(name, klass) + @name = name + @klass = klass + end + + def translated_name + I18n.t("strategies.#{@name}.name") + end + + end + +end \ No newline at end of file