hoodoo 1.13.0 → 1.14.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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hoodoo.rb +1 -0
  3. data/lib/hoodoo/services/services/session.rb +8 -104
  4. data/lib/hoodoo/transient_store.rb +19 -0
  5. data/lib/hoodoo/transient_store/mocks/dalli_client.rb +148 -0
  6. data/lib/hoodoo/transient_store/mocks/redis.rb +138 -0
  7. data/lib/hoodoo/transient_store/transient_store.rb +344 -0
  8. data/lib/hoodoo/transient_store/transient_store/base.rb +81 -0
  9. data/lib/hoodoo/transient_store/transient_store/memcached.rb +116 -0
  10. data/lib/hoodoo/transient_store/transient_store/memcached_redis_mirror.rb +181 -0
  11. data/lib/hoodoo/transient_store/transient_store/redis.rb +126 -0
  12. data/lib/hoodoo/version.rb +1 -1
  13. data/spec/active/active_record/support_spec.rb +3 -9
  14. data/spec/active/active_record/translated_spec.rb +2 -5
  15. data/spec/logger/writers/file_writer_spec.rb +1 -4
  16. data/spec/logger/writers/stream_writer_spec.rb +2 -9
  17. data/spec/services/middleware/middleware_logging_spec.rb +1 -4
  18. data/spec/services/middleware/middleware_permissions_spec.rb +2 -2
  19. data/spec/services/services/interface_spec.rb +2 -2
  20. data/spec/services/services/session_spec.rb +26 -19
  21. data/spec/transient_store/transient_store/base_spec.rb +52 -0
  22. data/spec/transient_store/transient_store/memcached_redis_mirror_spec.rb +380 -0
  23. data/spec/transient_store/transient_store/memcached_spec.rb +244 -0
  24. data/spec/transient_store/transient_store/mocks/dalli_client_spec.rb +44 -0
  25. data/spec/transient_store/transient_store/mocks/redis_spec.rb +28 -0
  26. data/spec/transient_store/transient_store/redis_spec.rb +242 -0
  27. data/spec/transient_store/transient_store_spec.rb +448 -0
  28. metadata +31 -9
@@ -0,0 +1,244 @@
1
+ require 'spec_helper'
2
+
3
+ require 'dalli'
4
+ require 'hoodoo/transient_store/mocks/dalli_client'
5
+
6
+ # ============================================================================
7
+
8
+ old_level = Dalli.logger.level
9
+ Dalli.logger.level = Logger::ERROR
10
+ client = ::Dalli::Client.new( 'localhost:11211' )
11
+ result = client.stats() rescue nil
12
+ Dalli.logger.level = old_level
13
+ $memcached_missing = result.is_a?( Hash ) == false || result[ 'localhost:11211' ].nil?
14
+
15
+ backends = [ :mock ]
16
+ backends << :real unless $memcached_missing
17
+
18
+ # ============================================================================
19
+
20
+ describe Hoodoo::TransientStore::Memcached do
21
+ before :all do
22
+ @old_level = Dalli.logger.level
23
+ Dalli.logger.level = Logger::ERROR
24
+ end
25
+
26
+ after :all do
27
+ Dalli.logger.level = @old_level
28
+ end
29
+
30
+ it 'registers itself' do
31
+ expect( Hoodoo::TransientStore.supported_storage_engines() ).to include( :memcached )
32
+ end
33
+
34
+ if $memcached_missing
35
+ pending "*** WARNING *** Memcached not present on 'localhost:11211', cannot test real engine"
36
+ end
37
+
38
+ shared_examples 'a Memcached abstraction' do | backend |
39
+
40
+ # Either expect something on the known mock backend instance or an unknown
41
+ # (any) real Dalli::Client instance. Can then call "to" - i.e. use:
42
+ #
43
+ # expect_dalli_client( backend ).to...
44
+ #
45
+ # +backend+:: Pass either Symbol ":mock" or ":real".
46
+ #
47
+ def expect_dalli_client( backend )
48
+ return case backend
49
+ when :mock
50
+ expect( @mock_dalli_client_instance )
51
+ else
52
+ expect_any_instance_of( ::Dalli::Client )
53
+ end
54
+ end
55
+
56
+ # ========================================================================
57
+
58
+ before :each do
59
+ @storage_engine_uri = 'localhost:11211'
60
+ @namespace = Hoodoo::UUID.generate()
61
+
62
+ if backend == :mock
63
+ Hoodoo::TransientStore::Mocks::DalliClient.reset()
64
+ @mock_dalli_client_instance = Hoodoo::TransientStore::Mocks::DalliClient.new
65
+
66
+ expect( ::Dalli::Client ).to(
67
+ receive( :new ).
68
+ with(
69
+ @storage_engine_uri,
70
+ hash_including( { :namespace => @namespace } )
71
+ ).
72
+ and_return( @mock_dalli_client_instance )
73
+ )
74
+ else
75
+ expect( ::Dalli::Client ).to(
76
+ receive( :new ).
77
+ with(
78
+ @storage_engine_uri,
79
+ hash_including( { :namespace => @namespace } )
80
+ ).
81
+ and_call_original()
82
+ )
83
+ end
84
+ end
85
+
86
+ # ========================================================================
87
+
88
+ context "#initialize (#{ backend })" do
89
+ it 'initialises' do
90
+ instance = Hoodoo::TransientStore::Memcached.new(
91
+ storage_host_uri: @storage_engine_uri,
92
+ namespace: @namespace
93
+ )
94
+
95
+ expect( instance ).to be_a( Hoodoo::TransientStore::Memcached )
96
+ end
97
+
98
+ it 'complains about strange Memcached behaviour' do
99
+ expect_dalli_client( backend ).to receive( :stats ).and_return( nil )
100
+
101
+ expect {
102
+ instance = Hoodoo::TransientStore::Memcached.new(
103
+ storage_host_uri: @storage_engine_uri,
104
+ namespace: @namespace
105
+ )
106
+ }.to raise_error(
107
+ RuntimeError,
108
+ "Hoodoo::TransientStore::Memcached: Did not get back meaningful data from Memcached at '#{ @storage_engine_uri }'"
109
+ )
110
+ end
111
+
112
+ it 'handles exceptions' do
113
+ expect_dalli_client( backend ).to receive( :stats ).and_raise( 'Hello world' )
114
+
115
+ expect {
116
+ instance = Hoodoo::TransientStore::Memcached.new(
117
+ storage_host_uri: @storage_engine_uri,
118
+ namespace: @namespace
119
+ )
120
+ }.to raise_error(
121
+ RuntimeError,
122
+ "Hoodoo::TransientStore::Memcached: Cannot connect to Memcached at '#{ @storage_engine_uri }': Hello world"
123
+ )
124
+ end
125
+ end
126
+
127
+ # ========================================================================
128
+
129
+ context "when initialised (#{ backend })" do
130
+ before :each do
131
+ @instance = Hoodoo::TransientStore::Memcached.new(
132
+ storage_host_uri: @storage_engine_uri,
133
+ namespace: @namespace
134
+ )
135
+
136
+ @key = Hoodoo::UUID.generate()
137
+ @payload = { 'bar' => 'baz' }
138
+ @ttl = 120
139
+ end
140
+
141
+ context '#set' do
142
+ it 'sets' do
143
+ expect_dalli_client( backend ).to receive( :set ).with( @key, @payload, @ttl ).and_call_original()
144
+
145
+ result = @instance.set(
146
+ key: @key,
147
+ payload: @payload,
148
+ maximum_lifespan: @ttl
149
+ )
150
+
151
+ expect( result ).to eq( true )
152
+ end
153
+
154
+ it 'allows exceptions to propagate' do
155
+ expect_dalli_client( backend ).to receive( :set ).with( @key, @payload, @ttl ).and_raise( 'Hello world' )
156
+
157
+ expect {
158
+ @instance.set(
159
+ key: @key,
160
+ payload: @payload,
161
+ maximum_lifespan: @ttl
162
+ )
163
+ }.to raise_error( RuntimeError, "Hello world" )
164
+ end
165
+ end
166
+
167
+ context '#get' do
168
+ before :each do
169
+ expect_dalli_client( backend ).to receive( :set ).with( @key, @payload, @ttl ).and_call_original()
170
+
171
+ @instance.set(
172
+ key: @key,
173
+ payload: @payload,
174
+ maximum_lifespan: @ttl
175
+ )
176
+ end
177
+
178
+ it 'gets known keys' do
179
+ expect_dalli_client( backend ).to receive( :get ).with( @key ).and_call_original()
180
+ expect( @instance.get( key: @key ) ).to eql( @payload )
181
+ end
182
+
183
+ it 'returns "nil" for unknown keys' do
184
+ expect( @instance.get( key: Hoodoo::UUID.generate() ) ).to be_nil
185
+ end
186
+
187
+ it 'allows exceptions to propagate' do
188
+ expect_dalli_client( backend ).to receive( :get ).with( @key ).and_raise( 'Hello world' )
189
+
190
+ expect {
191
+ @instance.get( key: @key )
192
+ }.to raise_error( RuntimeError, "Hello world" )
193
+ end
194
+ end
195
+
196
+ context '#delete' do
197
+ before :each do
198
+ expect_dalli_client( backend ).to receive( :set ).with( @key, @payload, @ttl ).and_call_original()
199
+
200
+ @instance.set(
201
+ key: @key,
202
+ payload: @payload,
203
+ maximum_lifespan: @ttl
204
+ )
205
+ end
206
+
207
+ it 'deletes known keys' do
208
+ expect( @instance.get( key: @key ) ).to eql( @payload )
209
+ expect_dalli_client( backend ).to receive( :delete ).with( @key ).and_call_original()
210
+
211
+ result = @instance.delete( key: @key )
212
+
213
+ expect( result ).to eq( true )
214
+ expect( @instance.get( key: @key ) ).to eql( nil )
215
+ end
216
+
217
+ it 'ignores unknown keys' do
218
+ expect( @instance.delete( key: Hoodoo::UUID.generate() ) ).to eql( true )
219
+ end
220
+
221
+ it 'allows exceptions to propagate' do
222
+ expect_dalli_client( backend ).to receive( :delete ).with( @key ).and_raise( 'Hello world' )
223
+
224
+ expect {
225
+ @instance.delete( key: @key )
226
+ }.to raise_error( RuntimeError, "Hello world" )
227
+ end
228
+ end
229
+
230
+ context 'close' do
231
+ it 'closes' do
232
+ expect_dalli_client( backend ).to receive( :close )
233
+ @instance.close()
234
+ end
235
+ end
236
+
237
+ end # 'context "when initialised (#{ backend })" do'
238
+ end # 'shared_examples ...'
239
+
240
+ backends.each do | backend |
241
+ it_behaves_like( 'a Memcached abstraction', backend )
242
+ end
243
+
244
+ end # 'describe...'
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ # Most of the mock client is covered by the TransientStore Memcached layer
4
+ # tests, which use the mock backend as well as a real one. Tests are implicit.
5
+ # This file contains any additional coverage not handled in
6
+ # transient_store/transient_store/memcached_spec.rb already.
7
+ #
8
+ describe Hoodoo::TransientStore::Mocks::DalliClient do
9
+
10
+ context 'sanctioned and maintained method' do
11
+ it '::store() returns the data store' do
12
+ Hoodoo::TransientStore::Mocks::DalliClient.reset()
13
+ mock_dalli_client_instance = Hoodoo::TransientStore::Mocks::DalliClient.new
14
+ mock_dalli_client_instance.set( 'foo', 'bar' )
15
+ expect( Hoodoo::TransientStore::Mocks::DalliClient.store ).to have_key( 'foo' )
16
+ end
17
+ end
18
+
19
+ # ==========================================================================
20
+
21
+ context 'deprecated but indefinitely maintained method' do
22
+
23
+ # Chicken and egg - we need to use the methods we're going to test in the
24
+ # test setup to avoid breaking or being broken by prior test execution!
25
+
26
+ before :all do
27
+ @old_bypass = Hoodoo::TransientStore::Mocks::DalliClient.bypass?
28
+ Hoodoo::TransientStore::Mocks::DalliClient.bypass( false )
29
+ end
30
+
31
+ after :all do
32
+ Hoodoo::TransientStore::Mocks::DalliClient.bypass( @old_bypass )
33
+ end
34
+
35
+ it '::bypass=(...) sets and ::bypass?() reads the "bypass" flag' do
36
+ Hoodoo::TransientStore::Mocks::DalliClient.bypass( true )
37
+ expect( Hoodoo::TransientStore::Mocks::DalliClient.bypass? ).to eql( true )
38
+
39
+ Hoodoo::TransientStore::Mocks::DalliClient.bypass( false )
40
+ expect( Hoodoo::TransientStore::Mocks::DalliClient.bypass? ).to eql( false )
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ # Most of the mock client is covered by the TransientStore Redis layer tests,
4
+ # which use the mock backend as well as a real one. Tests are implicit. This
5
+ # file contains any additional coverage not handled in
6
+ # transient_store/transient_store/redis_spec.rb already.
7
+ #
8
+ describe Hoodoo::TransientStore::Mocks::Redis do
9
+
10
+ context 'sanctioned and maintained method' do
11
+ it '::store() returns the data store' do
12
+ Hoodoo::TransientStore::Mocks::Redis.reset()
13
+ mock_redis_instance = Hoodoo::TransientStore::Mocks::Redis.new
14
+ mock_redis_instance.set( 'foo', 'bar' )
15
+ expect( Hoodoo::TransientStore::Mocks::Redis.store ).to have_key( 'foo' )
16
+ end
17
+
18
+ it '#expire(...) raises an error for attempts to expire bad keys' do
19
+ mock_redis_instance = Hoodoo::TransientStore::Mocks::Redis.new
20
+ key = Hoodoo::UUID.generate()
21
+
22
+ expect {
23
+ mock_redis_instance.expire( key, 60 )
24
+ }.to raise_error( RuntimeError, "Hoodoo::TransientStore::Mocks::Redis\#expire: Cannot find key '#{ key }'" )
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,242 @@
1
+ require 'spec_helper'
2
+
3
+ require 'redis'
4
+ require 'hoodoo/transient_store/mocks/redis'
5
+
6
+ # ============================================================================
7
+
8
+ client = ::Redis.new( :url => 'redis://localhost:6379' )
9
+ result = client.info( 'CPU' ) rescue nil
10
+
11
+ $redis_missing = result.is_a?( Hash ) == false
12
+
13
+ backends = [ :mock ]
14
+ backends << :real unless $redis_missing
15
+
16
+ # ============================================================================
17
+
18
+ describe Hoodoo::TransientStore::Redis do
19
+
20
+ it 'registers itself' do
21
+ expect( Hoodoo::TransientStore.supported_storage_engines() ).to include( :redis )
22
+ end
23
+
24
+ if $redis_missing
25
+ pending "*** WARNING *** Redis not present on 'redis://localhost:6379', cannot test real engine"
26
+ end
27
+
28
+ shared_examples 'a Redis abstraction' do | backend |
29
+
30
+ # Either expect something on the known mock Redis instance, or an unknown
31
+ # (any) real Redis instance. Can then call "to" - i.e. use:
32
+ #
33
+ # expect_redis( backend ).to...
34
+ #
35
+ # +backend+:: Pass either Symbol ":mock" or ":real".
36
+ #
37
+ def expect_redis( backend )
38
+ return case backend
39
+ when :mock
40
+ expect( @mock_redis_instance )
41
+ else
42
+ expect_any_instance_of( ::Redis )
43
+ end
44
+ end
45
+
46
+ # ========================================================================
47
+
48
+ before :each do
49
+ @storage_engine_uri = 'redis://localhost:6379'
50
+ @namespace = Hoodoo::UUID.generate()
51
+
52
+ if backend == :mock
53
+ Hoodoo::TransientStore::Mocks::Redis.reset()
54
+ @mock_redis_instance = Hoodoo::TransientStore::Mocks::Redis.new
55
+
56
+ expect( ::Redis ).to(
57
+ receive( :new ).
58
+ with( hash_including( { :url => @storage_engine_uri } ) ).
59
+ and_return( @mock_redis_instance )
60
+ )
61
+ else
62
+ expect( ::Redis ).to(
63
+ receive( :new ).
64
+ with( hash_including( { :url => @storage_engine_uri } ) ).
65
+ and_call_original()
66
+ )
67
+ end
68
+ end
69
+
70
+ # ========================================================================
71
+
72
+ context "#initialize (#{ backend })" do
73
+ it 'initialises' do
74
+ instance = Hoodoo::TransientStore::Redis.new(
75
+ storage_host_uri: @storage_engine_uri,
76
+ namespace: @namespace
77
+ )
78
+
79
+ expect( instance ).to be_a( Hoodoo::TransientStore::Redis )
80
+ end
81
+
82
+ it 'complains about strange Redis behaviour' do
83
+ expect_redis( backend ).to receive( :info ).and_return( nil )
84
+
85
+ expect {
86
+ instance = Hoodoo::TransientStore::Redis.new(
87
+ storage_host_uri: @storage_engine_uri,
88
+ namespace: @namespace
89
+ )
90
+ }.to raise_error(
91
+ RuntimeError,
92
+ "Hoodoo::TransientStore::Redis: Did not get back meaningful data from Redis at '#{ @storage_engine_uri }'"
93
+ )
94
+ end
95
+
96
+ it 'handles exceptions' do
97
+ expect_redis( backend ).to receive( :info ).and_raise( 'Hello world' )
98
+
99
+ expect {
100
+ instance = Hoodoo::TransientStore::Redis.new(
101
+ storage_host_uri: @storage_engine_uri,
102
+ namespace: @namespace
103
+ )
104
+ }.to raise_error(
105
+ RuntimeError,
106
+ "Hoodoo::TransientStore::Redis: Cannot connect to Redis at '#{ @storage_engine_uri }': Hello world"
107
+ )
108
+ end
109
+
110
+ it 'generates expected namespaced keys' do
111
+ instance = Hoodoo::TransientStore::Redis.new(
112
+ storage_host_uri: @storage_engine_uri,
113
+ namespace: @namespace
114
+ )
115
+
116
+ expect( instance.send( :namespaced_key, 'foo' ) ).to eql( "#{ @namespace }foo" )
117
+ end
118
+ end
119
+
120
+ # ========================================================================
121
+
122
+ context "when initialised (#{ backend })" do
123
+ before :each do
124
+ @instance = Hoodoo::TransientStore::Redis.new(
125
+ storage_host_uri: @storage_engine_uri,
126
+ namespace: @namespace
127
+ )
128
+
129
+ @key = Hoodoo::UUID.generate()
130
+ @nskey = @instance.send( :namespaced_key, @key )
131
+ @payload = { 'bar' => 'baz' }
132
+ @jpayload = JSON.fast_generate( @payload )
133
+ @ttl = 120
134
+ end
135
+
136
+ context '#set' do
137
+ it 'sets' do
138
+ expect_redis( backend ).to receive( :[]= ).with( @nskey, @jpayload ).and_call_original()
139
+ expect_redis( backend ).to receive( :expire ).with( @nskey, @ttl ).and_call_original()
140
+
141
+ result = @instance.set(
142
+ key: @key,
143
+ payload: @payload,
144
+ maximum_lifespan: @ttl
145
+ )
146
+
147
+ expect( result ).to eq( true )
148
+ end
149
+
150
+ it 'allows exceptions to propagate' do
151
+ expect_redis( backend ).to receive( :[]= ).with( @nskey, @jpayload ).and_raise( 'Hello world' )
152
+
153
+ expect {
154
+ @instance.set(
155
+ key: @key,
156
+ payload: @payload,
157
+ maximum_lifespan: @ttl
158
+ )
159
+ }.to raise_error( RuntimeError, "Hello world" )
160
+ end
161
+ end
162
+
163
+ context '#get' do
164
+ before :each do
165
+ expect_redis( backend ).to receive( :[]= ).with( @nskey, @jpayload ).and_call_original()
166
+ expect_redis( backend ).to receive( :expire ).with( @nskey, @ttl ).and_call_original()
167
+
168
+ @instance.set(
169
+ key: @key,
170
+ payload: @payload,
171
+ maximum_lifespan: @ttl
172
+ )
173
+ end
174
+
175
+ it 'gets known keys' do
176
+ expect_redis( backend ).to receive( :[] ).with( @nskey ).and_call_original()
177
+ expect( @instance.get( key: @key ) ).to eql( @payload )
178
+ end
179
+
180
+ it 'returns "nil" for unknown keys' do
181
+ expect( @instance.get( key: Hoodoo::UUID.generate() ) ).to be_nil
182
+ end
183
+
184
+ it 'allows exceptions to propagate' do
185
+ expect_redis( backend ).to receive( :[] ).with( @nskey ).and_raise( 'Hello world' )
186
+
187
+ expect {
188
+ @instance.get( key: @key )
189
+ }.to raise_error( RuntimeError, "Hello world" )
190
+ end
191
+ end
192
+
193
+ context '#delete' do
194
+ before :each do
195
+ expect_redis( backend ).to receive( :[]= ).with( @nskey, @jpayload ).and_call_original()
196
+ expect_redis( backend ).to receive( :expire ).with( @nskey, @ttl ).and_call_original()
197
+
198
+ @instance.set(
199
+ key: @key,
200
+ payload: @payload,
201
+ maximum_lifespan: @ttl
202
+ )
203
+ end
204
+
205
+ it 'deletes known keys' do
206
+ expect( @instance.get( key: @key ) ).to eql( @payload )
207
+ expect_redis( backend ).to receive( :del ).with( @nskey ).and_call_original()
208
+
209
+ result = @instance.delete( key: @key )
210
+
211
+ expect( result ).to eq( true )
212
+ expect( @instance.get( key: @key ) ).to eql( nil )
213
+ end
214
+
215
+ it 'ignores unknown keys' do
216
+ expect( @instance.delete( key: Hoodoo::UUID.generate() ) ).to eql( true )
217
+ end
218
+
219
+ it 'allows exceptions to propagate' do
220
+ expect_redis( backend ).to receive( :del ).with( @nskey ).and_raise( 'Hello world' )
221
+
222
+ expect {
223
+ @instance.delete( key: @key )
224
+ }.to raise_error( RuntimeError, "Hello world" )
225
+ end
226
+ end
227
+
228
+ context 'close' do
229
+ it 'closes' do
230
+ expect_redis( backend ).to receive( :quit )
231
+ @instance.close()
232
+ end
233
+ end
234
+
235
+ end # 'context "when initialised (#{ backend })" do'
236
+ end # 'shared_examples ...'
237
+
238
+ backends.each do | backend |
239
+ it_behaves_like( 'a Redis abstraction', backend )
240
+ end
241
+
242
+ end # 'describe...'