async_experiments 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +55 -11
- data/lib/async_experiments.rb +25 -14
- data/lib/async_experiments/candidate_worker.rb +3 -3
- data/lib/async_experiments/experiment_control.rb +7 -3
- data/lib/async_experiments/experiment_error_worker.rb +7 -2
- data/lib/async_experiments/experiment_result.rb +19 -16
- data/lib/async_experiments/experiment_result_candidate_worker.rb +24 -0
- data/lib/async_experiments/experiment_result_control_worker.rb +29 -0
- data/lib/async_experiments/version.rb +1 -1
- data/spec/async_experiments_spec.rb +81 -20
- data/spec/candidate_worker_spec.rb +21 -24
- data/spec/examples.txt +48 -37
- data/spec/experiment_control_spec.rb +8 -6
- data/spec/experiment_error_worker_spec.rb +27 -6
- data/spec/experiment_result_candidate_worker_spec.rb +50 -0
- data/spec/experiment_result_control_worker_spec.rb +96 -0
- data/spec/experiment_result_spec.rb +103 -85
- metadata +8 -5
- data/lib/async_experiments/experiment_result_worker.rb +0 -36
- data/spec/experiment_result_worker_spec.rb +0 -106
@@ -1,12 +1,22 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "async_experiments/candidate_worker"
|
3
|
-
require "async_experiments/
|
3
|
+
require "async_experiments/experiment_result_candidate_worker"
|
4
4
|
|
5
5
|
RSpec.describe AsyncExperiments::CandidateWorker do
|
6
6
|
let(:name) { "some_experiment" }
|
7
7
|
let(:id) { SecureRandom.uuid }
|
8
|
+
let(:candidate_expiry) { 5 }
|
9
|
+
let(:results_expiry) { 60 }
|
10
|
+
let(:arguments) do
|
11
|
+
{
|
12
|
+
name: name,
|
13
|
+
id: id,
|
14
|
+
candidate_expiry: candidate_expiry,
|
15
|
+
results_expiry: results_expiry,
|
16
|
+
}
|
17
|
+
end
|
8
18
|
|
9
|
-
let(:run_output) { [{some: "output"}] }
|
19
|
+
let(:run_output) { [{ some: "output" }] }
|
10
20
|
|
11
21
|
class TestWorker < AsyncExperiments::CandidateWorker
|
12
22
|
def perform(run_output, experiment_config)
|
@@ -29,23 +39,16 @@ RSpec.describe AsyncExperiments::CandidateWorker do
|
|
29
39
|
|
30
40
|
|
31
41
|
it "returns the control run output" do
|
32
|
-
output = subject.perform(run_output,
|
33
|
-
name: name,
|
34
|
-
id: id,
|
35
|
-
)
|
36
|
-
|
42
|
+
output = subject.perform(run_output, arguments)
|
37
43
|
expect(output).to eq(run_output)
|
38
44
|
end
|
39
45
|
|
40
46
|
describe "#experiment_candidate(experiment_config)" do
|
41
|
-
it "triggers an
|
42
|
-
expect(AsyncExperiments::
|
43
|
-
.with(name, id, run_output, instance_of(Float),
|
44
|
-
|
45
|
-
subject.perform(run_output,
|
46
|
-
name: name,
|
47
|
-
id: id,
|
48
|
-
)
|
47
|
+
it "triggers an ExperimentResultCandidateWorker with the candidate output and duration" do
|
48
|
+
expect(AsyncExperiments::ExperimentResultCandidateWorker).to receive(:perform_async)
|
49
|
+
.with(name, id, run_output, instance_of(Float), a_kind_of(Integer))
|
50
|
+
|
51
|
+
subject.perform(run_output, arguments)
|
49
52
|
end
|
50
53
|
|
51
54
|
context "when experiment errors are being raised" do
|
@@ -62,10 +65,7 @@ RSpec.describe AsyncExperiments::CandidateWorker do
|
|
62
65
|
allow(Time).to receive(:now).and_raise(StandardError.new("Test exception"))
|
63
66
|
|
64
67
|
expect {
|
65
|
-
subject.perform(run_output,
|
66
|
-
"name" => name,
|
67
|
-
"id" => id,
|
68
|
-
)
|
68
|
+
subject.perform(run_output, arguments)
|
69
69
|
}.to raise_error(Exception, "Test exception")
|
70
70
|
end
|
71
71
|
end
|
@@ -84,12 +84,9 @@ RSpec.describe AsyncExperiments::CandidateWorker do
|
|
84
84
|
allow(Time).to receive(:now).and_raise(StandardError.new("Test exception"))
|
85
85
|
|
86
86
|
expect(AsyncExperiments::ExperimentErrorWorker).to receive(:perform_async)
|
87
|
-
.with(name, instance_of(String))
|
87
|
+
.with(name, instance_of(String), a_kind_of(Integer))
|
88
88
|
|
89
|
-
subject.perform(run_output,
|
90
|
-
name: name,
|
91
|
-
id: id,
|
92
|
-
)
|
89
|
+
subject.perform(run_output, arguments)
|
93
90
|
end
|
94
91
|
end
|
95
92
|
end
|
data/spec/examples.txt
CHANGED
@@ -1,37 +1,48 @@
|
|
1
|
-
example_id
|
2
|
-
|
3
|
-
./spec/async_experiments_spec.rb[1:1:1]
|
4
|
-
./spec/
|
5
|
-
./spec/
|
6
|
-
./spec/
|
7
|
-
./spec/
|
8
|
-
./spec/candidate_worker_spec.rb[1:
|
9
|
-
./spec/
|
10
|
-
./spec/
|
11
|
-
./spec/
|
12
|
-
./spec/
|
13
|
-
./spec/
|
14
|
-
./spec/
|
15
|
-
./spec/
|
16
|
-
./spec/
|
17
|
-
./spec/
|
18
|
-
./spec/
|
19
|
-
./spec/
|
20
|
-
./spec/
|
21
|
-
./spec/
|
22
|
-
./spec/
|
23
|
-
./spec/
|
24
|
-
./spec/
|
25
|
-
./spec/
|
26
|
-
./spec/
|
27
|
-
./spec/
|
28
|
-
./spec/
|
29
|
-
./spec/
|
30
|
-
./spec/
|
31
|
-
./spec/
|
32
|
-
./spec/
|
33
|
-
./spec/
|
34
|
-
./spec/
|
35
|
-
./spec/
|
36
|
-
./spec/
|
37
|
-
./spec/
|
1
|
+
example_id | status | run_time |
|
2
|
+
-------------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/async_experiments_spec.rb[1:1:1] | passed | 0.00074 seconds |
|
4
|
+
./spec/async_experiments_spec.rb[1:2:1] | passed | 0.00066 seconds |
|
5
|
+
./spec/async_experiments_spec.rb[1:3:1:1] | passed | 0.00059 seconds |
|
6
|
+
./spec/async_experiments_spec.rb[1:3:2:1] | passed | 0.00069 seconds |
|
7
|
+
./spec/async_experiments_spec.rb[1:3:3:1] | passed | 0.00076 seconds |
|
8
|
+
./spec/candidate_worker_spec.rb[1:1] | passed | 0.00034 seconds |
|
9
|
+
./spec/candidate_worker_spec.rb[1:2] | passed | 0.00076 seconds |
|
10
|
+
./spec/candidate_worker_spec.rb[1:3:1] | passed | 0.00093 seconds |
|
11
|
+
./spec/candidate_worker_spec.rb[1:3:2:1] | passed | 0.00204 seconds |
|
12
|
+
./spec/candidate_worker_spec.rb[1:3:3:1] | passed | 0.00128 seconds |
|
13
|
+
./spec/experiment_control_spec.rb[1:1:1] | passed | 0.00098 seconds |
|
14
|
+
./spec/experiment_control_spec.rb[1:1:2] | passed | 0.0089 seconds |
|
15
|
+
./spec/experiment_control_spec.rb[1:1:3] | passed | 0.00131 seconds |
|
16
|
+
./spec/experiment_error_worker_spec.rb[1:1] | passed | 0.00054 seconds |
|
17
|
+
./spec/experiment_error_worker_spec.rb[1:2] | passed | 0.00057 seconds |
|
18
|
+
./spec/experiment_error_worker_spec.rb[1:3:1] | passed | 0.00064 seconds |
|
19
|
+
./spec/experiment_error_worker_spec.rb[1:4:1] | passed | 0.00069 seconds |
|
20
|
+
./spec/experiment_error_worker_spec.rb[1:5] | passed | 0.00071 seconds |
|
21
|
+
./spec/experiment_result_candidate_worker_spec.rb[1:1] | passed | 0.00059 seconds |
|
22
|
+
./spec/experiment_result_candidate_worker_spec.rb[1:2] | passed | 0.00142 seconds |
|
23
|
+
./spec/experiment_result_control_worker_spec.rb[1:1] | passed | 0.00119 seconds |
|
24
|
+
./spec/experiment_result_control_worker_spec.rb[1:2:1] | passed | 0.00128 seconds |
|
25
|
+
./spec/experiment_result_control_worker_spec.rb[1:3:1] | passed | 0.0022 seconds |
|
26
|
+
./spec/experiment_result_control_worker_spec.rb[1:3:2] | passed | 0.00238 seconds |
|
27
|
+
./spec/experiment_result_control_worker_spec.rb[1:3:3:1] | passed | 0.00213 seconds |
|
28
|
+
./spec/experiment_result_spec.rb[1:1:1] | passed | 0.00062 seconds |
|
29
|
+
./spec/experiment_result_spec.rb[1:1:2] | passed | 0.00058 seconds |
|
30
|
+
./spec/experiment_result_spec.rb[1:2:1] | passed | 0.00431 seconds |
|
31
|
+
./spec/experiment_result_spec.rb[1:2:2] | passed | 0.00067 seconds |
|
32
|
+
./spec/experiment_result_spec.rb[1:2:3:1] | passed | 0.00056 seconds |
|
33
|
+
./spec/experiment_result_spec.rb[1:2:3:2:1] | passed | 0.00063 seconds |
|
34
|
+
./spec/experiment_result_spec.rb[1:2:3:3:1] | passed | 0.00069 seconds |
|
35
|
+
./spec/experiment_result_spec.rb[1:2:3:4] | passed | 0.00059 seconds |
|
36
|
+
./spec/experiment_result_spec.rb[1:2:4:1] | passed | 0.00053 seconds |
|
37
|
+
./spec/experiment_result_spec.rb[1:2:4:2] | passed | 0.0006 seconds |
|
38
|
+
./spec/experiment_result_spec.rb[1:2:4:3] | passed | 0.0005 seconds |
|
39
|
+
./spec/experiment_result_spec.rb[1:3:1:1:1] | passed | 0.00069 seconds |
|
40
|
+
./spec/experiment_result_spec.rb[1:3:1:2:1] | passed | 0.00052 seconds |
|
41
|
+
./spec/experiment_result_spec.rb[1:3:2:1] | passed | 0.0004 seconds |
|
42
|
+
./spec/experiment_result_spec.rb[1:3:3:1] | passed | 0.0011 seconds |
|
43
|
+
./spec/util_spec.rb[1:1:1] | passed | 0.00013 seconds |
|
44
|
+
./spec/util_spec.rb[1:2:1] | passed | 0.00024 seconds |
|
45
|
+
./spec/util_spec.rb[1:3:1] | passed | 0.00018 seconds |
|
46
|
+
./spec/util_spec.rb[1:3:2] | passed | 0.00017 seconds |
|
47
|
+
./spec/util_spec.rb[1:3:3] | passed | 0.00009 seconds |
|
48
|
+
./spec/util_spec.rb[1:3:4] | passed | 0.00032 seconds |
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "async_experiments/experiment_control"
|
3
|
-
require "async_experiments/
|
3
|
+
require "async_experiments/experiment_result_control_worker"
|
4
4
|
|
5
5
|
RSpec.describe AsyncExperiments::ExperimentControl do
|
6
6
|
let(:name) { :some_experiment }
|
@@ -15,7 +15,7 @@ RSpec.describe AsyncExperiments::ExperimentControl do
|
|
15
15
|
}
|
16
16
|
}
|
17
17
|
|
18
|
-
let(:run_output) { [{some: "output"}] }
|
18
|
+
let(:run_output) { [{ some: "output" }] }
|
19
19
|
|
20
20
|
class TestClass
|
21
21
|
include AsyncExperiments::ExperimentControl
|
@@ -34,10 +34,10 @@ RSpec.describe AsyncExperiments::ExperimentControl do
|
|
34
34
|
allow(SecureRandom).to receive(:uuid).and_return(id)
|
35
35
|
end
|
36
36
|
|
37
|
-
describe "#experiment_control
|
38
|
-
it "triggers an
|
39
|
-
expect(AsyncExperiments::
|
40
|
-
.with(name, id, run_output, instance_of(Float),
|
37
|
+
describe "#experiment_control" do
|
38
|
+
it "triggers an ExperimentResultControlWorker with the control output and duration" do
|
39
|
+
expect(AsyncExperiments::ExperimentResultControlWorker).to receive(:perform_in)
|
40
|
+
.with(1, name, id, run_output, instance_of(Float), a_kind_of(Integer))
|
41
41
|
|
42
42
|
subject.call(name, run_output, candidate_config)
|
43
43
|
end
|
@@ -47,6 +47,8 @@ RSpec.describe AsyncExperiments::ExperimentControl do
|
|
47
47
|
.with(*candidate_args,
|
48
48
|
name: name,
|
49
49
|
id: id,
|
50
|
+
candidate_expiry: a_kind_of(Integer),
|
51
|
+
results_expiry: a_kind_of(Integer),
|
50
52
|
)
|
51
53
|
|
52
54
|
subject.call(name, run_output, candidate_config)
|
@@ -5,9 +5,10 @@ require "async_experiments/experiment_error_worker"
|
|
5
5
|
RSpec.describe AsyncExperiments::ExperimentErrorWorker do
|
6
6
|
let(:name) { "some_experiment" }
|
7
7
|
let(:error) { "Something went wrong" }
|
8
|
+
let(:expiry) { 30 }
|
8
9
|
|
9
10
|
let(:statsd) { double(:statsd, increment: nil) }
|
10
|
-
let(:redis) { double(:redis,
|
11
|
+
let(:redis) { double(:redis, set: nil, exists: false, expire: nil) }
|
11
12
|
|
12
13
|
subject { described_class.new }
|
13
14
|
|
@@ -20,17 +21,37 @@ RSpec.describe AsyncExperiments::ExperimentErrorWorker do
|
|
20
21
|
Sidekiq::Testing.fake! do
|
21
22
|
described_class.perform_async
|
22
23
|
|
23
|
-
expect(
|
24
|
+
expect(Sidekiq::Queues["experiments"].size).to eq(1)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
28
|
it "increments the statsd error count for the experiment" do
|
28
29
|
expect(statsd).to receive(:increment).with("experiments.#{name}.exceptions")
|
29
|
-
subject.perform(name, error)
|
30
|
+
subject.perform(name, error, expiry)
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
context "when redis already has the exception stored" do
|
34
|
+
before { allow(redis).to receive(:exists).and_return(true) }
|
35
|
+
|
36
|
+
it "does not store the exception" do
|
37
|
+
expect(redis).not_to receive(:set)
|
38
|
+
subject.perform(name, error, expiry)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when redis does not have the exception stored" do
|
43
|
+
before { allow(redis).to receive(:exists).and_return(false) }
|
44
|
+
|
45
|
+
it "stores the exception" do
|
46
|
+
expect(redis).to receive(:set)
|
47
|
+
.with(/^experiments:#{Regexp.quote(name)}:exceptions:/, error)
|
48
|
+
subject.perform(name, error, expiry)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "sets an expiry time" do
|
53
|
+
expect(redis).to receive(:expire)
|
54
|
+
.with(/^experiments:#{Regexp.quote(name)}:exceptions:/, expiry)
|
55
|
+
subject.perform(name, error, expiry)
|
35
56
|
end
|
36
57
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "async_experiments"
|
3
|
+
require "async_experiments/experiment_result"
|
4
|
+
require "async_experiments/experiment_result_candidate_worker"
|
5
|
+
|
6
|
+
RSpec.describe AsyncExperiments::ExperimentResultCandidateWorker do
|
7
|
+
let(:name) { "some_experiment" }
|
8
|
+
let(:id) { SecureRandom.uuid }
|
9
|
+
|
10
|
+
let(:statsd) { double(:statsd) }
|
11
|
+
let(:redis) { double(:redis) }
|
12
|
+
let(:redis_key) { double(:redis_key) }
|
13
|
+
|
14
|
+
let(:candidate_output) { "candidate output" }
|
15
|
+
let(:candidate_duration) { 5.0 }
|
16
|
+
|
17
|
+
let(:candidate) {
|
18
|
+
double(:candidate,
|
19
|
+
candidate?: true,
|
20
|
+
control?: false,
|
21
|
+
key: redis_key,
|
22
|
+
)
|
23
|
+
}
|
24
|
+
let(:expiry) { 10 }
|
25
|
+
|
26
|
+
subject { described_class.new }
|
27
|
+
|
28
|
+
before do
|
29
|
+
allow(Sidekiq).to receive(:redis).and_yield(redis)
|
30
|
+
AsyncExperiments.statsd = statsd
|
31
|
+
end
|
32
|
+
|
33
|
+
it "uses the 'experiments' queue" do
|
34
|
+
Sidekiq::Testing.fake! do
|
35
|
+
described_class.perform_async
|
36
|
+
|
37
|
+
expect(Sidekiq::Queues["experiments"].size).to eq(1)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "stores the run output and duration" do
|
42
|
+
allow(AsyncExperiments::ExperimentResult).to receive(:new)
|
43
|
+
.with(name, id, :candidate, redis, statsd, candidate_output, candidate_duration)
|
44
|
+
.and_return(candidate)
|
45
|
+
|
46
|
+
expect(candidate).to receive(:store_run_output).with(expiry)
|
47
|
+
|
48
|
+
subject.perform(name, id, candidate_output, candidate_duration, expiry)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "async_experiments"
|
3
|
+
require "async_experiments/experiment_result"
|
4
|
+
require "async_experiments/experiment_result_control_worker"
|
5
|
+
|
6
|
+
RSpec.describe AsyncExperiments::ExperimentResultControlWorker do
|
7
|
+
let(:name) { "some_experiment" }
|
8
|
+
let(:id) { SecureRandom.uuid }
|
9
|
+
|
10
|
+
let(:statsd) { double(:statsd) }
|
11
|
+
let(:redis) { double(:redis) }
|
12
|
+
let(:redis_key) { double(:redis_key) }
|
13
|
+
|
14
|
+
let(:control_output) { "control output" }
|
15
|
+
let(:control_duration) { 10.0 }
|
16
|
+
|
17
|
+
let(:candidate_output) { "candidate output" }
|
18
|
+
let(:candidate_duration) { 5.0 }
|
19
|
+
let(:allowed_attempts) { 5 }
|
20
|
+
|
21
|
+
let(:expiry) { 30 }
|
22
|
+
|
23
|
+
let(:control) {
|
24
|
+
double(:control,
|
25
|
+
control?: true,
|
26
|
+
candidate?: false,
|
27
|
+
key: redis_key,
|
28
|
+
)
|
29
|
+
}
|
30
|
+
let(:candidate) {
|
31
|
+
double(:candidate,
|
32
|
+
candidate?: true,
|
33
|
+
control?: false,
|
34
|
+
key: redis_key,
|
35
|
+
)
|
36
|
+
}
|
37
|
+
|
38
|
+
subject { described_class.new }
|
39
|
+
|
40
|
+
before do
|
41
|
+
allow(Sidekiq).to receive(:redis).and_yield(redis)
|
42
|
+
AsyncExperiments.statsd = statsd
|
43
|
+
allow(AsyncExperiments::ExperimentResult).to receive(:new)
|
44
|
+
.with(name, id, :control, redis, statsd, control_output, control_duration)
|
45
|
+
.and_return(control)
|
46
|
+
|
47
|
+
allow(AsyncExperiments::ExperimentResult).to receive(:new)
|
48
|
+
.with(name, id, :candidate, redis, statsd)
|
49
|
+
.and_return(candidate)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "uses the 'experiments' queue" do
|
53
|
+
Sidekiq::Testing.fake! do
|
54
|
+
described_class.perform_async
|
55
|
+
|
56
|
+
expect(Sidekiq::Queues["experiments"].size).to eq(1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when the candidate is available" do
|
61
|
+
before do
|
62
|
+
allow(candidate).to receive(:available?).and_return(true)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "processes the run output with the candidate" do
|
66
|
+
expect(control).to receive(:process_run_output).with(candidate, expiry)
|
67
|
+
subject.perform(name, id, control_output, control_duration, expiry)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when the candidate is unavailable" do
|
72
|
+
before do
|
73
|
+
allow(candidate).to receive(:available?).and_return(false)
|
74
|
+
allow(described_class).to receive(:perform_in)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "does not process the run output" do
|
78
|
+
expect(control).not_to receive(:process_run_output)
|
79
|
+
subject.perform(name, id, control_output, control_duration, expiry)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "schedules the job to run again later" do
|
83
|
+
args = [name, id, control_output, control_duration, expiry, allowed_attempts]
|
84
|
+
expect(described_class).to receive(:perform_in).with(5, *args, 2)
|
85
|
+
subject.perform(*args)
|
86
|
+
end
|
87
|
+
|
88
|
+
context "and it has completed it's allowed jobs" do
|
89
|
+
it "does not schedule the job for later" do
|
90
|
+
args = [name, id, control_output, control_duration, expiry, allowed_attempts, allowed_attempts]
|
91
|
+
expect(described_class).to_not receive(:perform_in)
|
92
|
+
subject.perform(*args)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -2,135 +2,153 @@ require "spec_helper"
|
|
2
2
|
require "async_experiments/experiment_result"
|
3
3
|
|
4
4
|
RSpec.describe AsyncExperiments::ExperimentResult do
|
5
|
+
let(:name) { :test_name }
|
5
6
|
let(:id) { SecureRandom.uuid }
|
6
|
-
let(:
|
7
|
-
|
8
|
-
let(:
|
9
|
-
let(:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
let(:control) { described_class.new(name, id, :control, redis, statsd, control_run_output, control_duration) }
|
18
|
-
let(:candidate) { described_class.new(name, id, :candidate, redis, statsd, candidate_run_output, candidate_duration) }
|
19
|
-
|
20
|
-
describe "#key" do
|
21
|
-
it "builds a key from the name and ID" do
|
22
|
-
expect(control.key).to eq("test_experiment:#{id}")
|
23
|
-
end
|
7
|
+
let(:type) { :control }
|
8
|
+
let(:output) { "output" }
|
9
|
+
let(:duration) { 1.0 }
|
10
|
+
let(:redis_key) { "experiments:#{name}:#{id}:#{type}" }
|
11
|
+
let(:statsd) do
|
12
|
+
double(
|
13
|
+
:statsd,
|
14
|
+
timing: nil,
|
15
|
+
increment: nil,
|
16
|
+
)
|
24
17
|
end
|
18
|
+
let(:redis) do
|
19
|
+
double(
|
20
|
+
:redis,
|
21
|
+
set: true,
|
22
|
+
expire: true,
|
23
|
+
del: true,
|
24
|
+
exists: false,
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
subject { described_class.new(name, id, type, redis, statsd, output, duration) }
|
25
29
|
|
26
30
|
describe "#store_run_output" do
|
27
|
-
|
31
|
+
let(:expiry) { 10 }
|
32
|
+
after { subject.store_run_output(expiry) }
|
33
|
+
|
34
|
+
it "sets item in redis" do
|
28
35
|
expect(redis).to receive(:set)
|
29
|
-
.with(
|
30
|
-
|
31
|
-
duration: candidate_duration,
|
32
|
-
}.to_json)
|
36
|
+
.with(redis_key, { run_output: output, duration: duration }.to_json)
|
37
|
+
end
|
33
38
|
|
34
|
-
|
39
|
+
it "sets an expiry on the redis entry" do
|
40
|
+
expect(redis).to receive(:expire)
|
41
|
+
.with(redis_key, expiry)
|
35
42
|
end
|
36
43
|
end
|
37
44
|
|
38
|
-
describe "
|
39
|
-
|
40
|
-
|
45
|
+
describe "#process_run_output" do
|
46
|
+
let(:candidate_key) { "experiments:#{name}:#{id}:candidate" }
|
47
|
+
let(:candidate_duration) { 5.0 }
|
48
|
+
let(:candidate_output) { "different" }
|
49
|
+
let(:candidate) do
|
50
|
+
double(
|
51
|
+
:candidate,
|
52
|
+
run_output: candidate_output,
|
53
|
+
duration: candidate_duration,
|
54
|
+
)
|
41
55
|
end
|
56
|
+
let(:expiry) { 30 }
|
57
|
+
after { subject.process_run_output(candidate, expiry) }
|
42
58
|
|
43
|
-
it "
|
59
|
+
it "stores the durations with statsd" do
|
44
60
|
expect(statsd).to receive(:timing)
|
45
|
-
.with("experiments.#{name}.control",
|
46
|
-
|
61
|
+
.with("experiments.#{name}.control", duration)
|
47
62
|
expect(statsd).to receive(:timing)
|
48
63
|
.with("experiments.#{name}.candidate", candidate_duration)
|
64
|
+
end
|
49
65
|
|
50
|
-
|
66
|
+
it "deletes candidate data" do
|
67
|
+
expect(redis).to receive(:del).with(candidate_key)
|
51
68
|
end
|
52
69
|
|
53
|
-
|
54
|
-
|
70
|
+
context "when candidate data is different to control data" do
|
71
|
+
let(:candidate_output) { "different" }
|
55
72
|
|
56
|
-
|
57
|
-
|
73
|
+
it "increments mismatch count with statsd" do
|
74
|
+
expect(statsd).to receive(:increment).with("experiments.#{name}.mismatches")
|
75
|
+
end
|
58
76
|
|
59
|
-
|
60
|
-
|
61
|
-
expect(statsd).to receive(:increment)
|
62
|
-
.with("experiments.#{name}.mismatches")
|
77
|
+
context "and the the data is already in redis" do
|
78
|
+
before { allow(redis).to receive(:exists).and_return(true) }
|
63
79
|
|
64
|
-
|
80
|
+
it "doesn't add the difference to redis" do
|
81
|
+
expect(redis).not_to receive(:set)
|
82
|
+
end
|
65
83
|
end
|
66
84
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
85
|
+
context "but the data is already in redis" do
|
86
|
+
before { allow(redis).to receive(:exists).and_return(false) }
|
87
|
+
|
88
|
+
it "adds the difference to redis" do
|
89
|
+
expect(redis).to receive(:set)
|
90
|
+
.with(/^experiments:#{Regexp.quote(name)}:mismatches:/, a_kind_of(String))
|
91
|
+
end
|
92
|
+
end
|
72
93
|
|
73
|
-
|
94
|
+
it "sets the expiry in redis" do
|
95
|
+
expect(redis).to receive(:expire)
|
96
|
+
.with(/^experiments:#{Regexp.quote(name)}:mismatches:/, expiry)
|
74
97
|
end
|
75
98
|
end
|
76
99
|
|
77
|
-
context "
|
78
|
-
let(:
|
100
|
+
context "when candidate data is the same as control data" do
|
101
|
+
let(:candidate_output) { output }
|
79
102
|
|
80
|
-
it "
|
103
|
+
it "doesn't increment a mismatch with statsd" do
|
81
104
|
expect(statsd).not_to receive(:increment)
|
82
|
-
control.process_run_output(candidate)
|
83
105
|
end
|
84
106
|
|
85
|
-
it "
|
107
|
+
it "doesn't add a difference to redis" do
|
86
108
|
expect(redis).not_to receive(:rpush)
|
87
|
-
|
109
|
+
end
|
110
|
+
|
111
|
+
it "doesn't update mismatch result expiry" do
|
112
|
+
expect(redis).not_to receive(:expire)
|
88
113
|
end
|
89
114
|
end
|
90
115
|
end
|
91
116
|
|
92
|
-
describe "
|
93
|
-
|
117
|
+
describe "#available?" do
|
118
|
+
subject { described_class.new(name, id, type, redis, statsd, output, duration).available? }
|
94
119
|
|
95
|
-
context "
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
120
|
+
context "when instance is initialised with output and duration as nil" do
|
121
|
+
let(:output) { nil }
|
122
|
+
let(:duration) { nil }
|
123
|
+
before do
|
124
|
+
allow(redis).to receive(:get)
|
125
|
+
.and_return(redis_data.nil? ? nil : JSON.dump(redis_data))
|
100
126
|
end
|
101
|
-
end
|
102
127
|
|
103
|
-
context "if duration and output are not provided" do
|
104
128
|
context "and redis has the data" do
|
105
|
-
|
106
|
-
allow(redis).to receive(:get).with("experiments:#{name}:#{id}:#{type}").and_return({
|
107
|
-
run_output: candidate_run_output,
|
108
|
-
duration: candidate_duration,
|
109
|
-
}.to_json)
|
110
|
-
end
|
111
|
-
|
112
|
-
it "uses the redis data" do
|
113
|
-
candidate = described_class.new(name, id, type, redis, statsd)
|
114
|
-
expect(candidate.run_output).to eq(candidate_run_output)
|
115
|
-
expect(candidate.duration).to eq(candidate_duration)
|
116
|
-
end
|
129
|
+
let(:redis_data) { { run_output: "output", duration: 1.0 } }
|
117
130
|
|
118
|
-
it
|
119
|
-
candidate = described_class.new(name, id, type, redis, statsd)
|
120
|
-
expect(candidate.available?).to eq(true)
|
121
|
-
end
|
131
|
+
it { is_expected.to be true }
|
122
132
|
end
|
123
133
|
|
124
|
-
context "but redis
|
125
|
-
|
126
|
-
allow(redis).to receive(:get).and_return("")
|
127
|
-
end
|
134
|
+
context "but redis doesn't have the data" do
|
135
|
+
let(:redis_data) { nil }
|
128
136
|
|
129
|
-
it
|
130
|
-
missing_candidate = described_class.new(name, id, type, redis, statsd)
|
131
|
-
expect(missing_candidate.available?).to eq(false)
|
132
|
-
end
|
137
|
+
it { is_expected.to be false }
|
133
138
|
end
|
134
139
|
end
|
140
|
+
|
141
|
+
context "when instance is initialised with output and duration as values" do
|
142
|
+
let(:output) { "output" }
|
143
|
+
let(:duration) { 1.0 }
|
144
|
+
it { is_expected.to be true }
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when run_output is nil and duration is provided" do
|
148
|
+
let(:output) { nil }
|
149
|
+
let(:duration) { 1.0 }
|
150
|
+
|
151
|
+
it { is_expected.to be true }
|
152
|
+
end
|
135
153
|
end
|
136
154
|
end
|