pika_que 0.1.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 (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