lib/split/experiment.rb in split-0.4.6 vs lib/split/experiment.rb in split-0.5.0
- old
+ new
@@ -1,23 +1,55 @@
module Split
class Experiment
attr_accessor :name
+ attr_writer :algorithm
+ attr_accessor :resettable
- def initialize(name, *alternative_names)
- @name = name.to_s
- @alternatives = alternative_names.map do |alternative|
- Split::Alternative.new(alternative, name)
- end
+ def initialize(name, options = {})
+ options = {
+ :resettable => true,
+ }.merge(options)
+
+ @name = name.to_s
+ @alternatives = options[:alternatives] if !options[:alternatives].nil?
+
+ if !options[:algorithm].nil?
+ @algorithm = options[:algorithm].is_a?(String) ? options[:algorithm].constantize : options[:algorithm]
+ end
+
+ if !options[:resettable].nil?
+ @resettable = options[:resettable].is_a?(String) ? options[:resettable] == 'true' : options[:resettable]
+ end
+
+ if !options[:alternative_names].nil?
+ @alternatives = options[:alternative_names].map do |alternative|
+ Split::Alternative.new(alternative, name)
+ end
+ end
+
+
end
+
+ def algorithm
+ @algorithm ||= Split.configuration.algorithm
+ end
+ def ==(obj)
+ self.name == obj.name
+ end
+
def winner
if w = Split.redis.hget(:experiment_winner, name)
Split::Alternative.new(w, name)
else
nil
end
end
+
+ def participant_count
+ alternatives.inject(0){|sum,a| sum + a.participant_count}
+ end
def control
alternatives.first
end
@@ -31,10 +63,14 @@
def start_time
t = Split.redis.hget(:experiment_start_times, @name)
Time.parse(t) if t
end
+
+ def [](name)
+ alternatives.find{|a| a.name == name}
+ end
def alternatives
@alternatives.dup
end
@@ -45,18 +81,14 @@
def next_alternative
winner || random_alternative
end
def random_alternative
- weights = alternatives.map(&:weight)
-
- total = weights.inject(:+)
- point = rand * total
-
- alternatives.zip(weights).each do |n,w|
- return n if w >= point
- point -= w
+ if alternatives.length > 1
+ Split.configuration.algorithm.choose_alternative(self)
+ else
+ alternatives.first
end
end
def version
@version ||= (Split.redis.get("#{name.to_s}:version").to_i || 0)
@@ -76,10 +108,14 @@
def finished_key
"#{key}:finished"
end
+ def resettable?
+ resettable
+ end
+
def reset
alternatives.each(&:reset)
reset_winner
increment_version
end
@@ -103,13 +139,35 @@
@alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) }
else
Split.redis.del(name)
@alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) }
end
+ config_key = Split::Experiment.experiment_config_key(name)
+ Split.redis.hset(config_key, :resettable, resettable)
+ Split.redis.hset(config_key, :algorithm, algorithm.to_s)
+ self
end
def self.load_alternatives_for(name)
+ if Split.configuration.experiment_for(name)
+ load_alternatives_from_configuration_for(name)
+ else
+ load_alternatives_from_redis_for(name)
+ end
+ end
+
+ def self.load_alternatives_from_configuration_for(name)
+ alts = Split.configuration.experiment_for(name)[:alternatives]
+ raise ArgumentError, "Experiment configuration is missing :alternatives array" if alts.nil?
+ if alts.is_a?(Hash)
+ alts.keys
+ else
+ alts.flatten
+ end
+ end
+
+ def self.load_alternatives_from_redis_for(name)
case Split.redis.type(name)
when 'set' # convert legacy sets to lists
alts = Split.redis.smembers(name)
Split.redis.del(name)
alts.reverse.each {|a| Split.redis.lpush(name, a) }
@@ -117,59 +175,89 @@
else
Split.redis.lrange(name, 0, -1)
end
end
+ def self.load_from_configuration(name)
+ exp_config = Split.configuration.experiment_for(name) || {}
+ self.new(name, :alternative_names => load_alternatives_for(name),
+ :resettable => exp_config[:resettable],
+ :algorithm => exp_config[:algorithm])
+ end
+
+ def self.load_from_redis(name)
+ exp_config = Split.redis.hgetall(experiment_config_key(name))
+ self.new(name, :alternative_names => load_alternatives_for(name),
+ :resettable => exp_config['resettable'],
+ :algorithm => exp_config['algorithm'])
+ end
+
+ def self.experiment_config_key(name)
+ "experiment_configurations/#{name}"
+ end
+
def self.all
- Array(Split.redis.smembers(:experiments)).map {|e| find(e)}
+ Array(all_experiment_names_from_redis + all_experiment_names_from_configuration).map {|e| find(e)}
end
+ def self.all_experiment_names_from_redis
+ Split.redis.smembers(:experiments)
+ end
+
+ def self.all_experiment_names_from_configuration
+ Split.configuration.experiments ? Split.configuration.experiments.keys : []
+ end
+
+
def self.find(name)
- if Split.redis.exists(name)
- self.new(name, *load_alternatives_for(name))
+ if Split.configuration.experiment_for(name)
+ obj = load_from_configuration(name)
+ elsif Split.redis.exists(name)
+ obj = load_from_redis(name)
+ else
+ obj = nil
end
+ obj
end
def self.find_or_create(key, *alternatives)
name = key.to_s.split(':')[0]
if alternatives.length == 1
if alternatives[0].is_a? Hash
alternatives = alternatives[0].map{|k,v| {k => v} }
- else
- raise InvalidArgument, 'You must declare at least 2 alternatives'
end
end
alts = initialize_alternatives(alternatives, name)
if Split.redis.exists(name)
existing_alternatives = load_alternatives_for(name)
if existing_alternatives == alts.map(&:name)
- experiment = self.new(name, *alternatives)
+ experiment = self.new(name, :alternative_names => alternatives)
else
- exp = self.new(name, *existing_alternatives)
+ exp = self.new(name, :alternative_names => existing_alternatives)
exp.reset
exp.alternatives.each(&:delete)
- experiment = self.new(name, *alternatives)
+ experiment = self.new(name, :alternative_names =>alternatives)
experiment.save
end
else
- experiment = self.new(name, *alternatives)
+ experiment = self.new(name, :alternative_names => alternatives)
experiment.save
end
return experiment
end
def self.initialize_alternatives(alternatives, name)
unless alternatives.all? { |a| Split::Alternative.valid?(a) }
- raise InvalidArgument, 'Alternatives must be strings'
+ raise ArgumentError, 'Alternatives must be strings'
end
alternatives.map do |alternative|
Split::Alternative.new(alternative, name)
end
end
end
-end
\ No newline at end of file
+end