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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50c44fd8fcc0cf5605c9d21fd5eefcd4dddeffeb03c48e7ec046639d7154715b
4
- data.tar.gz: d55759fff1145aa793f9ae42232518130ba93efcb5fb62a2f7cbfa24d0322a0f
3
+ metadata.gz: c8ad702f655cca4734df9f1019f7df665cbe04914468bdcb07ae2a5e9fcc27ca
4
+ data.tar.gz: f31057b426bb631a36d99bc9a6f1588078b0009909ad25c2f5daa2180a407d66
5
5
  SHA512:
6
- metadata.gz: 8ed63a2aaf52ebffe454da41e3022e87df834b80130cb0ab14902466ea6d630ce9d046e1b202e32fc6491a2dc9974f4f825f50aaa14aa862103888f31d5c3df6
7
- data.tar.gz: 12c379615c6593caf46749157ebca884d91fb17a97d81b7fc60c53cd3531785a0a0c078e79e4b387cd21676f26d55962e0a2ab1ba0caeacb5d99208c3f1e8f8d
6
+ metadata.gz: 1ac4f9f4cef8e978fc36ea7848cee6b4ab15cb895d57598388f4b42997f141189a51c511f4a4b877f254b8a0d32909a153aa00da8b7e42940dbaed6d7ec9e4ec
7
+ data.tar.gz: f03f49ca42347dc3bf54c0ab449df49df32e6b5fc175392e3259b4e238324de03406f64d38a17e8744f662ec9543319a41bebec0df12031f9f0ab03bb05253c2
@@ -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
- @redis = redis
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.hget(scope_key(scope), key)
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.hset(scope_key(scope), key, value)
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.hdel(scope_key(scope), key)
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.del(scope_key(scope))
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.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
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
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.15.2"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -1,97 +1,75 @@
1
1
  require 'test_helper'
2
2
 
3
- class RedisStorageTest < Minitest::Test
4
3
 
5
- def setup
6
- @redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
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 @redis.hexists(experiment_key, 'assignment_subject_1')
8
+ refute redis.hexists(experiment_key, 'assignment_subject_1')
21
9
 
22
- new_assignment = @experiment.assign('subject_1')
10
+ new_assignment = experiment.assign('subject_1')
23
11
  assert new_assignment.qualified?
24
12
  refute new_assignment.returning?
25
13
 
26
- assert @redis.hexists(experiment_key, 'assignment_subject_1')
14
+ assert redis.hexists(experiment_key, 'assignment_subject_1')
27
15
 
28
- returning_assignment = @experiment.assign('subject_1')
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 @redis.hexists(experiment_key, 'assignment_subject_2')
23
+ refute redis.hexists(experiment_key, 'assignment_subject_2')
36
24
 
37
- new_assignment = @experiment.assign('subject_2')
25
+ new_assignment = experiment.assign('subject_2')
38
26
 
39
27
  refute new_assignment.returning?
40
28
  refute new_assignment.qualified?
41
- assert @redis.hexists(experiment_key, 'assignment_subject_2')
29
+ assert redis.hexists(experiment_key, 'assignment_subject_2')
42
30
 
43
- returning_assignment = @experiment.assign('subject_2')
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
- @experiment.assign('subject_3')
62
- assert @redis.hexists(experiment_key, 'assignment_subject_3')
63
- @experiment.remove_subject_assignment('subject_3')
64
- refute @redis.hexists(experiment_key, 'assignment_subject_3')
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 @redis.hexists(experiment_key, "started_at")
69
- a = @experiment.send(:ensure_experiment_has_started)
70
- assert @redis.hexists(experiment_key, "started_at")
71
- assert_equal a, @experiment.started_at
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
- @experiment.assign("something_#{n}")
54
+ experiment.assign("something_#{n}")
77
55
  end
78
56
 
79
- assert_operator @redis, :exists, experiment_key
57
+ assert_operator redis, :exists, experiment_key
80
58
 
81
59
  Verdict.default_logger.expects(:warn).with(regexp_matches(/deprecated/))
82
- @storage.cleanup(:redis_storage)
83
- refute_operator @redis, :exists, experiment_key
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
- @experiment.assign("something_#{n}")
66
+ experiment.assign("something_#{n}")
89
67
  end
90
68
 
91
- assert_operator @redis, :exists, experiment_key
69
+ assert_operator redis, :exists, experiment_key
92
70
 
93
- @storage.cleanup(@experiment)
94
- refute_operator @redis, :exists, experiment_key
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
@@ -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
@@ -25,4 +25,5 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency("timecop")
26
26
  gem.add_development_dependency("redis")
27
27
  gem.add_development_dependency("rails")
28
+ gem.add_development_dependency("connection_pool")
28
29
  end
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.15.2
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-15 00:00:00.000000000 Z
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