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