Skip to content

Commit c9b54c8

Browse files
committed
Batch cleanup_old_experiments! into one pipelined roundtrip
1 parent 1c8b88b commit c9b54c8

3 files changed

Lines changed: 101 additions & 5 deletions

File tree

lib/split/user.rb

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,25 @@ def initialize(context, adapter = nil)
1515

1616
def cleanup_old_experiments!
1717
return if @cleaned_up
18-
keys_without_finished(user.keys).each do |key|
19-
experiment = Experiment.new key_without_version(key)
20-
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
21-
user.delete key
22-
user.delete Experiment.finished_key(key)
18+
keys = keys_without_finished(user.keys)
19+
names = keys.map { |key| key_without_version(key) }
20+
21+
unless names.empty?
22+
winners, start_times = Split.redis.pipelined do |pipe|
23+
pipe.hmget(:experiment_winner, *names)
24+
pipe.hmget(:experiment_start_times, *names)
25+
end
26+
27+
keys.each_with_index do |key, index|
28+
has_winner = !winners[index].nil?
29+
not_started = start_times[index].nil?
30+
if has_winner || not_started
31+
user.delete key
32+
user.delete Experiment.finished_key(key)
33+
end
2334
end
2435
end
36+
2537
@cleaned_up = true
2638
end
2739

spec/support/redis_call_counter.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require "redis-client"
4+
5+
module SplitRedisCallInstrumentation
6+
KEY = :split_redis_call_count
7+
8+
def call(command, config)
9+
SplitRedisCallInstrumentation.increment
10+
super
11+
end
12+
13+
def call_pipelined(commands, config)
14+
SplitRedisCallInstrumentation.increment
15+
super
16+
end
17+
18+
class << self
19+
def count
20+
previous = Thread.current[KEY]
21+
Thread.current[KEY] = 0
22+
yield
23+
Thread.current[KEY]
24+
ensure
25+
Thread.current[KEY] = previous
26+
end
27+
28+
def increment
29+
count = Thread.current[KEY]
30+
Thread.current[KEY] = count + 1 if count
31+
end
32+
end
33+
end
34+
35+
RedisClient.register(SplitRedisCallInstrumentation)
36+
37+
RSpec::Matchers.define :make_redis_calls do |expected|
38+
supports_block_expectations
39+
40+
match do |block|
41+
@actual = SplitRedisCallInstrumentation.count(&block)
42+
values_match?(expected, @actual)
43+
end
44+
45+
failure_message do
46+
"expected block to make #{description_of(expected)} Redis roundtrip(s), but made #{@actual}"
47+
end
48+
49+
failure_message_when_negated do
50+
"expected block not to make #{description_of(expected)} Redis roundtrip(s), but it did (#{@actual})"
51+
end
52+
53+
description do
54+
"make #{description_of(expected)} Redis roundtrip(s)"
55+
end
56+
end

spec/user_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,34 @@
8888
@subject.cleanup_old_experiments!
8989
end
9090
end
91+
92+
context "with many experiments" do
93+
let(:user_keys) do
94+
{
95+
"with_winner" => "red",
96+
"not_started" => "red",
97+
"active" => "red"
98+
}
99+
end
100+
101+
before do
102+
with_winner = Split::ExperimentCatalog.find_or_create("with_winner", "red", "blue")
103+
with_winner.start
104+
with_winner.winner = "red"
105+
106+
Split::ExperimentCatalog.find_or_create("active", "red", "blue").start
107+
end
108+
109+
it "keeps active experiments while dropping finished/not-started ones" do
110+
@subject.cleanup_old_experiments!
111+
112+
expect(@subject.keys).to eq(["active"])
113+
end
114+
115+
it "batches the winner/start-time lookups into a single roundtrip" do
116+
expect { @subject.cleanup_old_experiments! }.to make_redis_calls(1)
117+
end
118+
end
91119
end
92120

93121
context "allows user to be loaded from adapter" do

0 commit comments

Comments
 (0)