Skip to content

Commit 35b5259

Browse files
authored
Merge pull request #648 from yankaindustries/master
Add Split::Cache to reduce load on Redis
2 parents 00d76dc + a26b595 commit 35b5259

File tree

9 files changed

+139
-16
lines changed

9 files changed

+139
-16
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,21 @@ split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
756756
Split.redis = split_config[Rails.env]
757757
```
758758

759+
### Redis Caching
760+
761+
In some high-volume usage scenarios, Redis load can be incurred by repeated
762+
fetches for fairly static data. Enabling caching will reduce this load, but
763+
require a restart for changes to experiment definitions to take effect.
764+
765+
```ruby
766+
Split.configuration.cache = true
767+
````
768+
769+
This currently caches:
770+
- `Split::ExperimentCatalog.find`
771+
- `Split::Experiment.start_time`
772+
- `Split::Experiment.winner`
773+
759774
## Namespaces
760775

761776
If you're running multiple, separate instances of Split you may want

lib/split.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'split/algorithms/weighted_sample'
77
require 'split/algorithms/whiplash'
88
require 'split/alternative'
9+
require 'split/cache'
910
require 'split/configuration'
1011
require 'split/encapsulated_helper'
1112
require 'split/exceptions'
@@ -65,6 +66,10 @@ def configure
6566
self.configuration ||= Configuration.new
6667
yield(configuration)
6768
end
69+
70+
def cache(namespace, key, &block)
71+
Split::Cache.fetch(namespace, key, &block)
72+
end
6873
end
6974

7075
# Check to see if being run in a Rails application. If so, wait until before_initialize to run configuration so Gems that create ENV variables have the chance to initialize first.

lib/split/cache.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module Split
4+
class Cache
5+
6+
def self.clear
7+
@cache = nil
8+
end
9+
10+
def self.fetch(namespace, key)
11+
return yield unless Split.configuration.cache
12+
13+
@cache ||= {}
14+
@cache[namespace] ||= {}
15+
16+
value = @cache[namespace][key]
17+
return value if value
18+
19+
@cache[namespace][key] = yield
20+
end
21+
end
22+
end

lib/split/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Configuration
2828
attr_accessor :winning_alternative_recalculation_interval
2929
attr_accessor :redis
3030
attr_accessor :dashboard_pagination_default_per_page
31+
attr_accessor :cache
3132

3233
attr_reader :experiments
3334

lib/split/experiment.rb

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,13 @@ def alternatives=(alts)
128128
end
129129

130130
def winner
131-
experiment_winner = redis.hget(:experiment_winner, name)
132-
if experiment_winner
133-
Split::Alternative.new(experiment_winner, name)
134-
else
135-
nil
131+
Split.cache(:experiment_winner, name) do
132+
experiment_winner = redis.hget(:experiment_winner, name)
133+
if experiment_winner
134+
Split::Alternative.new(experiment_winner, name)
135+
else
136+
nil
137+
end
136138
end
137139
end
138140

@@ -165,13 +167,15 @@ def start
165167
end
166168

167169
def start_time
168-
t = redis.hget(:experiment_start_times, @name)
169-
if t
170-
# Check if stored time is an integer
171-
if t =~ /^[-+]?[0-9]+$/
172-
Time.at(t.to_i)
173-
else
174-
Time.parse(t)
170+
Split.cache(:experiment_start_times, @name) do
171+
t = redis.hget(:experiment_start_times, @name)
172+
if t
173+
# Check if stored time is an integer
174+
if t =~ /^[-+]?[0-9]+$/
175+
Time.at(t.to_i)
176+
else
177+
Time.parse(t)
178+
end
175179
end
176180
end
177181
end

lib/split/experiment_catalog.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ def self.all_active_first
1414
end
1515

1616
def self.find(name)
17-
return unless Split.redis.exists?(name)
18-
Experiment.new(name).tap { |exp| exp.load_from_redis }
17+
Split.cache(:experiment_catalog, name) do
18+
return unless Split.redis.exists?(name)
19+
Experiment.new(name).tap { |exp| exp.load_from_redis }
20+
end
1921
end
2022

2123
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
@@ -47,6 +49,5 @@ def self.normalize_experiment(metric_descriptor)
4749
return experiment_name, goals
4850
end
4951
private_class_method :normalize_experiment
50-
5152
end
5253
end

spec/cache_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
require 'spec_helper'
3+
4+
describe Split::Cache do
5+
6+
let(:namespace) { :test_namespace }
7+
let(:key) { :test_key }
8+
let(:now) { 1606189017 }
9+
10+
before { allow(Time).to receive(:now).and_return(now) }
11+
12+
describe 'clear' do
13+
14+
before { Split.configuration.cache = true }
15+
16+
it 'clears the cache' do
17+
expect(Time).to receive(:now).and_return(now).exactly(2).times
18+
Split::Cache.fetch(namespace, key) { Time.now }
19+
Split::Cache.clear
20+
Split::Cache.fetch(namespace, key) { Time.now }
21+
end
22+
end
23+
24+
describe 'fetch' do
25+
26+
subject { Split::Cache.fetch(namespace, key) { Time.now } }
27+
28+
context 'when cache disabled' do
29+
30+
before { Split.configuration.cache = false }
31+
32+
it 'returns the yield' do
33+
expect(subject).to eql(now)
34+
end
35+
36+
it 'yields every time' do
37+
expect(Time).to receive(:now).and_return(now).exactly(2).times
38+
Split::Cache.fetch(namespace, key) { Time.now }
39+
Split::Cache.fetch(namespace, key) { Time.now }
40+
end
41+
end
42+
43+
context 'when cache enabled' do
44+
45+
before { Split.configuration.cache = true }
46+
47+
it 'returns the yield' do
48+
expect(subject).to eql(now)
49+
end
50+
51+
it 'yields once' do
52+
expect(Time).to receive(:now).and_return(now).once
53+
Split::Cache.fetch(namespace, key) { Time.now }
54+
Split::Cache.fetch(namespace, key) { Time.now }
55+
end
56+
57+
it 'honors namespace' do
58+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
59+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
60+
61+
expect(Split::Cache.fetch(:a, key) { :a }).to eql(:a)
62+
expect(Split::Cache.fetch(:b, key) { :b }).to eql(:b)
63+
end
64+
65+
it 'honors key' do
66+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
67+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
68+
69+
expect(Split::Cache.fetch(namespace, :a) { :a }).to eql(:a)
70+
expect(Split::Cache.fetch(namespace, :b) { :b }).to eql(:b)
71+
end
72+
end
73+
end
74+
end

spec/dashboard_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def link(color)
188188

189189
it "calls disable of cohorting when action is disable" do
190190
post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "disable" }
191-
191+
192192
expect(experiment.cohorting_disabled?).to eq true
193193
end
194194

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module GlobalSharedContext
2222
Split.redis = Redis.new
2323
Split.redis.select(10)
2424
Split.redis.flushdb
25+
Split::Cache.clear
2526
@ab_user = mock_user
2627
params = nil
2728
end

0 commit comments

Comments
 (0)