bandit 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -1
- data/Rakefile +7 -0
- data/bandit.gemspec +2 -2
- data/lib/bandit.rb +3 -2
- data/lib/bandit/extensions/controller_concerns.rb +23 -28
- data/lib/bandit/extensions/view_concerns.rb +28 -33
- data/lib/bandit/storage/base.rb +7 -6
- data/lib/bandit/storage/dalli.rb +44 -0
- data/lib/bandit/version.rb +1 -1
- data/test/dalli_storage_test.rb +17 -0
- metadata +14 -27
data/README.rdoc
CHANGED
@@ -12,7 +12,7 @@ Then, run the following:
|
|
12
12
|
bundle install
|
13
13
|
rails generate bandit:install
|
14
14
|
|
15
|
-
You can then edit the bandit.yml file in your config directory to set your storage and player parameters. Redis, memcache, and memory storage options are available. Memory storage should only be used for testing.
|
15
|
+
You can then edit the bandit.yml file in your config directory to set your storage and player parameters. Redis, memcache, dalli, and memory storage options are available (you will need to add either the memcache-client, redis, or dalli gem to your Gemfile). Memory storage should only be used for testing.
|
16
16
|
|
17
17
|
See the file players.rdoc for information about available players.
|
18
18
|
|
@@ -84,6 +84,7 @@ To run tests:
|
|
84
84
|
rake test_memory
|
85
85
|
rake test_memcache
|
86
86
|
rake test_redis
|
87
|
+
rake test_dalli
|
87
88
|
|
88
89
|
To produce fake data for the past week, first create an experiment definition. Then, run the following rake task:
|
89
90
|
|
data/Rakefile
CHANGED
@@ -26,6 +26,13 @@ Rake::TestTask.new("test_memcache") { |t|
|
|
26
26
|
t.verbose = true
|
27
27
|
}
|
28
28
|
|
29
|
+
desc "Run all unit tests with dalli storage"
|
30
|
+
Rake::TestTask.new("test_dalli") { |t|
|
31
|
+
t.libs << "lib"
|
32
|
+
t.test_files = FileList['test/dalli_*.rb']
|
33
|
+
t.verbose = true
|
34
|
+
}
|
35
|
+
|
29
36
|
desc "Run all unit tests with redis storage"
|
30
37
|
Rake::TestTask.new("test_redis") { |t|
|
31
38
|
t.libs << "lib"
|
data/bandit.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
|
-
s.add_dependency("rails", ">= 3.0
|
20
|
-
s.
|
19
|
+
s.add_dependency("rails", ">= 3.2.0")
|
20
|
+
s.requirements << "Either redis or memcache gem"
|
21
21
|
s.add_development_dependency('rdoc')
|
22
22
|
end
|
data/lib/bandit.rb
CHANGED
@@ -13,6 +13,7 @@ require "bandit/storage/base"
|
|
13
13
|
require "bandit/storage/memory"
|
14
14
|
require "bandit/storage/memcache"
|
15
15
|
require "bandit/storage/redis"
|
16
|
+
require "bandit/storage/dalli"
|
16
17
|
|
17
18
|
require "bandit/extensions/controller_concerns"
|
18
19
|
require "bandit/extensions/array"
|
@@ -32,7 +33,7 @@ module Bandit
|
|
32
33
|
config.check!
|
33
34
|
# intern keys in storage config
|
34
35
|
config.storage_config = config.storage_config.inject({}) { |n,o| n[o.first.intern] = o.last; n }
|
35
|
-
end
|
36
|
+
end
|
36
37
|
|
37
38
|
def self.storage
|
38
39
|
# try using configured storage at least once every 5 minutes until resolved
|
@@ -53,7 +54,7 @@ module Bandit
|
|
53
54
|
end
|
54
55
|
|
55
56
|
def self.get_experiment(name)
|
56
|
-
exp = Experiment.instances.select { |e| e.name == name }
|
57
|
+
exp = Experiment.instances.select { |e| e.name == name }
|
57
58
|
exp.length > 0 ? exp.first : nil
|
58
59
|
end
|
59
60
|
|
@@ -4,40 +4,35 @@ module Bandit
|
|
4
4
|
module ControllerConcerns
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
-
|
7
|
+
# default convert is a session based conversion
|
8
|
+
def bandit_convert!(exp, alt=nil, count=1)
|
9
|
+
bandit_session_convert!(exp, alt, count)
|
8
10
|
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
12
|
+
# look mum, no cookies
|
13
|
+
def bandit_simple_convert!(exp, alt, count=1)
|
14
|
+
Bandit.get_experiment(exp).convert!(alt, count)
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
# expects a session cookie, deletes it, will convert again
|
18
|
+
def bandit_session_convert!(exp, alt=nil, count=1)
|
19
|
+
cookiename = "bandit_#{exp}".intern
|
20
|
+
cookiename_converted = "bandit_#{exp}_converted".intern
|
21
|
+
alt ||= cookies.signed[cookiename]
|
22
|
+
unless alt.nil? or cookies.signed[cookiename_converted]
|
18
23
|
Bandit.get_experiment(exp).convert!(alt, count)
|
24
|
+
cookies.delete(cookiename)
|
19
25
|
end
|
26
|
+
end
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# creates a _converted cookie, prevents multiple conversions
|
33
|
-
def bandit_sticky_convert!(exp, alt=nil, count=1)
|
34
|
-
cookiename = "bandit_#{exp}".intern
|
35
|
-
cookiename_converted = "bandit_#{exp}_converted".intern
|
36
|
-
alt ||= cookies.signed[cookiename]
|
37
|
-
unless alt.nil? or cookies.signed[cookiename_converted]
|
38
|
-
cookies.permanent.signed[cookiename_converted] = "true"
|
39
|
-
Bandit.get_experiment(exp).convert!(alt, count)
|
40
|
-
end
|
28
|
+
# creates a _converted cookie, prevents multiple conversions
|
29
|
+
def bandit_sticky_convert!(exp, alt=nil, count=1)
|
30
|
+
cookiename = "bandit_#{exp}".intern
|
31
|
+
cookiename_converted = "bandit_#{exp}_converted".intern
|
32
|
+
alt ||= cookies.signed[cookiename]
|
33
|
+
unless alt.nil? or cookies.signed[cookiename_converted]
|
34
|
+
cookies.permanent.signed[cookiename_converted] = "true"
|
35
|
+
Bandit.get_experiment(exp).convert!(alt, count)
|
41
36
|
end
|
42
37
|
end
|
43
38
|
end
|
@@ -4,43 +4,38 @@ module Bandit
|
|
4
4
|
module ViewConcerns
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
-
|
7
|
+
# default choose is a session based choice
|
8
|
+
def bandit_choose(exp)
|
9
|
+
bandit_session_choose(exp)
|
8
10
|
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
# always choose something new and increase the participant count
|
17
|
-
def bandit_simple_choose(exp)
|
18
|
-
Bandit.get_experiment(exp).choose(nil)
|
19
|
-
end
|
12
|
+
# always choose something new and increase the participant count
|
13
|
+
def bandit_simple_choose(exp)
|
14
|
+
Bandit.get_experiment(exp).choose(nil)
|
15
|
+
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
17
|
+
# stick to one alternative for the entire browser session
|
18
|
+
def bandit_session_choose(exp)
|
19
|
+
name = "bandit_#{exp}".intern
|
20
|
+
# choose url param with preference
|
21
|
+
value = params[name].nil? ? cookies.signed[name] : params[name]
|
22
|
+
# choose with default, and set cookie
|
23
|
+
cookies.signed[name] = Bandit.get_experiment(exp).choose(value)
|
24
|
+
end
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
26
|
+
# stick to one alternative until user deletes cookies or changes browser
|
27
|
+
def bandit_sticky_choose(exp)
|
28
|
+
name = "bandit_#{exp}".intern
|
29
|
+
# choose url param with preference
|
30
|
+
value = params[name].nil? ? cookies.signed[name] : params[name]
|
31
|
+
# sticky choice may outlast a given alternative
|
32
|
+
alternative = if Bandit.get_experiment(exp).alternatives.include?(value)
|
33
|
+
value
|
34
|
+
else
|
35
|
+
Bandit.get_experiment(exp).choose(value)
|
36
|
+
end
|
37
|
+
# re-set cookie
|
38
|
+
cookies.permanent.signed[name] = alternative
|
44
39
|
end
|
45
40
|
end
|
46
41
|
end
|
data/lib/bandit/storage/base.rb
CHANGED
@@ -17,9 +17,10 @@ module Bandit
|
|
17
17
|
def self.get_storage(name, config)
|
18
18
|
config ||= {}
|
19
19
|
|
20
|
-
case name
|
20
|
+
case name
|
21
21
|
when :memory then MemoryStorage.new(config)
|
22
22
|
when :memcache then MemCacheStorage.new(config)
|
23
|
+
when :dalli then DalliStorage.new(config)
|
23
24
|
when :redis then RedisStorage.new(config)
|
24
25
|
else raise UnknownStorageEngineError, "#{name} not a known storage method"
|
25
26
|
end
|
@@ -50,17 +51,17 @@ module Bandit
|
|
50
51
|
raise NotImplementedError
|
51
52
|
end
|
52
53
|
|
53
|
-
def incr_participants(experiment, alternative, count=1, date_hour=nil)
|
54
|
+
def incr_participants(experiment, alternative, count=1, date_hour=nil)
|
54
55
|
date_hour ||= DateHour.now
|
55
56
|
|
56
57
|
# initialize first start time for alternative if we haven't inited yet
|
57
58
|
init alt_started_key(experiment, alternative), date_hour.to_i
|
58
|
-
|
59
|
+
|
59
60
|
# increment total count and per hour count
|
60
61
|
incr part_key(experiment, alternative), count
|
61
62
|
incr part_key(experiment, alternative, date_hour), count
|
62
63
|
end
|
63
|
-
|
64
|
+
|
64
65
|
def incr_conversions(experiment, alternative, count=1, date_hour=nil)
|
65
66
|
# increment total count and per hour count
|
66
67
|
incr conv_key(experiment, alternative), count
|
@@ -78,7 +79,7 @@ module Bandit
|
|
78
79
|
def conversion_count(experiment, alternative, date_hour=nil)
|
79
80
|
get conv_key(experiment, alternative, date_hour)
|
80
81
|
end
|
81
|
-
|
82
|
+
|
82
83
|
def player_state_set(experiment, player, name, value)
|
83
84
|
set player_state_key(experiment, player, name), value
|
84
85
|
end
|
@@ -91,7 +92,7 @@ module Bandit
|
|
91
92
|
secs = get alt_started_key(experiment, alternative), nil
|
92
93
|
secs.nil? ? nil : Time.at(secs).to_date_hour
|
93
94
|
end
|
94
|
-
|
95
|
+
|
95
96
|
# if date_hour is nil, create key for total
|
96
97
|
# otherwise, create key for hourly based
|
97
98
|
def part_key(exp, alt, date_hour=nil)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Bandit
|
2
|
+
class DalliStorage < BaseStorage
|
3
|
+
def initialize(config)
|
4
|
+
require 'dalli'
|
5
|
+
config[:namespace] ||= 'bandit'
|
6
|
+
@dalli = Dalli::Client.new(config.fetch(:host, 'localhost:11211'), config)
|
7
|
+
end
|
8
|
+
|
9
|
+
# increment key by count
|
10
|
+
def incr(key, count=1)
|
11
|
+
# dalli incr is broken just like in memcache-client gem
|
12
|
+
with_failure_grace(count) {
|
13
|
+
set(key, get(key, 0) + count)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# initialize key if not set
|
18
|
+
def init(key, value)
|
19
|
+
with_failure_grace(value) {
|
20
|
+
@dalli.add(key, value)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# get key if exists, otherwise 0
|
25
|
+
def get(key, default=0)
|
26
|
+
with_failure_grace(default) {
|
27
|
+
@dalli.get(key) || default
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# set key with value, regardless of whether it is set or not
|
32
|
+
def set(key, value)
|
33
|
+
with_failure_grace(value) {
|
34
|
+
@dalli.set(key, value)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear!
|
39
|
+
with_failure_grace(nil) {
|
40
|
+
@dalli.flush_all
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/bandit/version.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.join File.dirname(__FILE__), 'helper'
|
2
|
+
require File.join File.dirname(__FILE__), 'storage_test_base'
|
3
|
+
|
4
|
+
class DalliStorageTest < Test::Unit::TestCase
|
5
|
+
include StorageTestBase
|
6
|
+
include SetupHelper
|
7
|
+
|
8
|
+
def setup
|
9
|
+
Bandit.setup do |config|
|
10
|
+
config.player = "round_robin"
|
11
|
+
config.storage = 'dalli'
|
12
|
+
config.storage_config = CONFIG['memcache_storage_config']
|
13
|
+
end
|
14
|
+
|
15
|
+
@storage = Bandit.storage
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bandit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 9
|
10
|
+
version: 0.0.9
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Brian Muller
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-05-
|
18
|
+
date: 2012-05-29 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rails
|
@@ -25,34 +25,18 @@ dependencies:
|
|
25
25
|
requirements:
|
26
26
|
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
hash:
|
28
|
+
hash: 15
|
29
29
|
segments:
|
30
30
|
- 3
|
31
|
+
- 2
|
31
32
|
- 0
|
32
|
-
|
33
|
-
version: 3.0.5
|
33
|
+
version: 3.2.0
|
34
34
|
type: :runtime
|
35
35
|
version_requirements: *id001
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: redis
|
38
|
-
prerelease: false
|
39
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
-
none: false
|
41
|
-
requirements:
|
42
|
-
- - ">="
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
hash: 3
|
45
|
-
segments:
|
46
|
-
- 2
|
47
|
-
- 2
|
48
|
-
- 2
|
49
|
-
version: 2.2.2
|
50
|
-
type: :runtime
|
51
|
-
version_requirements: *id002
|
52
36
|
- !ruby/object:Gem::Dependency
|
53
37
|
name: rdoc
|
54
38
|
prerelease: false
|
55
|
-
requirement: &
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
56
40
|
none: false
|
57
41
|
requirements:
|
58
42
|
- - ">="
|
@@ -62,7 +46,7 @@ dependencies:
|
|
62
46
|
- 0
|
63
47
|
version: "0"
|
64
48
|
type: :development
|
65
|
-
version_requirements: *
|
49
|
+
version_requirements: *id002
|
66
50
|
description: Bandit provides a way to do multi-armed bandit optimization of alternatives in a rails website
|
67
51
|
email:
|
68
52
|
- brian.muller@livingsocial.com
|
@@ -95,6 +79,7 @@ files:
|
|
95
79
|
- lib/bandit/players/epsilon_greedy.rb
|
96
80
|
- lib/bandit/players/round_robin.rb
|
97
81
|
- lib/bandit/storage/base.rb
|
82
|
+
- lib/bandit/storage/dalli.rb
|
98
83
|
- lib/bandit/storage/memcache.rb
|
99
84
|
- lib/bandit/storage/memory.rb
|
100
85
|
- lib/bandit/storage/redis.rb
|
@@ -124,6 +109,7 @@ files:
|
|
124
109
|
- lib/generators/bandit/templates/dashboard/view/show.html.erb
|
125
110
|
- players.rdoc
|
126
111
|
- test/config.yml
|
112
|
+
- test/dalli_storage_test.rb
|
127
113
|
- test/helper.rb
|
128
114
|
- test/memcache_storage_test.rb
|
129
115
|
- test/memory_storage_test.rb
|
@@ -156,8 +142,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
142
|
segments:
|
157
143
|
- 0
|
158
144
|
version: "0"
|
159
|
-
requirements:
|
160
|
-
|
145
|
+
requirements:
|
146
|
+
- Either redis or memcache gem
|
161
147
|
rubyforge_project: bandit
|
162
148
|
rubygems_version: 1.8.17
|
163
149
|
signing_key:
|
@@ -165,6 +151,7 @@ specification_version: 3
|
|
165
151
|
summary: Multi-armed bandit testing in rails
|
166
152
|
test_files:
|
167
153
|
- test/config.yml
|
154
|
+
- test/dalli_storage_test.rb
|
168
155
|
- test/helper.rb
|
169
156
|
- test/memcache_storage_test.rb
|
170
157
|
- test/memory_storage_test.rb
|