verdict 0.15.2 → 0.16.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/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
|