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.
@@ -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