verdict 0.15.2 → 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/CHANGELOG.md +3 -0
- data/lib/verdict/storage/redis_storage.rb +33 -6
- data/lib/verdict/version.rb +1 -1
- 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/CHANGELOG.md
CHANGED
@@ -1,3 +1,6 @@
|
|
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
|
+
|
1
4
|
## v0.15.2
|
2
5
|
* Fix edge case where inheriting from `Verdict::Experiment` and overriding `subject_qualifies?` resulted in an error.
|
3
6
|
|
@@ -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
@@ -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-07-
|
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
|