async_experiments 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,22 @@
1
1
  require "spec_helper"
2
2
  require "async_experiments/candidate_worker"
3
- require "async_experiments/experiment_result_worker"
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 ExperimentResultWorker with the candidate output and duration" do
42
- expect(AsyncExperiments::ExperimentResultWorker).to receive(:perform_async)
43
- .with(name, id, run_output, instance_of(Float), :candidate)
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
@@ -1,37 +1,48 @@
1
- example_id | status | run_time |
2
- ------------------------------------------------ | ------ | --------------- |
3
- ./spec/async_experiments_spec.rb[1:1:1] | passed | 0.00062 seconds |
4
- ./spec/candidate_worker_spec.rb[1:1] | passed | 0.00023 seconds |
5
- ./spec/candidate_worker_spec.rb[1:2] | passed | 0.00053 seconds |
6
- ./spec/candidate_worker_spec.rb[1:3:1] | passed | 0.00069 seconds |
7
- ./spec/candidate_worker_spec.rb[1:3:2:1] | passed | 0.00209 seconds |
8
- ./spec/candidate_worker_spec.rb[1:3:3:1] | passed | 0.0008 seconds |
9
- ./spec/experiment_control_spec.rb[1:1:1] | passed | 0.00089 seconds |
10
- ./spec/experiment_control_spec.rb[1:1:2] | passed | 0.00117 seconds |
11
- ./spec/experiment_control_spec.rb[1:1:3] | passed | 0.00061 seconds |
12
- ./spec/experiment_error_worker_spec.rb[1:1] | passed | 0.00266 seconds |
13
- ./spec/experiment_error_worker_spec.rb[1:2] | passed | 0.00056 seconds |
14
- ./spec/experiment_error_worker_spec.rb[1:3] | passed | 0.0077 seconds |
15
- ./spec/experiment_result_spec.rb[1:1:1] | passed | 0.00042 seconds |
16
- ./spec/experiment_result_spec.rb[1:2:1] | passed | 0.00043 seconds |
17
- ./spec/experiment_result_spec.rb[1:3:1] | passed | 0.00071 seconds |
18
- ./spec/experiment_result_spec.rb[1:3:2] | passed | 0.00045 seconds |
19
- ./spec/experiment_result_spec.rb[1:3:3:1] | passed | 0.00055 seconds |
20
- ./spec/experiment_result_spec.rb[1:3:3:2] | passed | 0.00048 seconds |
21
- ./spec/experiment_result_spec.rb[1:3:4:1] | passed | 0.00043 seconds |
22
- ./spec/experiment_result_spec.rb[1:3:4:2] | passed | 0.00058 seconds |
23
- ./spec/experiment_result_spec.rb[1:4:1:1] | passed | 0.00037 seconds |
24
- ./spec/experiment_result_spec.rb[1:4:2:1:1] | passed | 0.00095 seconds |
25
- ./spec/experiment_result_spec.rb[1:4:2:1:2] | passed | 0.00055 seconds |
26
- ./spec/experiment_result_spec.rb[1:4:2:2:1] | passed | 0.0005 seconds |
27
- ./spec/experiment_result_worker_spec.rb[1:1] | passed | 0.00048 seconds |
28
- ./spec/experiment_result_worker_spec.rb[1:2:1] | passed | 0.00175 seconds |
29
- ./spec/experiment_result_worker_spec.rb[1:3:1:1] | passed | 0.0013 seconds |
30
- ./spec/experiment_result_worker_spec.rb[1:3:2:1] | passed | 0.00235 seconds |
31
- ./spec/experiment_result_worker_spec.rb[1:3:2:2] | passed | 0.0025 seconds |
32
- ./spec/util_spec.rb[1:1:1] | passed | 0.00021 seconds |
33
- ./spec/util_spec.rb[1:2:1] | passed | 0.00016 seconds |
34
- ./spec/util_spec.rb[1:3:1] | passed | 0.00014 seconds |
35
- ./spec/util_spec.rb[1:3:2] | passed | 0.00016 seconds |
36
- ./spec/util_spec.rb[1:3:3] | passed | 0.00012 seconds |
37
- ./spec/util_spec.rb[1:3:4] | passed | 0.00011 seconds |
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/experiment_result_worker"
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(name, candidate: candidate_config)" do
38
- it "triggers an ExperimentResultWorker with the control output and duration" do
39
- expect(AsyncExperiments::ExperimentResultWorker).to receive(:perform_async)
40
- .with(name, id, run_output, instance_of(Float), :control)
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, rpush: nil) }
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(described_class.jobs.size).to eq(1)
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
- it "stores the exception for later reporting" do
33
- expect(redis).to receive(:rpush).with("experiments:#{name}:exceptions", error)
34
- subject.perform(name, error)
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(:name) { :test_experiment }
7
-
8
- let(:control_run_output) { "control output" }
9
- let(:control_duration) { 10.0 }
10
-
11
- let(:candidate_run_output) { "candidate output" }
12
- let(:candidate_duration) { 5.0 }
13
-
14
- let(:redis) { double(:redis, del: nil) }
15
- let(:statsd) { double(:statsd, timing: nil, increment: nil) }
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
- it "stores the branch's run output and duration" do
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("experiments:#{name}:#{id}:candidate", {
30
- run_output: candidate_run_output,
31
- duration: candidate_duration,
32
- }.to_json)
36
+ .with(redis_key, { run_output: output, duration: duration }.to_json)
37
+ end
33
38
 
34
- candidate.store_run_output
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 "control#process_run_output(candidate)" do
39
- before do
40
- allow(redis).to receive(:rpush)
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 "reports the control and candidate durations to statsd" do
59
+ it "stores the durations with statsd" do
44
60
  expect(statsd).to receive(:timing)
45
- .with("experiments.#{name}.control", control_duration)
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
- control.process_run_output(candidate)
66
+ it "deletes candidate data" do
67
+ expect(redis).to receive(:del).with(candidate_key)
51
68
  end
52
69
 
53
- it "deletes the candidate data from redis" do
54
- expect(redis).to receive(:del).with("experiments:#{name}:#{id}:candidate")
70
+ context "when candidate data is different to control data" do
71
+ let(:candidate_output) { "different" }
55
72
 
56
- control.process_run_output(candidate)
57
- end
73
+ it "increments mismatch count with statsd" do
74
+ expect(statsd).to receive(:increment).with("experiments.#{name}.mismatches")
75
+ end
58
76
 
59
- context "if there's variation between the outputs" do
60
- it "increments the mismatch count in statsd" do
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
- control.process_run_output(candidate)
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
- it "logs the mismatch to redis" do
68
- expect(redis).to receive(:rpush).with(
69
- "experiments:#{name}:mismatches",
70
- [["~", "", "control output", "candidate output"]].to_json,
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
- control.process_run_output(candidate)
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 "if there's no variation" do
78
- let(:candidate_run_output) { control_run_output }
100
+ context "when candidate data is the same as control data" do
101
+ let(:candidate_output) { output }
79
102
 
80
- it "does not increment the mismatch count" do
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 "does not log the mismatch to redis" do
107
+ it "doesn't add a difference to redis" do
86
108
  expect(redis).not_to receive(:rpush)
87
- control.process_run_output(candidate)
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 ".new" do
93
- let(:type) { :candidate }
117
+ describe "#available?" do
118
+ subject { described_class.new(name, id, type, redis, statsd, output, duration).available? }
94
119
 
95
- context "if duration and output are provided" do
96
- it "uses those" do
97
- candidate = described_class.new(name, id, type, redis, statsd, "arbitrary output", 1.23)
98
- expect(candidate.run_output).to eq("arbitrary output")
99
- expect(candidate.duration).to eq(1.23)
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
- before do
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 "is considered available" do
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 does not have the data" do
125
- before do
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 "is considered unavailable" do
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