qs 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/bench/config.qs +4 -27
  2. data/bench/dispatcher.qs +24 -0
  3. data/bench/report.rb +80 -10
  4. data/bench/report.txt +10 -3
  5. data/bench/setup.rb +55 -0
  6. data/lib/qs.rb +75 -15
  7. data/lib/qs/client.rb +73 -22
  8. data/lib/qs/daemon.rb +21 -21
  9. data/lib/qs/daemon_data.rb +4 -4
  10. data/lib/qs/dispatch_job.rb +36 -0
  11. data/lib/qs/dispatch_job_handler.rb +79 -0
  12. data/lib/qs/dispatcher_queue.rb +19 -0
  13. data/lib/qs/error_handler.rb +12 -12
  14. data/lib/qs/event.rb +82 -0
  15. data/lib/qs/event_handler.rb +34 -0
  16. data/lib/qs/event_handler_test_helpers.rb +17 -0
  17. data/lib/qs/job.rb +19 -31
  18. data/lib/qs/job_handler.rb +6 -63
  19. data/lib/qs/{test_helpers.rb → job_handler_test_helpers.rb} +2 -2
  20. data/lib/qs/message.rb +29 -0
  21. data/lib/qs/message_handler.rb +84 -0
  22. data/lib/qs/payload.rb +98 -0
  23. data/lib/qs/payload_handler.rb +106 -54
  24. data/lib/qs/queue.rb +39 -6
  25. data/lib/qs/queue_item.rb +33 -0
  26. data/lib/qs/route.rb +7 -7
  27. data/lib/qs/runner.rb +6 -5
  28. data/lib/qs/test_runner.rb +41 -13
  29. data/lib/qs/version.rb +1 -1
  30. data/qs.gemspec +1 -1
  31. data/test/helper.rb +1 -1
  32. data/test/support/app_daemon.rb +77 -11
  33. data/test/support/factory.rb +34 -0
  34. data/test/system/daemon_tests.rb +146 -77
  35. data/test/system/queue_tests.rb +87 -0
  36. data/test/unit/client_tests.rb +184 -45
  37. data/test/unit/daemon_data_tests.rb +4 -4
  38. data/test/unit/daemon_tests.rb +32 -32
  39. data/test/unit/dispatch_job_handler_tests.rb +163 -0
  40. data/test/unit/dispatch_job_tests.rb +75 -0
  41. data/test/unit/dispatcher_queue_tests.rb +42 -0
  42. data/test/unit/error_handler_tests.rb +9 -9
  43. data/test/unit/event_handler_test_helpers_tests.rb +55 -0
  44. data/test/unit/event_handler_tests.rb +63 -0
  45. data/test/unit/event_tests.rb +162 -0
  46. data/test/unit/{test_helper_tests.rb → job_handler_test_helper_tests.rb} +13 -19
  47. data/test/unit/job_handler_tests.rb +17 -210
  48. data/test/unit/job_tests.rb +49 -79
  49. data/test/unit/message_handler_tests.rb +235 -0
  50. data/test/unit/message_tests.rb +64 -0
  51. data/test/unit/payload_handler_tests.rb +285 -86
  52. data/test/unit/payload_tests.rb +139 -0
  53. data/test/unit/qs_runner_tests.rb +6 -6
  54. data/test/unit/qs_tests.rb +167 -28
  55. data/test/unit/queue_item_tests.rb +51 -0
  56. data/test/unit/queue_tests.rb +126 -18
  57. data/test/unit/route_tests.rb +12 -13
  58. data/test/unit/runner_tests.rb +10 -10
  59. data/test/unit/test_runner_tests.rb +117 -24
  60. metadata +51 -21
  61. data/bench/queue.rb +0 -8
  62. data/lib/qs/redis_item.rb +0 -33
  63. data/test/unit/redis_item_tests.rb +0 -49
@@ -0,0 +1,139 @@
1
+ require 'assert'
2
+ require 'qs/payload'
3
+
4
+ module Qs::Payload
5
+
6
+ class UnitTests < Assert::Context
7
+ desc "Qs::Payload"
8
+ setup do
9
+ # the default JSON encoder/decoder is not deterministic, the keys in the
10
+ # string can be randomly ordered
11
+ Assert.stub(Qs, :encode){ |hash| hash.to_a.sort }
12
+ Assert.stub(Qs, :decode){ |array| Hash[array] }
13
+ end
14
+ subject{ Qs::Payload }
15
+
16
+ should have_imeths :type_method_name
17
+ should have_imeths :deserialize, :serialize
18
+ should have_imeths :job, :job_hash
19
+ should have_imeths :event, :event_hash
20
+
21
+ should "convert payload types to method names using `type_method_name`" do
22
+ assert_equal 'job', subject.type_method_name(Qs::Job::PAYLOAD_TYPE)
23
+ assert_equal 'event', subject.type_method_name(Qs::Event::PAYLOAD_TYPE)
24
+ assert_raises(InvalidError){ subject.type_method_name(Factory.string) }
25
+ end
26
+
27
+ should "serialize and deserialize messages" do
28
+ message = Factory.message
29
+ encoded_payload = subject.serialize(message)
30
+ exp = Qs.encode(subject.send("#{message.payload_type}_hash", message))
31
+ assert_equal exp, encoded_payload
32
+ deserialized_job = subject.deserialize(encoded_payload)
33
+ assert_equal message, deserialized_job
34
+ end
35
+
36
+ should "build jobs and job payload hashes" do
37
+ job = Factory.job
38
+ payload_hash = {
39
+ 'type' => job.payload_type,
40
+ 'name' => job.name,
41
+ 'params' => job.params,
42
+ 'created_at' => Timestamp.new(job.created_at)
43
+ }
44
+ assert_equal job, subject.job(payload_hash)
45
+ assert_equal payload_hash, subject.job_hash(job)
46
+ end
47
+
48
+ should "sanitize its jobs attributes when building a job payload hash" do
49
+ job = Factory.job({
50
+ :name => Factory.string.to_sym,
51
+ :params => { Factory.string.to_sym => Factory.string }
52
+ })
53
+ payload_hash = subject.job_hash(job)
54
+
55
+ assert_equal job.name.to_s, payload_hash['name']
56
+ exp = StringifyParams.new(job.params)
57
+ assert_equal exp, payload_hash['params']
58
+ end
59
+
60
+ should "build events and event payload hashes" do
61
+ event = Factory.event
62
+ payload_hash = {
63
+ 'type' => event.payload_type,
64
+ 'channel' => event.channel,
65
+ 'name' => event.name,
66
+ 'params' => event.params,
67
+ 'publisher' => event.publisher,
68
+ 'published_at' => Timestamp.new(event.published_at)
69
+ }
70
+ assert_equal event, subject.event(payload_hash)
71
+ assert_equal payload_hash, subject.event_hash(event)
72
+ end
73
+
74
+ should "sanitize its events attributes when building an event payload hash" do
75
+ event = Factory.event({
76
+ :channel => Factory.string.to_sym,
77
+ :name => Factory.string.to_sym,
78
+ :params => { Factory.string.to_sym => Factory.string },
79
+ :publisher => Factory.string.to_sym
80
+ })
81
+ payload_hash = subject.event_hash(event)
82
+
83
+ assert_equal event.channel.to_s, payload_hash['channel']
84
+ assert_equal event.name.to_s, payload_hash['name']
85
+ exp = StringifyParams.new(event.params)
86
+ assert_equal exp, payload_hash['params']
87
+ assert_equal event.publisher.to_s, payload_hash['publisher']
88
+ end
89
+
90
+ should "raise errors for unknown parent types" do
91
+ message = Factory.message
92
+ Assert.stub(message, :payload_type){ Factory.string }
93
+ payload_hash = { 'type' => message.payload_type }
94
+
95
+ assert_raises(InvalidError){ subject.deserialize(payload_hash) }
96
+ assert_raises(InvalidError){ subject.serialize(message) }
97
+ end
98
+
99
+ end
100
+
101
+ class StringifyParamsTests < UnitTests
102
+ desc "StringifyParams"
103
+ subject{ StringifyParams }
104
+
105
+ should have_imeths :new
106
+
107
+ should "convert all hash keys to strings" do
108
+ key, value = Factory.string.to_sym, Factory.string
109
+ result = subject.new({
110
+ key => value,
111
+ :hash => { key => [value] },
112
+ :array => [{ key => value }]
113
+ })
114
+ exp = {
115
+ key.to_s => value,
116
+ 'hash' => { key.to_s => [value] },
117
+ 'array' => [{ key.to_s => value }]
118
+ }
119
+ assert_equal exp, result
120
+ end
121
+
122
+ end
123
+
124
+ class TimestampTests < UnitTests
125
+ desc "Timestamp"
126
+ subject{ Timestamp }
127
+
128
+ should have_imeths :to_time, :new
129
+
130
+ should "handle building timestamps and converting them to times" do
131
+ time = Factory.time
132
+ timestamp = subject.new(time)
133
+ assert_equal time.to_i, timestamp
134
+ assert_equal time, subject.to_time(timestamp)
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -2,7 +2,7 @@ require 'assert'
2
2
  require 'qs/qs_runner'
3
3
 
4
4
  require 'qs'
5
- require 'qs/job_handler'
5
+ require 'qs/message_handler'
6
6
 
7
7
  class Qs::QsRunner
8
8
 
@@ -27,7 +27,7 @@ class Qs::QsRunner
27
27
  class InitTests < UnitTests
28
28
  desc "when init"
29
29
  setup do
30
- @handler_class = TestJobHandler
30
+ @handler_class = TestMessageHandler
31
31
  @runner = @runner_class.new(@handler_class)
32
32
  end
33
33
  subject{ @runner }
@@ -36,8 +36,8 @@ class Qs::QsRunner
36
36
  should have_imeths :run
37
37
 
38
38
  should "know its timeout" do
39
- assert_equal TestJobHandler.timeout, subject.timeout
40
- handler_class = Class.new{ include Qs::JobHandler }
39
+ assert_equal TestMessageHandler.timeout, subject.timeout
40
+ handler_class = Class.new{ include Qs::MessageHandler }
41
41
  runner = @runner_class.new(handler_class)
42
42
  assert_equal Qs.config.timeout, runner.timeout
43
43
  end
@@ -131,8 +131,8 @@ class Qs::QsRunner
131
131
 
132
132
  end
133
133
 
134
- class TestJobHandler
135
- include Qs::JobHandler
134
+ class TestMessageHandler
135
+ include Qs::MessageHandler
136
136
 
137
137
  attr_reader :first_before_call_order, :second_before_call_order
138
138
  attr_reader :first_after_call_order, :second_after_call_order
@@ -3,7 +3,6 @@ require 'qs'
3
3
 
4
4
  require 'hella-redis/connection_spy'
5
5
  require 'ns-options/assert_macros'
6
- require 'qs/job'
7
6
  require 'qs/queue'
8
7
 
9
8
  module Qs
@@ -21,9 +20,14 @@ module Qs
21
20
  subject{ @module }
22
21
 
23
22
  should have_imeths :config, :configure, :init, :reset!
24
- should have_imeths :enqueue, :push
25
- should have_imeths :serialize, :deserialize
23
+ should have_imeths :enqueue, :publish, :publish_as, :push
24
+ should have_imeths :encode, :decode
25
+ should have_imeths :sync_subscriptions, :clear_subscriptions
26
+ should have_imeths :event_subscribers
26
27
  should have_imeths :client, :redis, :redis_config
28
+ should have_imeths :dispatcher_queue, :dispatcher_job_name
29
+ should have_imeths :event_publisher
30
+ should have_imeths :published_events
27
31
 
28
32
  should "know its config" do
29
33
  assert_instance_of Config, subject.config
@@ -32,7 +36,7 @@ module Qs
32
36
  should "allow configuring its config" do
33
37
  yielded = nil
34
38
  subject.configure{ |c| yielded = c }
35
- assert_equal subject.config, yielded
39
+ assert_same subject.config, yielded
36
40
  end
37
41
 
38
42
  should "not have a client or redis connection by default" do
@@ -50,11 +54,16 @@ module Qs
50
54
  class InitTests < UnitTests
51
55
  desc "when init"
52
56
  setup do
53
- @module.config.serializer = proc{ |v| v.to_s }
54
- @module.config.deserializer = proc{ |v| v.to_i }
55
- @module.config.redis.ip = Factory.string
56
- @module.config.redis.port = Factory.integer
57
- @module.config.redis.db = Factory.integer
57
+ @module.config.encoder = proc{ |v| v.to_s }
58
+ @module.config.decoder = proc{ |v| v.to_i }
59
+ @module.config.redis.ip = Factory.string
60
+ @module.config.redis.port = Factory.integer
61
+ @module.config.redis.db = Factory.integer
62
+
63
+ @dispatcher_queue_spy = nil
64
+ Assert.stub(DispatcherQueue, :new) do |*args|
65
+ @dispatcher_queue_spy = DispatcherQueueSpy.new(*args)
66
+ end
58
67
 
59
68
  @client_spy = nil
60
69
  Assert.stub(Client, :new) do |*args|
@@ -73,6 +82,17 @@ module Qs
73
82
  assert_equal expected, subject.config.redis.url
74
83
  end
75
84
 
85
+ should "build a dispatcher queue" do
86
+ assert_equal @dispatcher_queue_spy, subject.dispatcher_queue
87
+ exp = subject.config.dispatcher_queue_class
88
+ assert_equal exp, @dispatcher_queue_spy.queue_class
89
+ dispatcher_config = subject.config.dispatcher
90
+ assert_equal dispatcher_config.queue_name, @dispatcher_queue_spy.queue_name
91
+ assert_equal dispatcher_config.job_name, @dispatcher_queue_spy.job_name
92
+ exp = dispatcher_config.job_handler_class_name
93
+ assert_equal exp, @dispatcher_queue_spy.job_handler_class_name
94
+ end
95
+
76
96
  should "build a client" do
77
97
  assert_equal @client_spy, subject.client
78
98
  assert_equal @client_spy.redis, subject.redis
@@ -91,6 +111,32 @@ module Qs
91
111
  assert_equal job_params, call.job_params
92
112
  end
93
113
 
114
+ should "call publish on its client using `publish`" do
115
+ event_channel = Factory.string
116
+ event_name = Factory.string
117
+ event_params = { Factory.string => Factory.string }
118
+ subject.publish(event_channel, event_name, event_params)
119
+
120
+ call = @client_spy.publish_calls.last
121
+ assert_equal event_channel, call.event_channel
122
+ assert_equal event_name, call.event_name
123
+ assert_equal event_params, call.event_params
124
+ end
125
+
126
+ should "call publish as on its client using `publish_as`" do
127
+ event_publisher = Factory.string
128
+ event_channel = Factory.string
129
+ event_name = Factory.string
130
+ event_params = { Factory.string => Factory.string }
131
+ subject.publish_as(event_publisher, event_channel, event_name, event_params)
132
+
133
+ call = @client_spy.publish_calls.last
134
+ assert_equal event_publisher, call.event_publisher
135
+ assert_equal event_channel, call.event_channel
136
+ assert_equal event_name, call.event_name
137
+ assert_equal event_params, call.event_params
138
+ end
139
+
94
140
  should "call push on its client using `push`" do
95
141
  queue_name = Factory.string
96
142
  payload = { Factory.string => Factory.string }
@@ -101,22 +147,58 @@ module Qs
101
147
  assert_equal payload, call.payload
102
148
  end
103
149
 
104
- should "use the configured serializer using `serialize`" do
150
+ should "use the configured encoder using `encode`" do
105
151
  value = Factory.integer
106
- result = subject.serialize(value)
152
+ result = subject.encode(value)
107
153
  assert_equal value.to_s, result
108
154
  end
109
155
 
110
- should "use the configured deserializer using `deserialize`" do
156
+ should "use the configured decoder using `decode`" do
111
157
  value = Factory.integer.to_s
112
- result = subject.deserialize(value)
158
+ result = subject.decode(value)
113
159
  assert_equal value.to_i, result
114
160
  end
115
161
 
116
- should "not reset its client or redis connection when init again" do
162
+ should "demeter its clients subscription methods" do
163
+ queue = Qs::Queue.new{ name Factory.string }
164
+
165
+ subject.sync_subscriptions(queue)
166
+ call = @client_spy.sync_subscriptions_calls.last
167
+ assert_equal queue, call.queue
168
+
169
+ subject.clear_subscriptions(queue)
170
+ call = @client_spy.clear_subscriptions_calls.last
171
+ assert_equal queue, call.queue
172
+ end
173
+
174
+ should "demeter its event publishers method to its client" do
175
+ event = Factory.event
176
+ subject.event_subscribers(event)
177
+
178
+ call = @client_spy.event_subscribers_calls.last
179
+ assert_equal event, call.event
180
+ end
181
+
182
+ should "know its dispatcher job name and event publisher" do
183
+ exp = subject.config.dispatcher.job_name
184
+ assert_equal exp, subject.dispatcher_job_name
185
+ exp = subject.config.event_publisher
186
+ assert_equal exp, subject.event_publisher
187
+ end
188
+
189
+ should "return the dispatcher queue published events using `published_events`" do
190
+ queue = subject.dispatcher_queue
191
+ published_events = Factory.integer(3).times.map{ Factory.string }
192
+ Assert.stub(queue, :published_events){ published_events }
193
+ assert_equal queue.published_events, subject.published_events
194
+ end
195
+
196
+ should "not reset its attributes when init again" do
197
+ queue = subject.dispatcher_queue
117
198
  client = subject.client
118
199
  redis = subject.redis
119
200
  subject.init
201
+ assert_same queue, subject.dispatcher_queue
120
202
  assert_same client, subject.client
121
203
  assert_same redis, subject.redis
122
204
  end
@@ -124,10 +206,11 @@ module Qs
124
206
  should "reset itself using `reset!`" do
125
207
  subject.reset!
126
208
  assert_nil subject.config.redis.url
209
+ assert_nil subject.dispatcher_queue
127
210
  assert_nil subject.client
128
211
  assert_nil subject.redis
129
- assert_raises(NoMethodError){ subject.serialize(Factory.integer) }
130
- assert_raises(NoMethodError){ subject.deserialize(Factory.integer) }
212
+ assert_raises(NoMethodError){ subject.encode(Factory.integer) }
213
+ assert_raises(NoMethodError){ subject.decode(Factory.integer) }
131
214
  end
132
215
 
133
216
  end
@@ -141,25 +224,39 @@ module Qs
141
224
  end
142
225
  subject{ @config }
143
226
 
144
- should have_options :serializer, :deserializer, :timeout
145
- should have_namespace :redis
227
+ should have_options :encoder, :decoder, :timeout
228
+ should have_options :event_publisher
229
+ should have_namespace :dispatcher, :redis
230
+ should have_accessors :dispatcher_queue_class
146
231
 
147
- should "know its default serializer/deserializer" do
232
+ should "know its default decoder/encoder" do
148
233
  payload = { Factory.string => Factory.string }
149
234
 
150
235
  exp = JSON.dump(payload)
151
- serialized_payload = subject.serializer.call(payload)
152
- assert_equal exp, serialized_payload
236
+ encoded_payload = subject.encoder.call(payload)
237
+ assert_equal exp, encoded_payload
153
238
  exp = JSON.load(exp)
154
- assert_equal exp, subject.deserializer.call(serialized_payload)
239
+ assert_equal exp, subject.decoder.call(encoded_payload)
155
240
  end
156
241
 
157
242
  should "know its default timeout" do
158
243
  assert_nil subject.timeout
159
244
  end
160
245
 
246
+ should "not have a default event publisher" do
247
+ assert_nil subject.event_publisher
248
+ end
249
+
250
+ should "know its default dispatcher options" do
251
+ assert_equal Queue, subject.dispatcher_queue_class
252
+ assert_equal 'dispatcher', subject.dispatcher.queue_name
253
+ assert_equal 'run_dispatch_job', subject.dispatcher.job_name
254
+ exp = DispatcherQueue::RunDispatchJob.to_s
255
+ assert_equal exp, subject.dispatcher.job_handler_class_name
256
+ end
257
+
161
258
  should "know its default redis options" do
162
- assert_equal 'localhost', subject.redis.ip
259
+ assert_equal '127.0.0.1', subject.redis.ip
163
260
  assert_equal 6379, subject.redis.port
164
261
  assert_equal 0, subject.redis.db
165
262
  assert_equal 'qs', subject.redis.redis_ns
@@ -191,27 +288,69 @@ module Qs
191
288
 
192
289
  end
193
290
 
291
+ class DispatcherQueueSpy
292
+ attr_reader :queue_class, :queue_name
293
+ attr_reader :job_name, :job_handler_class_name
294
+ attr_reader :published_events
295
+
296
+ def initialize(options)
297
+ @queue_class = options[:queue_class]
298
+ @queue_name = options[:queue_name]
299
+ @job_name = options[:job_name]
300
+ @job_handler_class_name = options[:job_handler_class_name]
301
+ end
302
+ end
303
+
194
304
  class ClientSpy
195
305
  attr_reader :redis_config, :redis
196
- attr_reader :enqueue_calls, :push_calls
306
+ attr_reader :enqueue_calls, :publish_calls, :push_calls
307
+ attr_reader :sync_subscriptions_calls, :clear_subscriptions_calls
308
+ attr_reader :event_subscribers_calls
197
309
 
198
310
  def initialize(redis_confg)
199
311
  @redis_config = redis_confg
200
312
  @redis = Factory.string
201
313
  @enqueue_calls = []
314
+ @publish_calls = []
202
315
  @push_calls = []
316
+ @sync_subscriptions_calls = []
317
+ @clear_subscriptions_calls = []
318
+ @event_subscribers_calls = []
319
+ end
320
+
321
+ def enqueue(queue, name, params = nil)
322
+ @enqueue_calls << EnqueueCall.new(queue, name, params)
323
+ end
324
+
325
+ def publish(channel, name, params = nil)
326
+ @publish_calls << PublishCall.new(channel, name, params)
203
327
  end
204
328
 
205
- def enqueue(queue, job_name, job_params = nil)
206
- @enqueue_calls << EnqueueCall.new(queue, job_name, job_params)
329
+ def publish_as(publisher, channel, name, params = nil)
330
+ @publish_calls << PublishCall.new(channel, name, params, publisher)
207
331
  end
208
332
 
209
333
  def push(queue_name, payload)
210
334
  @push_calls << PushCall.new(queue_name, payload)
211
335
  end
212
336
 
213
- EnqueueCall = Struct.new(:queue, :job_name, :job_params)
214
- PushCall = Struct.new(:queue_name, :payload)
337
+ def sync_subscriptions(queue)
338
+ @sync_subscriptions_calls << SubscriptionsCall.new(queue)
339
+ end
340
+
341
+ def clear_subscriptions(queue)
342
+ @clear_subscriptions_calls << SubscriptionsCall.new(queue)
343
+ end
344
+
345
+ def event_subscribers(event)
346
+ @event_subscribers_calls << SubscribersCall.new(event)
347
+ end
348
+
349
+ EnqueueCall = Struct.new(:queue, :job_name, :job_params)
350
+ PublishCall = Struct.new(:event_channel, :event_name, :event_params, :event_publisher)
351
+ PushCall = Struct.new(:queue_name, :payload)
352
+ SubscriptionsCall = Struct.new(:queue, :event_job_names)
353
+ SubscribersCall = Struct.new(:event)
215
354
  end
216
355
 
217
356
  end