qs 0.3.0 → 0.4.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 (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
@@ -9,7 +9,7 @@ require 'qs/daemon_data'
9
9
  require 'qs/io_pipe'
10
10
  require 'qs/logger'
11
11
  require 'qs/payload_handler'
12
- require 'qs/redis_item'
12
+ require 'qs/queue_item'
13
13
 
14
14
  module Qs
15
15
 
@@ -33,7 +33,7 @@ module Qs
33
33
 
34
34
  # * Set the size of the client to the max workers + 1. This ensures we
35
35
  # have 1 connection for fetching work from redis and at least 1
36
- # connection for each worker to requeue its job when hard-shutdown.
36
+ # connection for each worker to requeue its message when hard-shutdown.
37
37
  def initialize
38
38
  self.class.configuration.validate!
39
39
  Qs.init
@@ -96,8 +96,8 @@ module Qs
96
96
 
97
97
  private
98
98
 
99
- def process(redis_item)
100
- Qs::PayloadHandler.new(self.daemon_data, redis_item).run
99
+ def process(queue_item)
100
+ Qs::PayloadHandler.new(self.daemon_data, queue_item).run
101
101
  end
102
102
 
103
103
  def work_loop
@@ -128,9 +128,9 @@ module Qs
128
128
  wp = DatWorkerPool.new(
129
129
  self.daemon_data.min_workers,
130
130
  self.daemon_data.max_workers
131
- ){ |redis_item| process(redis_item) }
132
- wp.on_worker_error do |worker, exception, redis_item|
133
- handle_worker_exception(exception, redis_item)
131
+ ){ |queue_item| process(queue_item) }
132
+ wp.on_worker_error do |worker, exception, queue_item|
133
+ handle_worker_exception(exception, queue_item)
134
134
  end
135
135
  wp.on_worker_sleep{ @worker_available_io.write(SIGNAL) }
136
136
  wp.start
@@ -138,8 +138,8 @@ module Qs
138
138
  end
139
139
 
140
140
  # * Shuffle the queue redis keys to avoid queue starvation. Redis will
141
- # pull jobs off queues in the order they are passed to the command, by
142
- # shuffling we ensure they are randomly ordered so every queue should
141
+ # pull messages off queues in the order they are passed to the command,
142
+ # by shuffling we ensure they are randomly ordered so every queue should
143
143
  # get a chance.
144
144
  # * Use 0 for the brpop timeout which means block indefinitely.
145
145
  # * Rescue runtime errors so the daemon thread doesn't fail if redis is
@@ -151,9 +151,9 @@ module Qs
151
151
 
152
152
  begin
153
153
  args = [self.signals_redis_key, self.queue_redis_keys.shuffle, 0].flatten
154
- redis_key, serialized_payload = @client.block_dequeue(*args)
154
+ redis_key, encoded_payload = @client.block_dequeue(*args)
155
155
  if redis_key != @signals_redis_key
156
- @worker_pool.add_work(RedisItem.new(redis_key, serialized_payload))
156
+ @worker_pool.add_work(QueueItem.new(redis_key, encoded_payload))
157
157
  end
158
158
  rescue RuntimeError => exception
159
159
  log "Error dequeueing #{exception.message.inspect}", :error
@@ -178,9 +178,9 @@ module Qs
178
178
  log "Shutting down, waiting for work to finish"
179
179
  end
180
180
  @worker_pool.shutdown(timeout)
181
- log "Requeueing #{@worker_pool.work_items.size} job(s)"
181
+ log "Requeueing #{@worker_pool.work_items.size} message(s)"
182
182
  @worker_pool.work_items.each do |ri|
183
- @client.prepend(ri.queue_redis_key, ri.serialized_payload)
183
+ @client.prepend(ri.queue_redis_key, ri.encoded_payload)
184
184
  end
185
185
  end
186
186
 
@@ -196,18 +196,18 @@ module Qs
196
196
  # * This only catches errors that happen outside of running the payload
197
197
  # handler. The only known use-case for this is dat worker pools
198
198
  # hard-shutdown errors.
199
- # * If there isn't a redis item (this can happen when an idle worker is
199
+ # * If there isn't a queue item (this can happen when an idle worker is
200
200
  # being forced to exit) then we don't need to do anything.
201
- # * If we never started processing the redis item, its safe to requeue it.
201
+ # * If we never started processing the queue item, its safe to requeue it.
202
202
  # Otherwise it happened while processing so the payload handler caught
203
203
  # it or it happened after the payload handler which we don't care about.
204
- def handle_worker_exception(exception, redis_item)
205
- return if redis_item.nil?
206
- if !redis_item.started
207
- log "Worker error, requeueing job because it hasn't started", :error
208
- @client.prepend(redis_item.queue_redis_key, redis_item.serialized_payload)
204
+ def handle_worker_exception(exception, queue_item)
205
+ return if queue_item.nil?
206
+ if !queue_item.started
207
+ log "Worker error, requeueing message because it hasn't started", :error
208
+ @client.prepend(queue_item.queue_redis_key, queue_item.encoded_payload)
209
209
  else
210
- log "Worker error after job was processed, ignoring", :error
210
+ log "Worker error after message was processed, ignoring", :error
211
211
  end
212
212
  log "#{exception.class}: #{exception.message}", :error
213
213
  log exception.backtrace.join("\n"), :error
@@ -5,7 +5,7 @@ module Qs
5
5
  # The daemon uses this to "compile" its configuration for speed. NsOptions
6
6
  # is relatively slow everytime an option is read. To avoid this, we read the
7
7
  # options one time here and memoize their values. This way, we don't pay the
8
- # NsOptions overhead when reading them while handling a job.
8
+ # NsOptions overhead when reading them while handling a message.
9
9
 
10
10
  attr_reader :name
11
11
  attr_reader :pid_file
@@ -29,14 +29,14 @@ module Qs
29
29
  @routes = build_routes(args[:routes] || [])
30
30
  end
31
31
 
32
- def route_for(name)
33
- @routes[name] || raise(NotFoundError, "no service named '#{name}'")
32
+ def route_for(route_id)
33
+ @routes[route_id] || raise(NotFoundError, "unknown message '#{route_id}'")
34
34
  end
35
35
 
36
36
  private
37
37
 
38
38
  def build_routes(routes)
39
- routes.inject({}){ |h, route| h.merge(route.name => route) }
39
+ routes.inject({}){ |h, route| h.merge(route.id => route) }
40
40
  end
41
41
 
42
42
  end
@@ -0,0 +1,36 @@
1
+ require 'qs'
2
+ require 'qs/event'
3
+ require 'qs/job'
4
+
5
+ module Qs
6
+
7
+ class DispatchJob < Qs::Job
8
+
9
+ def self.event(job)
10
+ Qs::Event.new(job.params['event_channel'], job.params['event_name'], {
11
+ :params => job.params['event_params'],
12
+ :publisher => job.params['event_publisher'],
13
+ :published_at => job.created_at
14
+ })
15
+ end
16
+
17
+ def initialize(event_channel, event_name, options = nil)
18
+ options ||= {}
19
+ event_params = options.delete(:event_params) || {}
20
+ event_publisher = options.delete(:event_publisher) || Qs.event_publisher
21
+ options[:params] = {
22
+ 'event_channel' => event_channel,
23
+ 'event_name' => event_name,
24
+ 'event_params' => event_params,
25
+ 'event_publisher' => event_publisher
26
+ }
27
+ super(Qs.dispatcher_job_name, options)
28
+ end
29
+
30
+ def event
31
+ @event ||= self.class.event(self)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,79 @@
1
+ require 'qs'
2
+ require 'qs/dispatch_job'
3
+ require 'qs/job_handler'
4
+
5
+ module Qs
6
+
7
+ module DispatchJobHandler
8
+
9
+ def self.included(klass)
10
+ klass.class_eval do
11
+ include Qs::JobHandler
12
+ include InstanceMethods
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+
18
+ attr_reader :event, :subscribed_queue_names
19
+
20
+ def init!
21
+ @event = Qs::DispatchJob.event(job)
22
+ @subscribed_queue_names = Qs.event_subscribers(@event)
23
+ @qs_failed_dispatches = []
24
+ end
25
+
26
+ def run!
27
+ logger.info "Dispatching #{self.event.route_name}"
28
+ logger.info " params: #{self.event.params.inspect}"
29
+ logger.info " publisher: #{self.event.publisher}"
30
+ logger.info " published at: #{self.event.published_at}"
31
+ logger.info "Found #{self.subscribed_queue_names.size} subscribed queue(s):"
32
+ self.subscribed_queue_names.each do |queue_name|
33
+ qs_dispatch(queue_name, self.event)
34
+ end
35
+ qs_handle_errors(self.event, @qs_failed_dispatches)
36
+ end
37
+
38
+ private
39
+
40
+ def qs_dispatch(queue_name, event)
41
+ Qs.push(queue_name, Qs::Payload.event_hash(event))
42
+ logger.info " => #{queue_name}"
43
+ rescue StandardError => exception
44
+ logger.info " => #{queue_name} (failed)"
45
+ @qs_failed_dispatches << FailedDispatch.new(queue_name, exception)
46
+ end
47
+
48
+ def qs_handle_errors(event, failed_dispatches)
49
+ return if failed_dispatches.empty?
50
+ logger.info "Failed to dispatch the event to " \
51
+ "#{failed_dispatches.size} subscribed queues"
52
+ descriptions = failed_dispatches.map do |fail|
53
+ exception_desc = "#{fail.exception.class}: #{fail.exception.message}"
54
+ logger.info "#{fail.queue_name}"
55
+ logger.info " #{exception_desc}"
56
+ logger.info " #{fail.exception.backtrace.first}"
57
+ "#{fail.queue_name} - #{exception_desc}"
58
+ end
59
+ message = "#{event.route_name} event wasn't dispatched to:\n" \
60
+ " #{descriptions.join("\n ")}"
61
+ raise DispatchError.new(message, failed_dispatches)
62
+ end
63
+
64
+ end
65
+
66
+ FailedDispatch = Struct.new(:queue_name, :exception)
67
+
68
+ class DispatchError < RuntimeError
69
+ attr_reader :failed_dispatches
70
+
71
+ def initialize(message, failed_dispatches)
72
+ super message
73
+ @failed_dispatches = failed_dispatches
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,19 @@
1
+ require 'qs/queue'
2
+ require 'qs/dispatch_job_handler'
3
+
4
+ module Qs
5
+
6
+ module DispatcherQueue
7
+
8
+ def self.new(options)
9
+ options[:queue_class].new do
10
+ name options[:queue_name]
11
+ job options[:job_name], options[:job_handler_class_name]
12
+ end
13
+ end
14
+
15
+ RunDispatchJob = Class.new{ include Qs::DispatchJobHandler }
16
+
17
+ end
18
+
19
+ end
@@ -31,24 +31,24 @@ module Qs
31
31
 
32
32
  class ErrorContext
33
33
  attr_reader :daemon_data
34
- attr_reader :queue_name, :serialized_payload
35
- attr_reader :job, :handler_class
34
+ attr_reader :queue_name, :encoded_payload
35
+ attr_reader :message, :handler_class
36
36
 
37
37
  def initialize(args)
38
- @daemon_data = args[:daemon_data]
39
- @queue_name = Queue::RedisKey.parse_name(args[:queue_redis_key].to_s)
40
- @serialized_payload = args[:serialized_payload]
41
- @job = args[:job]
42
- @handler_class = args[:handler_class]
38
+ @daemon_data = args[:daemon_data]
39
+ @queue_name = Queue::RedisKey.parse_name(args[:queue_redis_key].to_s)
40
+ @encoded_payload = args[:encoded_payload]
41
+ @message = args[:message]
42
+ @handler_class = args[:handler_class]
43
43
  end
44
44
 
45
45
  def ==(other)
46
46
  if other.kind_of?(self.class)
47
- self.daemon_data == other.daemon_data &&
48
- self.queue_name == other.queue_name &&
49
- self.serialized_payload == other.serialized_payload &&
50
- self.job == other.job &&
51
- self.handler_class == other.handler_class
47
+ self.daemon_data == other.daemon_data &&
48
+ self.queue_name == other.queue_name &&
49
+ self.encoded_payload == other.encoded_payload &&
50
+ self.message == other.message &&
51
+ self.handler_class == other.handler_class
52
52
  else
53
53
  super
54
54
  end
@@ -0,0 +1,82 @@
1
+ require 'qs/message'
2
+
3
+ module Qs
4
+
5
+ class Event < Message
6
+
7
+ PAYLOAD_TYPE = 'event'
8
+
9
+ attr_reader :channel, :name, :publisher, :published_at
10
+
11
+ def initialize(channel, name, options = nil)
12
+ options ||= {}
13
+ options[:params] ||= {}
14
+ validate!(channel, name, options[:params])
15
+ @channel = channel
16
+ @name = name
17
+ @publisher = options[:publisher]
18
+ @published_at = options[:published_at] || Time.now
19
+ super(PAYLOAD_TYPE, options)
20
+ end
21
+
22
+ def route_name
23
+ @route_name ||= Event::RouteName.new(self.channel, self.name)
24
+ end
25
+
26
+ def subscribers_redis_key
27
+ @subscribers_redis_key ||= SubscribersRedisKey.new(self.route_name)
28
+ end
29
+
30
+ def inspect
31
+ reference = '0x0%x' % (self.object_id << 1)
32
+ "#<#{self.class}:#{reference} " \
33
+ "@channel=#{self.channel.inspect} " \
34
+ "@name=#{self.name.inspect} " \
35
+ "@params=#{self.params.inspect} " \
36
+ "@publisher=#{self.publisher.inspect} " \
37
+ "@published_at=#{self.published_at.inspect}>"
38
+ end
39
+
40
+ def ==(other)
41
+ if other.kind_of?(self.class)
42
+ self.payload_type == other.payload_type &&
43
+ self.channel == other.channel &&
44
+ self.name == other.name &&
45
+ self.params == other.params &&
46
+ self.publisher == other.publisher &&
47
+ self.published_at == other.published_at
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def validate!(channel, name, params)
56
+ problem = if channel.to_s.empty?
57
+ "The event doesn't have a channel."
58
+ elsif name.to_s.empty?
59
+ "The event doesn't have a name."
60
+ elsif !params.kind_of?(::Hash)
61
+ "The event's params are not valid."
62
+ end
63
+ raise(InvalidError, problem) if problem
64
+ end
65
+
66
+ module RouteName
67
+ def self.new(event_channel, event_name)
68
+ "#{event_channel}:#{event_name}"
69
+ end
70
+ end
71
+
72
+ module SubscribersRedisKey
73
+ def self.new(route_name)
74
+ "events:#{route_name}:subscribers"
75
+ end
76
+ end
77
+
78
+ InvalidError = Class.new(ArgumentError)
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,34 @@
1
+ require 'qs/message_handler'
2
+
3
+ module Qs
4
+
5
+ module EventHandler
6
+
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include Qs::MessageHandler
10
+ include InstanceMethods
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+
16
+ def inspect
17
+ reference = '0x0%x' % (self.object_id << 1)
18
+ "#<#{self.class}:#{reference} @event=#{event.inspect}>"
19
+ end
20
+
21
+ private
22
+
23
+ # Helpers
24
+
25
+ def event; @qs_runner.message; end
26
+ def event_channel; event.channel; end
27
+ def event_name; event.name; end
28
+ def event_published_at; event.published_at; end
29
+
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,17 @@
1
+ require 'qs/test_runner'
2
+
3
+ module Qs::EventHandler
4
+
5
+ module TestHelpers
6
+
7
+ def test_runner(handler_class, args = nil)
8
+ Qs::EventTestRunner.new(handler_class, args)
9
+ end
10
+
11
+ def test_handler(handler_class, args = nil)
12
+ test_runner(handler_class, args).handler
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -1,26 +1,24 @@
1
+ require 'qs/message'
2
+
1
3
  module Qs
2
4
 
3
- class Job
5
+ class Job < Message
4
6
 
5
- def self.parse(payload)
6
- created_at = Time.at(payload['created_at'].to_i)
7
- self.new(payload['name'], payload['params'], created_at)
8
- end
7
+ PAYLOAD_TYPE = 'job'
9
8
 
10
- attr_reader :name, :params, :created_at
9
+ attr_reader :name, :created_at
11
10
 
12
- def initialize(name, params, created_at = nil)
13
- validate!(name, params)
11
+ def initialize(name, options = nil)
12
+ options ||= {}
13
+ options[:params] ||= {}
14
+ validate!(name, options[:params])
14
15
  @name = name
15
- @params = params
16
- @created_at = created_at || Time.now
16
+ @created_at = options[:created_at] || Time.now
17
+ super(PAYLOAD_TYPE, options)
17
18
  end
18
19
 
19
- def to_payload
20
- { 'name' => self.name.to_s,
21
- 'params' => StringifyParams.new(self.params),
22
- 'created_at' => self.created_at.to_i
23
- }
20
+ def route_name
21
+ self.name
24
22
  end
25
23
 
26
24
  def inspect
@@ -33,7 +31,10 @@ module Qs
33
31
 
34
32
  def ==(other)
35
33
  if other.kind_of?(self.class)
36
- self.to_payload == other.to_payload
34
+ self.payload_type == other.payload_type &&
35
+ self.name == other.name &&
36
+ self.params == other.params &&
37
+ self.created_at == other.created_at
37
38
  else
38
39
  super
39
40
  end
@@ -47,24 +48,11 @@ module Qs
47
48
  elsif !params.kind_of?(::Hash)
48
49
  "The job's params are not valid."
49
50
  end
50
- raise(BadJobError, problem) if problem
51
+ raise(InvalidError, problem) if problem
51
52
  end
52
53
 
53
- module StringifyParams
54
- def self.new(object)
55
- case(object)
56
- when Hash
57
- object.inject({}){ |h, (k, v)| h.merge(k.to_s => self.new(v)) }
58
- when Array
59
- object.map{ |item| self.new(item) }
60
- else
61
- object
62
- end
63
- end
64
- end
54
+ InvalidError = Class.new(ArgumentError)
65
55
 
66
56
  end
67
57
 
68
- BadJobError = Class.new(ArgumentError)
69
-
70
58
  end