Skip to content

Commit fcb3220

Browse files
committed
Merge pull request #203 from enova/redis-persist
Persistence within Redis directly
2 parents 87da249 + a53e5de commit fcb3220

File tree

4 files changed

+153
-4
lines changed

4 files changed

+153
-4
lines changed

README.mdown

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,11 @@ end
191191

192192
### Experiment Persistence
193193

194-
Split comes with two built-in persistence adapters for storing users and the alternatives they've been given for each experiment.
194+
Split comes with three built-in persistence adapters for storing users and the alternatives they've been given for each experiment.
195195

196196
By default Split will store the tests for each user in the session.
197197

198-
You can optionally configure Split to use a cookie or any custom adapter of your choosing.
198+
You can optionally configure Split to use a cookie, Redis, or any custom adapter of your choosing.
199199

200200
#### Cookies
201201

@@ -207,6 +207,22 @@ end
207207

208208
__Note:__ Using cookies depends on `ActionDispatch::Cookies` or any identical API
209209

210+
#### Redis
211+
212+
Using Redis will allow ab_users to persist across sessions or machines.
213+
214+
```ruby
215+
Split.configure do |config|
216+
config.persistence = Split::Persistence::RedisAdapter.with_config(:lookup_by => proc { |context| context.current_user_id }
217+
# Equivalent
218+
# config.persistence = Split::Persistence::RedisAdapter.with_config(:lookup_by => :current_user_id }
219+
end
220+
```
221+
222+
Options:
223+
* `lookup_by`: method to invoke per request for uniquely identifying ab_users (mandatory configuration)
224+
* `namespace`: separate namespace to store these persisted values (default "persistence")
225+
210226
#### Custom Adapter
211227

212228
Your custom adapter needs to implement the same API as existing adapters.

lib/split/persistence.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
%w[session_adapter cookie_adapter].each do |f|
1+
%w[session_adapter cookie_adapter redis_adapter].each do |f|
22
require "split/persistence/#{f}"
33
end
44

@@ -25,4 +25,4 @@ def self.persistence_config
2525
Split.configuration.persistence
2626
end
2727
end
28-
end
28+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module Split
2+
module Persistence
3+
class RedisAdapter
4+
DEFAULT_CONFIG = {:namespace => 'persistence'}.freeze
5+
6+
attr_reader :redis_key
7+
8+
def initialize(context)
9+
if lookup_by = self.class.config[:lookup_by]
10+
if lookup_by.respond_to?(:call)
11+
key_frag = lookup_by.call(context)
12+
else
13+
key_frag = context.send(lookup_by)
14+
end
15+
@redis_key = "#{self.class.config[:namespace]}:#{key_frag}"
16+
else
17+
raise "Please configure lookup_by"
18+
end
19+
end
20+
21+
def [](field)
22+
Split.redis.hget(redis_key, field)
23+
end
24+
25+
def []=(field, value)
26+
Split.redis.hset(redis_key, field, value)
27+
end
28+
29+
def delete(field)
30+
Split.redis.hdel(redis_key, field)
31+
end
32+
33+
def keys
34+
Split.redis.hkeys(redis_key)
35+
end
36+
37+
def self.with_config(options={})
38+
self.config.merge!(options)
39+
self
40+
end
41+
42+
def self.config
43+
@config ||= DEFAULT_CONFIG.dup
44+
end
45+
46+
def self.reset_config!
47+
@config = DEFAULT_CONFIG.dup
48+
end
49+
50+
end
51+
end
52+
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
require "spec_helper"
2+
3+
describe Split::Persistence::RedisAdapter do
4+
5+
let(:context) { double(:lookup => 'blah') }
6+
7+
subject { Split::Persistence::RedisAdapter.new(context) }
8+
9+
describe '#redis_key' do
10+
before { Split::Persistence::RedisAdapter.reset_config! }
11+
12+
context 'default' do
13+
it 'should raise error with prompt to set lookup_by' do
14+
expect{Split::Persistence::RedisAdapter.new(context)
15+
}.to raise_error
16+
end
17+
end
18+
19+
context 'config with lookup_by = proc { "block" }' do
20+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'block'}) }
21+
22+
it 'should be "persistence:block"' do
23+
subject.redis_key.should == 'persistence:block'
24+
end
25+
end
26+
27+
context 'config with lookup_by = proc { |context| context.test }' do
28+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'block'}) }
29+
let(:context) { double(:test => 'block') }
30+
31+
it 'should be "persistence:block"' do
32+
subject.redis_key.should == 'persistence:block'
33+
end
34+
end
35+
36+
context 'config with lookup_by = "method_name"' do
37+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'method_name') }
38+
let(:context) { double(:method_name => 'val') }
39+
40+
it 'should be "persistence:bar"' do
41+
subject.redis_key.should == 'persistence:val'
42+
end
43+
end
44+
45+
context 'config with namespace and lookup_by' do
46+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'frag'}, :namespace => 'namer') }
47+
48+
it 'should be "namer"' do
49+
subject.redis_key.should == 'namer:frag'
50+
end
51+
end
52+
end
53+
54+
context 'functional tests' do
55+
before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'lookup') }
56+
57+
describe "#[] and #[]=" do
58+
it "should set and return the value for given key" do
59+
subject["my_key"] = "my_value"
60+
subject["my_key"].should eq("my_value")
61+
end
62+
end
63+
64+
describe "#delete" do
65+
it "should delete the given key" do
66+
subject["my_key"] = "my_value"
67+
subject.delete("my_key")
68+
subject["my_key"].should be_nil
69+
end
70+
end
71+
72+
describe "#keys" do
73+
it "should return an array of the user's stored keys" do
74+
subject["my_key"] = "my_value"
75+
subject["my_second_key"] = "my_second_value"
76+
subject.keys.should =~ ["my_key", "my_second_key"]
77+
end
78+
end
79+
80+
end
81+
end

0 commit comments

Comments
 (0)