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