hoodoo 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
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...'