pika_que 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +41 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/examples/demo.rb +42 -0
  13. data/examples/demo_delay.rb +41 -0
  14. data/examples/demo_oneoff.rb +29 -0
  15. data/examples/demo_priority.rb +52 -0
  16. data/examples/demo_reporter.rb +19 -0
  17. data/examples/demo_retry.rb +41 -0
  18. data/examples/demo_worker.rb +17 -0
  19. data/examples/dev_worker.rb +19 -0
  20. data/exe/pika_que +8 -0
  21. data/lib/active_job/queue_adapters/pika_que_adapter.rb +42 -0
  22. data/lib/pika_que.rb +44 -0
  23. data/lib/pika_que/broker.rb +88 -0
  24. data/lib/pika_que/cli.rb +180 -0
  25. data/lib/pika_que/codecs/json.rb +22 -0
  26. data/lib/pika_que/codecs/noop.rb +20 -0
  27. data/lib/pika_que/codecs/rails.rb +22 -0
  28. data/lib/pika_que/configuration.rb +110 -0
  29. data/lib/pika_que/connection.rb +47 -0
  30. data/lib/pika_que/delay_worker.rb +55 -0
  31. data/lib/pika_que/errors.rb +5 -0
  32. data/lib/pika_que/handlers/default_handler.rb +31 -0
  33. data/lib/pika_que/handlers/delay_handler.rb +124 -0
  34. data/lib/pika_que/handlers/error_handler.rb +69 -0
  35. data/lib/pika_que/handlers/retry_handler.rb +186 -0
  36. data/lib/pika_que/launcher.rb +92 -0
  37. data/lib/pika_que/logging.rb +33 -0
  38. data/lib/pika_que/metrics.rb +26 -0
  39. data/lib/pika_que/metrics/log_metric.rb +23 -0
  40. data/lib/pika_que/metrics/null_metric.rb +14 -0
  41. data/lib/pika_que/middleware/active_record.rb +13 -0
  42. data/lib/pika_que/middleware/chain.rb +90 -0
  43. data/lib/pika_que/processor.rb +45 -0
  44. data/lib/pika_que/publisher.rb +23 -0
  45. data/lib/pika_que/rails.rb +62 -0
  46. data/lib/pika_que/reporters.rb +18 -0
  47. data/lib/pika_que/reporters/log_reporter.rb +13 -0
  48. data/lib/pika_que/runner.rb +24 -0
  49. data/lib/pika_que/subscriber.rb +80 -0
  50. data/lib/pika_que/util.rb +17 -0
  51. data/lib/pika_que/version.rb +3 -0
  52. data/lib/pika_que/worker.rb +99 -0
  53. data/pika_que.gemspec +37 -0
  54. metadata +181 -0
@@ -0,0 +1,47 @@
1
+ module PikaQue
2
+ class Connection
3
+ extend Forwardable
4
+
5
+ def_delegators :@connection, :create_channel
6
+
7
+ include Logging
8
+
9
+ attr_reader :connection
10
+
11
+ def initialize(opts = {})
12
+ @opts = PikaQue.config.merge(opts)
13
+ @opts[:amqp] = ENV.fetch('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672')
14
+ @opts[:vhost] = AMQ::Settings.parse_amqp_url(@opts[:amqp]).fetch(:vhost, '/')
15
+ end
16
+
17
+ def self.create(opts = {})
18
+ new(opts).tap{ |conn| conn.connect! }
19
+ end
20
+
21
+ def connect!
22
+ @connection ||= Bunny.new(@opts[:amqp], :vhost => @opts[:vhost],
23
+ :heartbeat => @opts[:heartbeat],
24
+ :properties => @opts.fetch(:properties, {}),
25
+ :logger => PikaQue::logger).tap do |conn|
26
+ conn.start
27
+ end
28
+ end
29
+
30
+ def connected?
31
+ @connection && @connection.connected?
32
+ end
33
+
34
+ def disconnect!
35
+ @connection.close if @connection
36
+ @connection = nil
37
+ end
38
+
39
+ def ensure_connection
40
+ unless connected?
41
+ @connection = nil
42
+ connect!
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ module PikaQue
2
+ class DelayWorker
3
+
4
+ attr_accessor :broker, :pool, :queue, :handler
5
+
6
+ def initialize(opts = {})
7
+ @opts = PikaQue.config.merge(opts)
8
+ @broker = @opts[:broker] || PikaQue::Broker.new(nil, @opts).tap{ |b| b.start }
9
+ @pool = @opts[:worker_pool] || Concurrent::FixedThreadPool.new(@opts[:concurrency] || 1)
10
+ @delay_name = "#{@opts[:exchange]}-delay"
11
+ end
12
+
13
+ def prepare
14
+ @queue = broker.queue(@delay_name, @opts[:queue_options])
15
+
16
+ @handler = broker.handler(@opts[:handler_class], @opts[:handler_options])
17
+ # TODO use routing keys?
18
+ @handler.bind_queue(@queue, @queue.name)
19
+ end
20
+
21
+ def run
22
+ @consumer = queue.subscribe(:block => false, :manual_ack => @opts[:ack]) do | delivery_info, metadata, msg |
23
+ pool.post do
24
+ work(delivery_info, metadata, msg)
25
+ end
26
+ end
27
+ end
28
+
29
+ def start
30
+ prepare
31
+ run
32
+ end
33
+
34
+ def stop
35
+ @consumer.cancel if @consumer
36
+ @consumer = nil
37
+
38
+ unless @opts[:worker_pool]
39
+ @pool.shutdown
40
+ @pool.wait_for_termination 12
41
+ end
42
+ broker.cleanup
43
+ broker.stop
44
+ end
45
+
46
+ def work(delivery_info, metadata, msg)
47
+ handler.handle(:ack, broker.channel, delivery_info, metadata, msg)
48
+ end
49
+
50
+ def logger
51
+ PikaQue.logger
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module PikaQue
2
+
3
+ class SetupError < StandardError; end
4
+
5
+ end
@@ -0,0 +1,31 @@
1
+ module PikaQue
2
+ module Handlers
3
+ class DefaultHandler
4
+
5
+ def initialize(opts = {})
6
+ # nothing to do here
7
+ end
8
+
9
+ def bind_queue(queue, routing_key)
10
+ end
11
+
12
+ def handle(response_code, channel, delivery_info, metadata, msg, error = nil)
13
+ case response_code
14
+ when :ack
15
+ PikaQue.logger.debug "DefaultHandler acknowledge <#{msg}>"
16
+ channel.acknowledge(delivery_info.delivery_tag, false)
17
+ when :requeue
18
+ PikaQue.logger.debug "DefaultHandler requeue <#{msg}>"
19
+ channel.reject(delivery_info.delivery_tag, true)
20
+ else
21
+ PikaQue.logger.debug "DefaultHandler reject <#{msg}>"
22
+ channel.reject(delivery_info.delivery_tag, false)
23
+ end
24
+ end
25
+
26
+ def close
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,124 @@
1
+ module PikaQue
2
+ module Handlers
3
+ class DelayHandler
4
+
5
+ # Create following exchanges with delay_name = pika-que-delay
6
+ # pika-que-delay
7
+ # pika-que-delay-requeue
8
+ # and following queues
9
+ # pika-que-delay-60
10
+ # pika-que-delay-600
11
+ # pika-que-delay-3600
12
+ # pika-que-delay-86400
13
+ #
14
+
15
+ # default delays are 1min, 10min, 1hr, 24hr
16
+ DEFAULT_DELAY_OPTS = {
17
+ :delay_periods => [60, 600, 3600, 86400],
18
+ :delay_backoff_multiplier => 1000,
19
+ }.freeze
20
+
21
+ def initialize(opts = {})
22
+ @opts = PikaQue.config.merge(DEFAULT_DELAY_OPTS).merge(opts)
23
+ @connection = opts[:connection] || PikaQue.connection
24
+ @channel = @connection.create_channel
25
+ @delay_monitor = Monitor.new
26
+ @root_monitor = Monitor.new
27
+
28
+ # make sure it is in descending order
29
+ @delay_periods = @opts[:delay_periods].sort!{ |x,y| y <=> x }
30
+ @backoff_multiplier = @opts[:delay_backoff_multiplier] # This is for example/dev/test
31
+
32
+ @delay_name = "#{@opts[:exchange]}-delay"
33
+ @requeue_name = "#{@opts[:exchange]}-delay-requeue"
34
+ @root_name = @opts[:exchange]
35
+
36
+ setup_exchanges
37
+ setup_queues
38
+ end
39
+
40
+ def bind_queue(queue, routing_key)
41
+ # bind the worker queue to requeue exchange
42
+ queue.bind(@requeue_exchange, :routing_key => routing_key)
43
+ end
44
+
45
+ def handle(response_code, channel, delivery_info, metadata, msg, error = nil)
46
+ delay_period = next_delay_period(metadata[:headers])
47
+ if delay_period > 0
48
+ # We will publish the message to the delay exchange
49
+ PikaQue.logger.info "DelayHandler msg=delaying, delay=#{delay_period}, headers=#{metadata[:headers]}"
50
+
51
+ publish_delay(delivery_info, msg, metadata[:headers].merge({ 'delay' => delay_period }))
52
+ channel.acknowledge(delivery_info.delivery_tag, false)
53
+ else
54
+ # Publish the original message with the routing_key to the root exchange
55
+ work_queue = metadata[:headers]['work_queue']
56
+ PikaQue.logger.info "DelayHandler msg=publishing, queue=#{work_queue}, headers=#{metadata[:headers]}"
57
+
58
+ publish_work(work_queue, msg)
59
+ channel.acknowledge(delivery_info.delivery_tag, false)
60
+ end
61
+ end
62
+
63
+ def close
64
+ @channel.close unless @channel.closed?
65
+ end
66
+
67
+ private
68
+
69
+ def setup_exchanges
70
+ PikaQue.logger.debug "DelayHandler creating exchange=#{@delay_name}"
71
+ @delay_exchange = @channel.exchange(@delay_name, :type => 'headers', :durable => exchange_durable?)
72
+
73
+ PikaQue.logger.debug "DelayHandler creating exchange=#{@requeue_name}"
74
+ @requeue_exchange = @channel.exchange(@requeue_name, :type => 'topic', :durable => exchange_durable?)
75
+
76
+ PikaQue.logger.debug "DelayHandler getting exchange=#{@root_name}"
77
+ @root_exchange = @channel.exchange(@root_name, :type => 'direct', :durable => exchange_durable?)
78
+ end
79
+
80
+ def setup_queues
81
+ @delay_periods.each do |t|
82
+ # Create the queues and bindings
83
+ PikaQue.logger.debug "DelayHandler creating queue=#{@delay_name}-#{t} x-dead-letter-exchange=#{@requeue_name}"
84
+
85
+ delay_queue = @channel.queue("#{@delay_name}-#{t}",
86
+ :durable => queue_durable?,
87
+ :arguments => {
88
+ :'x-dead-letter-exchange' => @requeue_name,
89
+ :'x-message-ttl' => t * @backoff_multiplier
90
+ })
91
+ delay_queue.bind(@delay_exchange, :arguments => { :delay => t })
92
+ end
93
+ end
94
+
95
+ def queue_durable?
96
+ @opts.fetch(:queue_options, {}).fetch(:durable, false)
97
+ end
98
+
99
+ def exchange_durable?
100
+ @opts.fetch(:exchange_options, {}).fetch(:durable, false)
101
+ end
102
+
103
+ def publish_delay(delivery_info, msg, headers)
104
+ @delay_monitor.synchronize do
105
+ @delay_exchange.publish(msg, routing_key: delivery_info.routing_key, headers: headers)
106
+ end
107
+ end
108
+
109
+ def publish_work(routing_key, msg)
110
+ @root_monitor.synchronize do
111
+ @root_exchange.publish(msg, routing_key: routing_key)
112
+ end
113
+ end
114
+
115
+ def next_delay_period(headers)
116
+ work_at = headers['work_at']
117
+ t = (work_at - Time.now.to_f).round
118
+ # greater check is to ignore remainder of time (seconds) smaller than the last delay
119
+ @delay_periods.bsearch{ |e| t >= e && (t / e.to_f).round > 0 } || 0
120
+ end
121
+
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,69 @@
1
+ module PikaQue
2
+ module Handlers
3
+ class ErrorHandler
4
+
5
+ DEFAULT_ERROR_OPTS = {
6
+ :exchange => 'pika-que-error',
7
+ :exchange_options => { :type => :topic },
8
+ :queue => 'pika-que-error',
9
+ :routing_key => '#'
10
+ }.freeze
11
+
12
+ def initialize(opts = {})
13
+ @opts = PikaQue.config.merge(DEFAULT_ERROR_OPTS).merge(opts)
14
+ @connection = @opts[:connection] || PikaQue.connection
15
+ @channel = @connection.create_channel
16
+ @exchange = @channel.exchange(@opts[:exchange], type: exchange_type, durable: exchange_durable?)
17
+ @queue = @channel.queue(@opts[:queue], durable: queue_durable?)
18
+ @queue.bind(@exchange, routing_key: @opts[:routing_key])
19
+ @monitor = Monitor.new
20
+ end
21
+
22
+ def bind_queue(queue, routing_key)
23
+ end
24
+
25
+ def handle(response_code, channel, delivery_info, metadata, msg, error = nil)
26
+ case response_code
27
+ when :ack
28
+ PikaQue.logger.debug "ErrorHandler acknowledge <#{msg}>"
29
+ channel.acknowledge(delivery_info.delivery_tag, false)
30
+ when :reject
31
+ PikaQue.logger.debug "ErrorHandler reject <#{msg}>"
32
+ channel.reject(delivery_info.delivery_tag, false)
33
+ when :requeue
34
+ PikaQue.logger.debug "ErrorHandler requeue <#{msg}>"
35
+ channel.reject(delivery_info.delivery_tag, true)
36
+ else
37
+ PikaQue.logger.debug "ErrorHandler publishing <#{msg}> to [#{@queue.name}]"
38
+ publish(delivery_info, msg)
39
+ channel.acknowledge(delivery_info.delivery_tag, false)
40
+ end
41
+ end
42
+
43
+ def close
44
+ @channel.close unless @channel.closed?
45
+ end
46
+
47
+ private
48
+
49
+ def queue_durable?
50
+ @opts.fetch(:queue_options, {}).fetch(:durable, false)
51
+ end
52
+
53
+ def exchange_durable?
54
+ @opts.fetch(:exchange_options, {}).fetch(:durable, false)
55
+ end
56
+
57
+ def exchange_type
58
+ @opts.fetch(:exchange_options, {}).fetch(:type, :topic)
59
+ end
60
+
61
+ def publish(delivery_info, msg)
62
+ @monitor.synchronize do
63
+ @exchange.publish(msg, routing_key: delivery_info.routing_key)
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,186 @@
1
+ module PikaQue
2
+ module Handlers
3
+ class RetryHandler
4
+
5
+ # Create following exchanges with retry_prefix = pika-que
6
+ # pika-que-retry
7
+ # pika-que-retry-requeue
8
+ # pika-que-error
9
+ # and following queues
10
+ # pika-que-retry-60 (default for const mode)
11
+ # pika-que-retry-120
12
+ # pika-que-retry-240
13
+ # pika-que-retry-480
14
+ # pika-que-retry-960
15
+ #
16
+ # retry_mode can be either :exp or :const
17
+
18
+ DEFAULT_RETRY_OPTS = {
19
+ :retry_prefix => 'pika-que',
20
+ :retry_mode => :exp,
21
+ :retry_max_times => 5,
22
+ :retry_backoff_base => 0,
23
+ :retry_backoff_multiplier => 1000,
24
+ :retry_const_backoff => 60
25
+ }.freeze
26
+
27
+ def initialize(opts = {})
28
+ @opts = PikaQue.config.merge(DEFAULT_RETRY_OPTS).merge(opts)
29
+ @connection = opts[:connection] || PikaQue.connection
30
+ @channel = @connection.create_channel
31
+ @retry_monitor = Monitor.new
32
+ @error_monitor = Monitor.new
33
+
34
+ @max_retries = @opts[:retry_max_times]
35
+ @backoff_base = @opts[:retry_backoff_base]
36
+ @backoff_multiplier = @opts[:retry_backoff_multiplier] # This is for example/dev/test
37
+
38
+ @retry_name = "#{@opts[:retry_prefix]}-retry"
39
+ @requeue_name = "#{@opts[:retry_prefix]}-retry-requeue"
40
+ @error_name = "#{@opts[:retry_prefix]}-error"
41
+
42
+ setup_exchanges
43
+ setup_queues
44
+ end
45
+
46
+ def bind_queue(queue, routing_key)
47
+ # bind the worker queue to requeue exchange
48
+ queue.bind(@requeue_exchange, :routing_key => routing_key)
49
+ end
50
+
51
+ def handle(response_code, channel, delivery_info, metadata, msg, error = nil)
52
+ case response_code
53
+ when :ack
54
+ PikaQue.logger.debug "RetryHandler acknowledge <#{msg}>"
55
+ channel.acknowledge(delivery_info.delivery_tag, false)
56
+ when :reject
57
+ PikaQue.logger.debug "RetryHandler reject retry <#{msg}>"
58
+ handle_retry(channel, delivery_info, metadata, msg, :reject)
59
+ when :requeue
60
+ PikaQue.logger.debug "RetryHandler requeue <#{msg}>"
61
+ channel.reject(delivery_info.delivery_tag, true)
62
+ else
63
+ PikaQue.logger.debug "RetryHandler error retry <#{msg}>"
64
+ handle_retry(channel, delivery_info, metadata, msg, error)
65
+ end
66
+ end
67
+
68
+ def close
69
+ @channel.close unless @channel.closed?
70
+ end
71
+
72
+
73
+ #####################################################
74
+ # formula
75
+ # base X = 0, 30, 60, 120, 180, etc defaults to 0
76
+ # (X + 15) * 2 ** (count + 1)
77
+ def self.backoff_periods(max_retries, backoff_base)
78
+ (1..max_retries).map{ |c| next_ttl(c, backoff_base) }
79
+ end
80
+
81
+ def self.next_ttl(count, backoff_base)
82
+ (backoff_base + 15) * 2 ** (count + 1)
83
+ end
84
+
85
+ private
86
+
87
+ def setup_exchanges
88
+ PikaQue.logger.debug "RetryHandler creating exchange=#{@retry_name}"
89
+ @retry_exchange = @channel.exchange(@retry_name, :type => 'headers', :durable => exchange_durable?)
90
+ @error_exchange, @requeue_exchange = [@error_name, @requeue_name].map do |name|
91
+ PikaQue.logger.debug "RetryHandler creating exchange=#{name}"
92
+ @channel.exchange(name, :type => 'topic', :durable => exchange_durable?)
93
+ end
94
+ end
95
+
96
+ def setup_queues
97
+ if @opts[:retry_mode] == :const
98
+ bo = @opts[:retry_const_backoff]
99
+ PikaQue.logger.debug "RetryHandler creating queue=#{@retry_name}-#{bo} x-dead-letter-exchange=#{@requeue_name}"
100
+ backoff_queue = @channel.queue("#{@retry_name}-#{bo}",
101
+ :durable => queue_durable?,
102
+ :arguments => {
103
+ :'x-dead-letter-exchange' => @requeue_name,
104
+ :'x-message-ttl' => bo * @backoff_multiplier
105
+ })
106
+ backoff_queue.bind(@retry_exchange, :arguments => { :backoff => bo })
107
+ else
108
+ backoffs = Expbackoff.backoff_periods(@max_retries, @backoff_base)
109
+ backoffs.each do |bo|
110
+ PikaQue.logger.debug "RetryHandler creating queue=#{@retry_name}-#{bo} x-dead-letter-exchange=#{@requeue_name}"
111
+ backoff_queue = @channel.queue("#{@retry_name}-#{bo}",
112
+ :durable => queue_durable?,
113
+ :arguments => {
114
+ :'x-dead-letter-exchange' => @requeue_name,
115
+ :'x-message-ttl' => bo * @backoff_multiplier
116
+ })
117
+ backoff_queue.bind(@retry_exchange, :arguments => { :backoff => bo })
118
+ end
119
+ end
120
+
121
+ PikaQue.logger.debug "RetryHandler creating queue=#{@error_name}"
122
+ @error_queue = @channel.queue(@error_name, :durable => queue_durable?)
123
+ @error_queue.bind(@error_exchange, :routing_key => '#')
124
+ end
125
+
126
+ def queue_durable?
127
+ @opts.fetch(:queue_options, {}).fetch(:durable, false)
128
+ end
129
+
130
+ def exchange_durable?
131
+ @opts.fetch(:exchange_options, {}).fetch(:durable, false)
132
+ end
133
+
134
+ def handle_retry(channel, delivery_info, metadata, msg, reason)
135
+ # +1 for the current attempt
136
+ num_attempts = failure_count(metadata[:headers]) + 1
137
+ if num_attempts <= @max_retries
138
+ # Publish message to the x-dead-letter-exchange (ie. retry exchange)
139
+ PikaQue.logger.info "RetryHandler msg=retrying, count=#{num_attempts}, headers=#{metadata[:headers] || {}}"
140
+
141
+ if @opts[:retry_mode] == :exp
142
+ backoff_ttl = Expbackoff.next_ttl(num_attempts, @backoff_base)
143
+ else
144
+ backoff_ttl = @opts[:retry_const_backoff]
145
+ end
146
+
147
+ publish_retry(delivery_info, msg, { backoff: backoff_ttl, count: num_attempts })
148
+ channel.acknowledge(delivery_info.delivery_tag, false)
149
+ else
150
+ PikaQue.logger.info "RetryHandler msg=failing, retry_count=#{num_attempts}, headers=#{metadata[:headers]}, reason=#{reason}"
151
+
152
+ publish_error(delivery_info, msg)
153
+ channel.acknowledge(delivery_info.delivery_tag, false)
154
+ end
155
+ end
156
+
157
+ # Uses the header to determine the number of failures this job has
158
+ # seen in the past. This does not count the current failure. So for
159
+ # instance, the first time the job fails, this will return 0, the second
160
+ # time, 1, etc.
161
+ # @param headers [Hash] Hash of headers that Rabbit delivers as part of
162
+ # the message
163
+ # @return [Integer] Count of number of failures.
164
+ def failure_count(headers)
165
+ if headers.nil? || headers['count'].nil?
166
+ 0
167
+ else
168
+ headers['count']
169
+ end
170
+ end
171
+
172
+ def publish_retry(delivery_info, msg, headers)
173
+ @retry_monitor.synchronize do
174
+ @retry_exchange.publish(msg, routing_key: delivery_info.routing_key, headers: headers)
175
+ end
176
+ end
177
+
178
+ def publish_error(delivery_info, msg)
179
+ @error_monitor.synchronize do
180
+ @error_exchange.publish(msg, routing_key: delivery_info.routing_key)
181
+ end
182
+ end
183
+
184
+ end
185
+ end
186
+ end