verdict 0.13.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/CHANGELOG.md +15 -0
- data/lib/verdict/experiment.rb +46 -16
- data/lib/verdict/storage/redis_storage.rb +33 -6
- data/lib/verdict/version.rb +1 -1
- data/test/experiment_test.rb +125 -0
- data/test/storage/redis_storage_test.rb +90 -48
- data/test/test_helper.rb +1 -0
- data/verdict.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8ad702f655cca4734df9f1019f7df665cbe04914468bdcb07ae2a5e9fcc27ca
|
4
|
+
data.tar.gz: f31057b426bb631a36d99bc9a6f1588078b0009909ad25c2f5daa2180a407d66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ac4f9f4cef8e978fc36ea7848cee6b4ab15cb895d57598388f4b42997f141189a51c511f4a4b877f254b8a0d32909a153aa00da8b7e42940dbaed6d7ec9e4ec
|
7
|
+
data.tar.gz: f03f49ca42347dc3bf54c0ab449df49df32e6b5fc175392e3259b4e238324de03406f64d38a17e8744f662ec9543319a41bebec0df12031f9f0ab03bb05253c2
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## v0.16.0
|
2
|
+
* Allow configuring the `RedisStorage` with a [`ConnectionPool`](https://github.com/mperham/connection_pool) instead of a raw `Redis` connection.
|
3
|
+
|
4
|
+
## v0.15.2
|
5
|
+
* Fix edge case where inheriting from `Verdict::Experiment` and overriding `subject_qualifies?` resulted in an error.
|
6
|
+
|
7
|
+
## v0.15.1
|
8
|
+
* Make the `dynamic_qualifies` parameter in `Verdict::Experiment#subject_qualifies?` optional. This fixes a bug where users that were previously calling this method directly experienced issues after v0.15.0
|
9
|
+
|
10
|
+
## v0.15.0
|
11
|
+
* Add optional `qualifiers` parameter to the `Verdict::Experiment#switch` method. This parameter accepts an array of procs and is used as additional qualifiers. The purpose of this parameter is to allow users to define qualification logic outside of the experiment definition.
|
12
|
+
|
13
|
+
## v0.14.0
|
14
|
+
* Add optional experiment definition method `schedule_stop_new_assignment_timestamp` to support limiting experiment's assignment lifetime with another pre-determined time interval. It allows users to have an assignment cooldown period for stable analysis of the experiment results. Experiment's lifetime now becomes: start experiment -> stop new assignments -> end experiment.
|
15
|
+
|
1
16
|
## v0.13.0
|
2
17
|
|
3
18
|
* Add optional experiment definition methods `schedule_start_timestamp` and `schedule_end_timestamp` to support limiting experiment's lifetime in a pre-determined time interval.
|
data/lib/verdict/experiment.rb
CHANGED
@@ -52,9 +52,17 @@ class Verdict::Experiment
|
|
52
52
|
return self
|
53
53
|
end
|
54
54
|
|
55
|
-
# Optional: Together with the "
|
56
|
-
# the given time interval.
|
57
|
-
#
|
55
|
+
# Optional: Together with the "end_timestamp" and "stop_new_assignment_timestamp", limits the experiment run timeline within
|
56
|
+
# the given time interval.
|
57
|
+
#
|
58
|
+
# Timestamps definitions:
|
59
|
+
# start_timestamp: Experiment's start time. No assignments are made i.e. switch will return nil before this timestamp.
|
60
|
+
# stop_new_assignment_timestamp: Experiment's new assignment stop time. No new assignments are made
|
61
|
+
# i.e. switch returns nil for new assignments but the existing assignments are preserved.
|
62
|
+
# end_timestamp: Experiment's end time. No assignments are made i.e. switch returns nil after this timestamp.
|
63
|
+
#
|
64
|
+
# Experiment run timeline:
|
65
|
+
# start_timestamp -> (new assignments occur) -> stop_new_assignment_timestamp -> (no new assignments occur) -> end_timestamp
|
58
66
|
def schedule_start_timestamp(timestamp)
|
59
67
|
@schedule_start_timestamp = timestamp
|
60
68
|
end
|
@@ -63,6 +71,10 @@ class Verdict::Experiment
|
|
63
71
|
@schedule_end_timestamp = timestamp
|
64
72
|
end
|
65
73
|
|
74
|
+
def schedule_stop_new_assignment_timestamp(timestamp)
|
75
|
+
@schedule_stop_new_assignment_timestamp = timestamp
|
76
|
+
end
|
77
|
+
|
66
78
|
def rollout_percentage(percentage, rollout_group_name = :enabled)
|
67
79
|
groups(Verdict::Segmenters::RolloutSegmenter) do
|
68
80
|
group rollout_group_name, percentage
|
@@ -130,18 +142,18 @@ class Verdict::Experiment
|
|
130
142
|
raise unless disqualify_empty_identifier?
|
131
143
|
end
|
132
144
|
|
133
|
-
def assign(subject, context = nil)
|
145
|
+
def assign(subject, context = nil, dynamic_qualifiers: [])
|
134
146
|
previous_assignment = lookup(subject)
|
135
147
|
|
136
148
|
subject_identifier = retrieve_subject_identifier(subject)
|
137
149
|
assignment = if previous_assignment
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
150
|
+
previous_assignment
|
151
|
+
elsif dynamic_subject_qualifies?(subject, dynamic_qualifiers, context) && is_make_new_assignments?
|
152
|
+
group = segmenter.assign(subject_identifier, subject, context)
|
153
|
+
subject_assignment(subject, group, nil, group.nil?)
|
154
|
+
else
|
155
|
+
nil_assignment(subject)
|
156
|
+
end
|
145
157
|
|
146
158
|
store_assignment(assignment)
|
147
159
|
rescue Verdict::StorageError
|
@@ -182,9 +194,11 @@ class Verdict::Experiment
|
|
182
194
|
@storage.remove_assignment(self, subject)
|
183
195
|
end
|
184
196
|
|
185
|
-
|
197
|
+
# The qualifiers param accepts an array of procs.
|
198
|
+
# This is intended for qualification logic that cannot be defined in the experiment definition
|
199
|
+
def switch(subject, context = nil, qualifiers: [])
|
186
200
|
return unless is_scheduled?
|
187
|
-
assign(subject, context).to_sym
|
201
|
+
assign(subject, context, dynamic_qualifiers: qualifiers).to_sym
|
188
202
|
end
|
189
203
|
|
190
204
|
def lookup(subject)
|
@@ -229,8 +243,9 @@ class Verdict::Experiment
|
|
229
243
|
@disqualify_empty_identifier
|
230
244
|
end
|
231
245
|
|
232
|
-
def subject_qualifies?(subject, context = nil)
|
246
|
+
def subject_qualifies?(subject, context = nil, dynamic_qualifiers: [])
|
233
247
|
ensure_experiment_has_started
|
248
|
+
return false unless dynamic_qualifiers.all? { |qualifier| qualifier.call(subject) }
|
234
249
|
everybody_qualifies? || @qualifiers.all? { |qualifier| qualifier.call(subject, context) }
|
235
250
|
end
|
236
251
|
|
@@ -268,12 +283,27 @@ class Verdict::Experiment
|
|
268
283
|
private
|
269
284
|
|
270
285
|
def is_scheduled?
|
271
|
-
if @schedule_start_timestamp
|
286
|
+
if @schedule_start_timestamp && @schedule_start_timestamp > Time.now
|
272
287
|
return false
|
273
288
|
end
|
274
|
-
if @schedule_end_timestamp
|
289
|
+
if @schedule_end_timestamp && @schedule_end_timestamp <= Time.now
|
275
290
|
return false
|
276
291
|
end
|
277
292
|
return true
|
278
293
|
end
|
294
|
+
|
295
|
+
def is_make_new_assignments?
|
296
|
+
return !(@schedule_stop_new_assignment_timestamp && @schedule_stop_new_assignment_timestamp <= Time.now)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Used when a Experiment class has overridden the subject_qualifies? method prior to v0.15.0
|
300
|
+
# The previous version of subject_qualifies did not accept dynamic qualifiers, thus this is used to
|
301
|
+
# determine how many parameters to pass
|
302
|
+
def dynamic_subject_qualifies?(subject, dynamic_qualifiers, context)
|
303
|
+
if method(:subject_qualifies?).parameters.include?([:key, :dynamic_qualifiers])
|
304
|
+
subject_qualifies?(subject, context, dynamic_qualifiers: dynamic_qualifiers)
|
305
|
+
else
|
306
|
+
subject_qualifies?(subject, context)
|
307
|
+
end
|
308
|
+
end
|
279
309
|
end
|
@@ -6,33 +6,56 @@ module Verdict
|
|
6
6
|
attr_accessor :redis, :key_prefix
|
7
7
|
|
8
8
|
def initialize(redis = nil, options = {})
|
9
|
-
|
9
|
+
if !redis.nil? && !redis.respond_to?(:with)
|
10
|
+
@redis = ConnectionPoolLike.new(redis)
|
11
|
+
else
|
12
|
+
@redis = redis
|
13
|
+
end
|
14
|
+
|
10
15
|
@key_prefix = options[:key_prefix] || 'experiments/'
|
11
16
|
end
|
12
17
|
|
13
18
|
protected
|
14
19
|
|
20
|
+
class ConnectionPoolLike
|
21
|
+
def initialize(redis)
|
22
|
+
@redis = redis
|
23
|
+
end
|
24
|
+
|
25
|
+
def with
|
26
|
+
yield @redis
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
15
30
|
def get(scope, key)
|
16
|
-
redis.
|
31
|
+
redis.with do |conn|
|
32
|
+
conn.hget(scope_key(scope), key)
|
33
|
+
end
|
17
34
|
rescue ::Redis::BaseError => e
|
18
35
|
raise Verdict::StorageError, "Redis error: #{e.message}"
|
19
36
|
end
|
20
37
|
|
21
38
|
def set(scope, key, value)
|
22
|
-
redis.
|
39
|
+
redis.with do |conn|
|
40
|
+
conn.hset(scope_key(scope), key, value)
|
41
|
+
end
|
23
42
|
rescue ::Redis::BaseError => e
|
24
43
|
raise Verdict::StorageError, "Redis error: #{e.message}"
|
25
44
|
end
|
26
45
|
|
27
46
|
def remove(scope, key)
|
28
|
-
redis.
|
47
|
+
redis.with do |conn|
|
48
|
+
conn.hdel(scope_key(scope), key)
|
49
|
+
end
|
29
50
|
rescue ::Redis::BaseError => e
|
30
51
|
raise Verdict::StorageError, "Redis error: #{e.message}"
|
31
52
|
end
|
32
53
|
|
33
54
|
def clear(scope, options)
|
34
55
|
scrub(scope)
|
35
|
-
redis.
|
56
|
+
redis.with do |conn|
|
57
|
+
conn.del(scope_key(scope))
|
58
|
+
end
|
36
59
|
rescue ::Redis::BaseError => e
|
37
60
|
raise Verdict::StorageError, "Redis error: #{e.message}"
|
38
61
|
end
|
@@ -44,10 +67,14 @@ module Verdict
|
|
44
67
|
end
|
45
68
|
|
46
69
|
def scrub(scope, cursor: 0)
|
47
|
-
cursor, results = redis.
|
70
|
+
cursor, results = redis.with do |conn|
|
71
|
+
conn.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
|
72
|
+
end
|
73
|
+
|
48
74
|
results.map(&:first).each do |key|
|
49
75
|
remove(scope, key)
|
50
76
|
end
|
77
|
+
|
51
78
|
scrub(scope, cursor: cursor) unless cursor.to_i.zero?
|
52
79
|
end
|
53
80
|
end
|
data/lib/verdict/version.rb
CHANGED
data/test/experiment_test.rb
CHANGED
@@ -565,9 +565,134 @@ class ExperimentTest < Minitest::Test
|
|
565
565
|
end
|
566
566
|
end
|
567
567
|
|
568
|
+
def test_is_stop_new_assignments
|
569
|
+
e = Verdict::Experiment.new('test') do
|
570
|
+
groups do
|
571
|
+
group :a, :half
|
572
|
+
group :b, :half
|
573
|
+
end
|
574
|
+
schedule_stop_new_assignment_timestamp Time.new(2020, 1, 15)
|
575
|
+
end
|
576
|
+
|
577
|
+
# new assignments stopped after the stop timestamp
|
578
|
+
Timecop.freeze(Time.new(2020, 1, 16)) do
|
579
|
+
assert !e.send(:is_make_new_assignments?)
|
580
|
+
assert_nil e.switch(1)
|
581
|
+
end
|
582
|
+
# new assignments didn't stop before the stop timestamp
|
583
|
+
Timecop.freeze(Time.new(2020, 1, 3)) do
|
584
|
+
assert e.send(:is_make_new_assignments?)
|
585
|
+
assert :a, e.switch(2)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def test_switch_preserves_old_assignments_after_stop_new_assignments_timestamp
|
590
|
+
e = Verdict::Experiment.new('test') do
|
591
|
+
groups do
|
592
|
+
group :a, :half
|
593
|
+
group :b, :half
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
assert_equal :a, e.switch(1)
|
598
|
+
|
599
|
+
e.schedule_stop_new_assignment_timestamp Time.new(2020, 4, 15)
|
600
|
+
|
601
|
+
# switch respects to stop new assignment timestamp, old assignment preserves, new assignment returns nil
|
602
|
+
Timecop.freeze(Time.new(2020, 4, 16)) do
|
603
|
+
assert !e.send(:is_make_new_assignments?)
|
604
|
+
# old assignment stay the same
|
605
|
+
assert_equal :a, e.switch(1)
|
606
|
+
# new assignment returns nil
|
607
|
+
assert_nil e.switch(2)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
def test_schedule_start_timestamp_and_stop_new_assignemnt_timestamp_are_inclusive_but_end_timestamp_is_exclusive
|
612
|
+
e = Verdict::Experiment.new('test') do
|
613
|
+
groups do
|
614
|
+
group :a, :half
|
615
|
+
group :b, :half
|
616
|
+
end
|
617
|
+
|
618
|
+
schedule_start_timestamp Time.new(2020, 1, 1)
|
619
|
+
schedule_stop_new_assignment_timestamp Time.new(2020, 1, 15)
|
620
|
+
schedule_end_timestamp Time.new(2020, 1, 31)
|
621
|
+
end
|
622
|
+
|
623
|
+
# start_timestamp is included
|
624
|
+
Timecop.freeze(Time.new(2020, 1, 1)) do
|
625
|
+
assert e.send(:is_scheduled?)
|
626
|
+
assert_equal :a, e.switch(1)
|
627
|
+
end
|
628
|
+
|
629
|
+
# stop_new_assignment_timestamp is included
|
630
|
+
Timecop.freeze(Time.new(2020, 1, 15)) do
|
631
|
+
assert !e.send(:is_make_new_assignments?)
|
632
|
+
# old assignment preserved
|
633
|
+
assert_equal :a, e.switch(1)
|
634
|
+
# new assignment returns nil
|
635
|
+
assert_nil e.switch(2)
|
636
|
+
end
|
637
|
+
|
638
|
+
# end_timestamp is excluded
|
639
|
+
Timecop.freeze(Time.new(2020, 1, 31)) do
|
640
|
+
assert !e.send(:is_scheduled?)
|
641
|
+
assert_nil e.switch(1)
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
def test_custom_qualifiers_success
|
646
|
+
e = Verdict::Experiment.new('test') do
|
647
|
+
groups do
|
648
|
+
group :all, 100
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
subject = 2
|
653
|
+
custom_qualifier_a = Proc.new { |subject| subject.even? }
|
654
|
+
custom_qualifier_b = Proc.new { |subject| subject > 0 }
|
655
|
+
|
656
|
+
group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
|
657
|
+
assert_equal e.group(:all).to_sym, group
|
658
|
+
end
|
659
|
+
|
660
|
+
def test_custom_qualifiers_failure
|
661
|
+
e = Verdict::Experiment.new('test') do
|
662
|
+
groups do
|
663
|
+
group :all, 100
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
subject = 3
|
668
|
+
custom_qualifier_a = Proc.new { |subject| subject.even? }
|
669
|
+
custom_qualifier_b = Proc.new { |subject| subject > 0 }
|
670
|
+
|
671
|
+
group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
|
672
|
+
assert_nil group
|
673
|
+
end
|
674
|
+
|
675
|
+
def test_dynamic_subject_qualifies_call_overridden_method
|
676
|
+
e = MyExperiment.new('test') do
|
677
|
+
groups do
|
678
|
+
group :all, 100
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
group = e.switch(4)
|
683
|
+
assert_nil group
|
684
|
+
end
|
685
|
+
|
568
686
|
private
|
569
687
|
|
570
688
|
def redis
|
571
689
|
@redis ||= ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
|
572
690
|
end
|
573
691
|
end
|
692
|
+
|
693
|
+
class MyExperiment < Verdict::Experiment
|
694
|
+
def subject_qualifies?(subject, context = nil)
|
695
|
+
return false if subject.even?
|
696
|
+
super
|
697
|
+
end
|
698
|
+
end
|
@@ -1,97 +1,75 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class RedisStorageTest < Minitest::Test
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
@storage = storage = Verdict::Storage::RedisStorage.new(@redis)
|
8
|
-
@experiment = Verdict::Experiment.new(:redis_storage) do
|
9
|
-
qualify { |s| s == 'subject_1' }
|
10
|
-
groups { group :all, 100 }
|
11
|
-
storage storage, store_unqualified: true
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def teardown
|
16
|
-
@redis.del('experiments/redis_storage')
|
17
|
-
end
|
4
|
+
module SharedRedisStorageTests
|
5
|
+
attr_reader :redis, :experiment, :storage
|
18
6
|
|
19
7
|
def test_store_and_retrieve_qualified_assignment
|
20
|
-
refute
|
8
|
+
refute redis.hexists(experiment_key, 'assignment_subject_1')
|
21
9
|
|
22
|
-
new_assignment =
|
10
|
+
new_assignment = experiment.assign('subject_1')
|
23
11
|
assert new_assignment.qualified?
|
24
12
|
refute new_assignment.returning?
|
25
13
|
|
26
|
-
assert
|
14
|
+
assert redis.hexists(experiment_key, 'assignment_subject_1')
|
27
15
|
|
28
|
-
returning_assignment =
|
16
|
+
returning_assignment = experiment.assign('subject_1')
|
29
17
|
assert returning_assignment.returning?
|
30
18
|
assert_equal new_assignment.experiment, returning_assignment.experiment
|
31
19
|
assert_equal new_assignment.group, returning_assignment.group
|
32
20
|
end
|
33
21
|
|
34
22
|
def test_store_and_retrieve_unqualified_assignment
|
35
|
-
refute
|
23
|
+
refute redis.hexists(experiment_key, 'assignment_subject_2')
|
36
24
|
|
37
|
-
new_assignment =
|
25
|
+
new_assignment = experiment.assign('subject_2')
|
38
26
|
|
39
27
|
refute new_assignment.returning?
|
40
28
|
refute new_assignment.qualified?
|
41
|
-
assert
|
29
|
+
assert redis.hexists(experiment_key, 'assignment_subject_2')
|
42
30
|
|
43
|
-
returning_assignment =
|
31
|
+
returning_assignment = experiment.assign('subject_2')
|
44
32
|
assert returning_assignment.returning?
|
45
33
|
assert_equal new_assignment.experiment, returning_assignment.experiment
|
46
34
|
assert_nil new_assignment.group
|
47
35
|
assert_nil returning_assignment.group
|
48
36
|
end
|
49
37
|
|
50
|
-
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
|
51
|
-
@redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
|
52
|
-
assert !@experiment.assign('subject_1').qualified?
|
53
|
-
end
|
54
|
-
|
55
|
-
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
|
56
|
-
@redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
|
57
|
-
assert !@experiment.assign('subject_1').qualified?
|
58
|
-
end
|
59
|
-
|
60
38
|
def test_remove_subject_assignment
|
61
|
-
|
62
|
-
assert
|
63
|
-
|
64
|
-
refute
|
39
|
+
experiment.assign('subject_3')
|
40
|
+
assert redis.hexists(experiment_key, 'assignment_subject_3')
|
41
|
+
experiment.remove_subject_assignment('subject_3')
|
42
|
+
refute redis.hexists(experiment_key, 'assignment_subject_3')
|
65
43
|
end
|
66
44
|
|
67
45
|
def test_started_at
|
68
|
-
refute
|
69
|
-
a =
|
70
|
-
assert
|
71
|
-
assert_equal a,
|
46
|
+
refute redis.hexists(experiment_key, "started_at")
|
47
|
+
a = experiment.send(:ensure_experiment_has_started)
|
48
|
+
assert redis.hexists(experiment_key, "started_at")
|
49
|
+
assert_equal a, experiment.started_at
|
72
50
|
end
|
73
51
|
|
74
52
|
def test_cleanup_with_scope_argument
|
75
53
|
1000.times do |n|
|
76
|
-
|
54
|
+
experiment.assign("something_#{n}")
|
77
55
|
end
|
78
56
|
|
79
|
-
assert_operator
|
57
|
+
assert_operator redis, :exists, experiment_key
|
80
58
|
|
81
59
|
Verdict.default_logger.expects(:warn).with(regexp_matches(/deprecated/))
|
82
|
-
|
83
|
-
refute_operator
|
60
|
+
storage.cleanup(:redis_storage)
|
61
|
+
refute_operator redis, :exists, experiment_key
|
84
62
|
end
|
85
63
|
|
86
64
|
def test_cleanup
|
87
65
|
1000.times do |n|
|
88
|
-
|
66
|
+
experiment.assign("something_#{n}")
|
89
67
|
end
|
90
68
|
|
91
|
-
assert_operator
|
69
|
+
assert_operator redis, :exists, experiment_key
|
92
70
|
|
93
|
-
|
94
|
-
refute_operator
|
71
|
+
storage.cleanup(experiment)
|
72
|
+
refute_operator redis, :exists, experiment_key
|
95
73
|
end
|
96
74
|
|
97
75
|
private
|
@@ -100,3 +78,67 @@ class RedisStorageTest < Minitest::Test
|
|
100
78
|
"experiments/redis_storage"
|
101
79
|
end
|
102
80
|
end
|
81
|
+
|
82
|
+
class RedisStorageTest < Minitest::Test
|
83
|
+
include SharedRedisStorageTests
|
84
|
+
|
85
|
+
def setup
|
86
|
+
@redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
|
87
|
+
@storage = storage = Verdict::Storage::RedisStorage.new(@redis)
|
88
|
+
@experiment = Verdict::Experiment.new(:redis_storage) do
|
89
|
+
qualify { |s| s == 'subject_1' }
|
90
|
+
groups { group :all, 100 }
|
91
|
+
storage storage, store_unqualified: true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def teardown
|
96
|
+
@redis.del('experiments/redis_storage')
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
|
100
|
+
redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
|
101
|
+
assert !experiment.assign('subject_1').qualified?
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
|
105
|
+
redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
|
106
|
+
assert !experiment.assign('subject_1').qualified?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class RedisStorageConnectionPoolTest < Minitest::Test
|
111
|
+
include SharedRedisStorageTests
|
112
|
+
|
113
|
+
def setup
|
114
|
+
@redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
|
115
|
+
@redis_pool = ::ConnectionPool.new(size: 3) do
|
116
|
+
::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
|
117
|
+
end
|
118
|
+
@storage = storage = Verdict::Storage::RedisStorage.new(@redis_pool)
|
119
|
+
@experiment = Verdict::Experiment.new(:redis_storage) do
|
120
|
+
qualify { |s| s == 'subject_1' }
|
121
|
+
groups { group :all, 100 }
|
122
|
+
storage storage, store_unqualified: true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def teardown
|
127
|
+
@redis.del('experiments/redis_storage')
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
|
131
|
+
redis_mock = stub
|
132
|
+
redis_mock.stubs(:hget).returns(nil)
|
133
|
+
redis_mock.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
|
134
|
+
@redis_pool.stubs(:with).yields(redis_mock)
|
135
|
+
assert !@experiment.assign('subject_1').qualified?
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_remove_subject_assignment
|
139
|
+
@experiment.assign('subject_3')
|
140
|
+
assert @redis.hexists(experiment_key, 'assignment_subject_3')
|
141
|
+
@experiment.remove_subject_assignment('subject_3')
|
142
|
+
refute @redis.hexists(experiment_key, 'assignment_subject_3')
|
143
|
+
end
|
144
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -13,6 +13,7 @@ require "mocha/setup"
|
|
13
13
|
require "timecop"
|
14
14
|
require "verdict"
|
15
15
|
require "redis"
|
16
|
+
require "connection_pool"
|
16
17
|
|
17
18
|
REDIS_HOST = ENV['REDIS_HOST'].nil? ? '127.0.0.1' : ENV['REDIS_HOST']
|
18
19
|
REDIS_PORT = (ENV['REDIS_PORT'].nil? ? '6379' : ENV['REDIS_PORT']).to_i
|
data/verdict.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: verdict
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: connection_pool
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
description: Shopify Experiments classes
|
98
112
|
email:
|
99
113
|
- kevin.mcphillips@shopify.com
|