cuniculus 0.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +25 -0
- data/README.md +117 -0
- data/bin/cuniculus +11 -0
- data/lib/cuniculus.rb +88 -0
- data/lib/cuniculus/cli.rb +98 -0
- data/lib/cuniculus/config.rb +61 -0
- data/lib/cuniculus/consumer.rb +92 -0
- data/lib/cuniculus/core.rb +40 -0
- data/lib/cuniculus/exceptions.rb +39 -0
- data/lib/cuniculus/job_queue.rb +19 -0
- data/lib/cuniculus/logger.rb +27 -0
- data/lib/cuniculus/plugins.rb +46 -0
- data/lib/cuniculus/plugins/health_check.rb +108 -0
- data/lib/cuniculus/queue_config.rb +57 -0
- data/lib/cuniculus/rmq_pool.rb +44 -0
- data/lib/cuniculus/supervisor.rb +67 -0
- data/lib/cuniculus/version.rb +25 -0
- data/lib/cuniculus/worker.rb +30 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a706ee37ab059f8897e2aba8cd1ab3a6dc15cdde2eabc78d98ddecd5113afb2a
|
4
|
+
data.tar.gz: 02bc1b2265e346485e99f583ed3f3c3a619335238d7f1552d809490dea3d5a0d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bc3a8d8a26b299ffab344a642fc539e762fd03f3922bd165ff0611d44a0ecd62a39cd4a242fe529bc283e063764a15e399c2f4ad1dbbb4112c97863953f032ab
|
7
|
+
data.tar.gz: efc582168bc82aa2b5a66ecb42f26dc1d4fc36a73f95a9f46fb9028114a0781d0ad0f4cc10dc4fb41ff0f32f93550c8f745b80fb1e6a93c16a502e7f19e4a73e
|
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
BSD 2-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2020, Marcelo
|
4
|
+
All rights reserved.
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
8
|
+
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
10
|
+
list of conditions and the following disclaimer.
|
11
|
+
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
14
|
+
and/or other materials provided with the distribution.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Cuniculus
|
2
|
+
|
3
|
+
Ruby job queue backed by RabbitMQ. The word _cuniculus_ comes from the scientific name of the European rabbit (Oryctolagus cuniculus).
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
```sh
|
8
|
+
gem install cuniculus
|
9
|
+
```
|
10
|
+
|
11
|
+
_The following minimal example assumes RabbitMQ is running on `localhost:5672`; see the [configuration section](#configuration) for how to change this._
|
12
|
+
|
13
|
+
Create a worker class:
|
14
|
+
```ruby
|
15
|
+
# -- my_worker.rb
|
16
|
+
require 'cuniculus/worker'
|
17
|
+
|
18
|
+
class MyWorker
|
19
|
+
include Cuniculus::Worker
|
20
|
+
|
21
|
+
def perform(arg1, arg2)
|
22
|
+
puts "Processing:"
|
23
|
+
puts "arg1: #{arg1.inspect}"
|
24
|
+
puts "arg2: #{arg2.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Add jobs to queue:
|
30
|
+
```ruby
|
31
|
+
MyWorker.perform_async('x', [1, 2, 3])
|
32
|
+
```
|
33
|
+
|
34
|
+
Start the job consumer:
|
35
|
+
```sh
|
36
|
+
cuniculus -r my_worker.rb
|
37
|
+
```
|
38
|
+
|
39
|
+
### Example
|
40
|
+
|
41
|
+
There is also a more complete example in the Cuniculus repository itself. To run it, clone the repository, then
|
42
|
+
- start the Ruby and RabbitMQ containers using [Docker Compose](https://docs.docker.com/compose/):
|
43
|
+
```
|
44
|
+
docker-compose up -d
|
45
|
+
```
|
46
|
+
- from within the _cuniculus_ container, produce a job:
|
47
|
+
```
|
48
|
+
ruby -Ilib examples/produce.rb
|
49
|
+
```
|
50
|
+
- also from within the container, start the consumer:
|
51
|
+
```
|
52
|
+
bin/cuniculus -I examples/ -r example/init_cuniculus.rb
|
53
|
+
```
|
54
|
+
|
55
|
+
## Configuration
|
56
|
+
|
57
|
+
Configuration is done through code, using `Cuniculus.configure`.
|
58
|
+
|
59
|
+
Example:
|
60
|
+
```ruby
|
61
|
+
require "cuniculus"
|
62
|
+
|
63
|
+
# The following Hash is passed as is to Bunny, the library that integrates with RabbitMQ.
|
64
|
+
rabbitmq_conn = {
|
65
|
+
host: 'rabbitmq', # default is 127.0.0.1
|
66
|
+
port: 5672,
|
67
|
+
ssl: false,
|
68
|
+
vhost: '/',
|
69
|
+
user: 'guest',
|
70
|
+
pass: 'guest',
|
71
|
+
auth_mechanism: 'PLAIN',
|
72
|
+
}
|
73
|
+
|
74
|
+
Cuniculus.configure do |cfg|
|
75
|
+
cfg.rabbitmq_opts = rabbitmq_conn
|
76
|
+
cfg.pub_thr_pool_size = 5 # Only affects job producers
|
77
|
+
cfg.dead_queue_ttl = 1000 * 60 * 60 * 24 * 30 # keep failed jobs for 30 days
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
## Error handling
|
82
|
+
|
83
|
+
By default, exceptions raised when consuming a job are logged to STDOUT. This can be overriden with the `Cuniculus.error_handler` method:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
Cuniculus.error_handler do |e|
|
87
|
+
puts "Oh nein! #{e}"
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
The method expects a block that will receive an exception, and run in the scope of the Worker instance.
|
92
|
+
|
93
|
+
## Retry mechanism
|
94
|
+
|
95
|
+
Cuniculus declares a `cun_default` queue, together with some `cun_default_{n}` queues used for job retries.
|
96
|
+
When a job raises an exception, it is placed into the `cun_default_1` queue for the first retry. It stays there for some pre-defined time, and then gets moved back into the `cun_default` queue for execution.
|
97
|
+
|
98
|
+
If it fails again, it gets moved to `cun_default_2`, where it stays for a longer period until it's moved back directly into the `cun_default` queue again.
|
99
|
+
|
100
|
+
This goes on until there are no more retry attempts, in which case the job gets moved into the `cun_dead` queue. It can be then only be moved back into the `cun_default` queue manually; otherwise it is discarded after some time, defined as the `dead_queue_ttl`, in milliseconds (by default, 180 days).
|
101
|
+
|
102
|
+
Note that if a job cannot even be parsed, it is moved straight to the dead queue, as there's no point in retrying.
|
103
|
+
|
104
|
+
## How it works
|
105
|
+
|
106
|
+
Cuniculus code and conventions are very much inspired by another Ruby job queue library: [Sidekiq](https://github.com/mperham/sidekiq).
|
107
|
+
|
108
|
+
To communicate with RabbitMQ, Cuniculus uses [Bunny](https://github.com/ruby-amqp/bunny).
|
109
|
+
|
110
|
+
The first time an async job is produced, a thread pool is created, each thread with its own communication channel to RabbitMQ. These threads push jobs to RabbitMQ.
|
111
|
+
|
112
|
+
For consuming, each queue will have a corresponding thread pool (handled by Bunny) for concurrency.
|
113
|
+
|
114
|
+
## License
|
115
|
+
|
116
|
+
Cuniculus is licensed under the "BSD 2-Clause License". See [LICENSE](./LICENSE) for details.
|
117
|
+
|
data/bin/cuniculus
ADDED
data/lib/cuniculus.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cuniculus/version"
|
4
|
+
|
5
|
+
raise "Cuniculus #{Cuniculus.version} does not support Ruby versions below 2.6." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
|
6
|
+
|
7
|
+
require "cuniculus/logger"
|
8
|
+
require "cuniculus/config"
|
9
|
+
require "cuniculus/plugins"
|
10
|
+
require "cuniculus/rmq_pool"
|
11
|
+
require "cuniculus/supervisor"
|
12
|
+
|
13
|
+
# Base definition of the Cuniculus Module
|
14
|
+
module Cuniculus
|
15
|
+
|
16
|
+
# Configure Cuniculus.
|
17
|
+
#
|
18
|
+
# @yield [Cuniculus::Config]
|
19
|
+
#
|
20
|
+
# @example Change RabbitMQ connection details.
|
21
|
+
# Cuniculus.configure do |cfg|
|
22
|
+
# cfg.rabbitmq_opts = { host: 'rmq.mycompany.com', user: 'guest', pass: 'guest' }
|
23
|
+
# end
|
24
|
+
def self.configure
|
25
|
+
cfg = Cuniculus::Config.new
|
26
|
+
yield cfg
|
27
|
+
cfg.declare!
|
28
|
+
@config = cfg
|
29
|
+
Cuniculus::RMQPool.configure(cfg)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Current config of Cuniculus
|
33
|
+
#
|
34
|
+
# Returns config for read-only purpose. Use {Cuniculus.configure Cuniculus.configure} to change the configured values.
|
35
|
+
#
|
36
|
+
# @return [Cuniculus::Config]
|
37
|
+
def self.config
|
38
|
+
@config ||= Cuniculus::Config.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Current Cuniculus logger
|
42
|
+
#
|
43
|
+
# @return [Cuniculus::Logger]
|
44
|
+
def self.logger
|
45
|
+
@logger ||= Cuniculus::Logger.new($stdout, level: Logger::INFO)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Receives a block that is called when the job consumer encounters an error.
|
49
|
+
# The block receives the exception object and runs in the context of the consumer instance.
|
50
|
+
#
|
51
|
+
# Note that overriding the default error handler does not affect the retry mechanism. This error handler
|
52
|
+
# is designed to be used for logging.
|
53
|
+
#
|
54
|
+
# The default error handler is defined in {Cuniculus::Consumer#handle_error}.
|
55
|
+
#
|
56
|
+
# @example Send error info to an external service.
|
57
|
+
# Cuniculus.error_handler do |e|
|
58
|
+
# err = "#{e.class.name}: #{e.message}"
|
59
|
+
# bt = e.backtrace.join("\n") unless e.backtrace.nil?
|
60
|
+
# MyLogginService.error(err, bt)
|
61
|
+
# end
|
62
|
+
def self.error_handler(&block)
|
63
|
+
Cuniculus::Consumer.define_method(:handle_error, &block)
|
64
|
+
Cuniculus::Consumer.instance_eval { private :handle_error }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Load a plugin. If plugin is a Module, it is loaded directly.
|
68
|
+
# If it is a symbol, then it needs to satisfy the following:
|
69
|
+
# - The call `require "cuniculus/plugins/#{plugin}"` should succeed
|
70
|
+
# - The required plugin must register itself by calling {Cuniculus::Plugins.register_plugin}
|
71
|
+
#
|
72
|
+
# The additional arguments and block are passed to the plugin's `configure` method, if it exists.
|
73
|
+
#
|
74
|
+
# @param plugin [Symbol, Module]
|
75
|
+
# @param args [Array<Object>] *args passed to the plugin's `configure` method
|
76
|
+
# @param [Block] block passed to the plugin's `configure` method
|
77
|
+
#
|
78
|
+
# @example Enable `:health_check` plugin
|
79
|
+
# Cuniculus.plugin(:health_check)
|
80
|
+
def self.plugin(plugin, *args, &block)
|
81
|
+
plugin = Cuniculus::Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
82
|
+
raise Cuniculus::Error, "Invalid plugin type: #{plugin.class.inspect}. It must be a module" unless plugin.is_a?(Module)
|
83
|
+
|
84
|
+
self::Supervisor.send(:include, plugin::SupervisorMethods) if defined?(plugin::SupervisorMethods)
|
85
|
+
self::Supervisor.send(:extend, plugin::SupervisorClassMethods) if defined?(plugin::SupervisorClassMethods)
|
86
|
+
plugin.configure(config.opts, *args, &block) if plugin.respond_to?(:configure)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
|
5
|
+
require "optparse"
|
6
|
+
require "singleton"
|
7
|
+
|
8
|
+
require "cuniculus"
|
9
|
+
require "cuniculus/supervisor"
|
10
|
+
|
11
|
+
module Cuniculus
|
12
|
+
class CLI
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
attr_reader :options
|
16
|
+
|
17
|
+
def parse(args = ARGV)
|
18
|
+
@options = parse_options(args)
|
19
|
+
|
20
|
+
return unless options[:require]
|
21
|
+
|
22
|
+
raise ArgumentError, "Invalid '--require' argument: #{options[:require]}. File does not exist" unless File.exist?(options[:require])
|
23
|
+
raise ArgumentError, "Invalid '--require' argument: #{options[:require]}. Cannot be a directory" if File.directory?(options[:require])
|
24
|
+
require File.join(Dir.pwd, options[:require])
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
pipe_reader, pipe_writer = IO.pipe
|
29
|
+
sigs = %w[INT TERM]
|
30
|
+
|
31
|
+
sigs.each do |sig|
|
32
|
+
trap sig do
|
33
|
+
pipe_writer.puts(sig)
|
34
|
+
end
|
35
|
+
rescue ArgumentError
|
36
|
+
puts "Signal #{sig} not supported"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
launch(pipe_reader)
|
41
|
+
end
|
42
|
+
|
43
|
+
def launch(pipe_reader)
|
44
|
+
config = Cuniculus.config
|
45
|
+
supervisor = Cuniculus::Supervisor.new(config)
|
46
|
+
|
47
|
+
begin
|
48
|
+
Cuniculus.logger.info("Starting process")
|
49
|
+
supervisor.start
|
50
|
+
|
51
|
+
while (readable_io = IO.select([pipe_reader]))
|
52
|
+
signal = readable_io.first[0].gets.strip
|
53
|
+
handle_signal(signal)
|
54
|
+
end
|
55
|
+
rescue Interrupt
|
56
|
+
Cuniculus.logger.info("Interrupt received; shutting down")
|
57
|
+
supervisor.stop
|
58
|
+
Cuniculus.logger.info("Shutdown complete")
|
59
|
+
end
|
60
|
+
|
61
|
+
exit(0)
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_signal(sig)
|
65
|
+
case sig
|
66
|
+
when "INT", "TERM"
|
67
|
+
raise Interrupt
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def parse_options(argv)
|
74
|
+
opts = {}
|
75
|
+
@parser = option_parser(opts)
|
76
|
+
@parser.parse!(argv)
|
77
|
+
opts
|
78
|
+
end
|
79
|
+
|
80
|
+
def option_parser(opts)
|
81
|
+
OptionParser.new do |o|
|
82
|
+
o.on("-r", "--require [PATH]", "location of file required before starting consumer") do |arg|
|
83
|
+
opts[:require] = arg
|
84
|
+
end
|
85
|
+
|
86
|
+
o.on("-I", "--include [DIR]", "add directory to LOAD_PATH") do |arg|
|
87
|
+
$LOAD_PATH << arg
|
88
|
+
end
|
89
|
+
|
90
|
+
o.on "-V", "--version", "print version and exit" do |arg|
|
91
|
+
puts "Cuniculus #{Cuniculus.version}"
|
92
|
+
exit(0)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cuniculus/core"
|
4
|
+
require "cuniculus/queue_config"
|
5
|
+
|
6
|
+
module Cuniculus
|
7
|
+
class Config
|
8
|
+
ENFORCED_CONN_OPTS = {
|
9
|
+
threaded: false # No need for a reader thread, since this connection is only used for publishing
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
attr_accessor :dead_queue_ttl, :exchange_name, :pub_thr_pool_size, :rabbitmq_opts
|
13
|
+
attr_reader :queues, :opts
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@opts = {}
|
17
|
+
@queues = { "cun_default" => QueueConfig.new({ "name" => "cun_default" }) }
|
18
|
+
@rabbitmq_opts = {
|
19
|
+
host: "127.0.0.1",
|
20
|
+
port: 5672,
|
21
|
+
user: "guest",
|
22
|
+
pass: "guest",
|
23
|
+
vhost: "/"
|
24
|
+
}
|
25
|
+
@exchange_name = "cuniculus"
|
26
|
+
@dead_queue_ttl = 1000 * 60 * 60 * 24 * 180 # 180 days
|
27
|
+
end
|
28
|
+
|
29
|
+
def declare!
|
30
|
+
conn = ::Bunny.new(rabbitmq_opts.merge(ENFORCED_CONN_OPTS))
|
31
|
+
conn.start
|
32
|
+
ch = conn.create_channel
|
33
|
+
declare_exchanges!(ch)
|
34
|
+
declare_dead_queue!(ch)
|
35
|
+
@queues.each_value { |q| q.declare!(ch) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_queue=(bool)
|
39
|
+
@queues.delete("cun_default") unless bool
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def declare_exchanges!(ch)
|
45
|
+
ch.direct(Cuniculus::CUNICULUS_EXCHANGE, { durable: true })
|
46
|
+
ch.fanout(Cuniculus::CUNICULUS_DLX_EXCHANGE, { durable: true })
|
47
|
+
end
|
48
|
+
|
49
|
+
def declare_dead_queue!(ch)
|
50
|
+
ch.queue(
|
51
|
+
"cun_dead",
|
52
|
+
durable: true,
|
53
|
+
exclusive: false,
|
54
|
+
arguments: {
|
55
|
+
"x-message-ttl" => dead_queue_ttl
|
56
|
+
}
|
57
|
+
).
|
58
|
+
bind(Cuniculus::CUNICULUS_DLX_EXCHANGE)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cuniculus/core"
|
4
|
+
require "cuniculus/logger"
|
5
|
+
|
6
|
+
module Cuniculus
|
7
|
+
class Consumer
|
8
|
+
POLL_TIME = 5
|
9
|
+
JOB_REQUIRED_KEYS = %w[class args].freeze
|
10
|
+
|
11
|
+
attr_reader :channel, :exchange, :job_queue, :queue_config
|
12
|
+
|
13
|
+
def initialize(queue_config, channel)
|
14
|
+
@channel = channel
|
15
|
+
@queue_config = queue_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
@exchange = channel.direct(Cuniculus::CUNICULUS_EXCHANGE, { durable: true })
|
20
|
+
# channel.direct(Cuniculus::CUNICULUS_DLX_EXCHANGE, { durable: true })
|
21
|
+
@job_queue = queue_config.declare!(channel)
|
22
|
+
@_consumer = job_queue.subscribe(manual_ack: true, block: false) do |delivery_info, properties, payload|
|
23
|
+
run_job(delivery_info, properties, payload)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
@_consumer&.cancel
|
29
|
+
channel.close unless channel.closed?
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_job(delivery_info, _properties, payload)
|
33
|
+
item = parse_job(payload)
|
34
|
+
klass = Object.const_get(item["class"])
|
35
|
+
worker = klass.new
|
36
|
+
worker.perform(*item["args"])
|
37
|
+
channel.ack(delivery_info.delivery_tag, false)
|
38
|
+
rescue Cuniculus::BadlyFormattedPayload => ex
|
39
|
+
handle_error(ex)
|
40
|
+
# If parse failed, send message straight to DLX
|
41
|
+
channel.nack(delivery_info.delivery_tag, false, false)
|
42
|
+
rescue StandardError => ex
|
43
|
+
handle_error(Cuniculus.convert_exception_class(ex, Cuniculus::Error))
|
44
|
+
maybe_retry(delivery_info, item)
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_job(payload)
|
48
|
+
msg = Cuniculus.load_job(payload)
|
49
|
+
raise Cuniculus::BadlyFormattedPayload, "Consumed message with missing information: #{payload}\nIt should have keys [#{JOB_REQUIRED_KEYS.join(', ')}]" unless (JOB_REQUIRED_KEYS - msg.keys).empty?
|
50
|
+
|
51
|
+
msg
|
52
|
+
rescue Cuniculus::BadlyFormattedPayload
|
53
|
+
raise
|
54
|
+
rescue StandardError => ex
|
55
|
+
raise Cuniculus.convert_exception_class(ex, Cuniculus::BadlyFormattedPayload), "Badly formatted consumed message: #{payload}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def maybe_retry(delivery_info, item)
|
59
|
+
retry_count = item["_cun_retries"].to_i
|
60
|
+
retry_queue_name = job_queue.retry_queue(retry_count)
|
61
|
+
unless retry_queue_name
|
62
|
+
channel.nack(delivery_info.delivery_tag, false, false)
|
63
|
+
return
|
64
|
+
end
|
65
|
+
payload = Cuniculus.dump_job(item.merge("_cun_retries" => retry_count + 1))
|
66
|
+
exchange.publish(
|
67
|
+
payload,
|
68
|
+
{
|
69
|
+
routing_key: retry_queue_name,
|
70
|
+
persistent: true
|
71
|
+
}
|
72
|
+
)
|
73
|
+
channel.ack(delivery_info.delivery_tag, false)
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_error(e)
|
77
|
+
Cuniculus.logger.error("#{e.class.name}: #{e.message}")
|
78
|
+
Cuniculus.logger.error(e.backtrace.join("\n")) unless e.backtrace.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def constantize(str)
|
82
|
+
return Object.const_get(str) unless str.include?("::")
|
83
|
+
|
84
|
+
names = str.split("::")
|
85
|
+
names.shift if names.empty? || names.first.empty?
|
86
|
+
|
87
|
+
names.inject(Object) do |constant, name|
|
88
|
+
constant.const_get(name, false)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Cuniculus
|
6
|
+
CUNICULUS_EXCHANGE = "cuniculus"
|
7
|
+
CUNICULUS_DLX_EXCHANGE = "cuniculus_dlx" # Dead Letter Exchange
|
8
|
+
|
9
|
+
# Core Cuniculus methods
|
10
|
+
module CuniculusMethods
|
11
|
+
# Convert a RabbitMQ message into Ruby object for processing.
|
12
|
+
def load_job(rmq_msg)
|
13
|
+
::JSON.parse(rmq_msg)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Serializes a Ruby object for publishing to RabbitMQ.
|
17
|
+
def dump_job(job)
|
18
|
+
::JSON.dump(job)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convert the input `exception` to the given class. The given class should be
|
22
|
+
# {Cuniculus::Error} or a subclass. Returns an instance of `klass` with
|
23
|
+
# the message and backtrace of `exception`.
|
24
|
+
#
|
25
|
+
# @param exception [Exception] The exception being wrapped
|
26
|
+
# @param [Cuniculus::Error] The subclass of `Cuniculus::Error`
|
27
|
+
#
|
28
|
+
# @return [Cuniculus::Error] An instance of the input `Cuniculus::Error`
|
29
|
+
def convert_exception_class(exception, klass)
|
30
|
+
return exception if exception.is_a?(klass)
|
31
|
+
|
32
|
+
e = klass.new("#{exception.class}: #{exception.message}")
|
33
|
+
e.wrapped_exception = exception
|
34
|
+
e.set_backtrace(exception.backtrace)
|
35
|
+
e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
extend CuniculusMethods
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cuniculus
|
4
|
+
# Cuniculus-specific exceptions
|
5
|
+
#
|
6
|
+
# * `Cuniculus::Error`: Default exception raised by Cuniculus.
|
7
|
+
# All exceptions classes defined by Cuniculus descend from this class.
|
8
|
+
# * `Cuniculus::RMQConnectionError`: Raised when unable to connect to RabbitMQ.
|
9
|
+
# * `Cuniculus::RMQQueueConfigurationConflict`: Raised when the queue configuration
|
10
|
+
# given to Cuniculus conflicts with the current configuration of the same
|
11
|
+
# existing queue in RabbitMQ.
|
12
|
+
# * `Cuniculus::BadlyFormattedPayload`: Raised when Cuniculus consumer receives an
|
13
|
+
# improperly formatted job message.
|
14
|
+
|
15
|
+
class Error < ::StandardError
|
16
|
+
# If the Cuniculus exception wraps an underlying exception, the latter
|
17
|
+
# is held here.
|
18
|
+
attr_accessor :wrapped_exception
|
19
|
+
|
20
|
+
# Underlying exception `cause`
|
21
|
+
#
|
22
|
+
# @return [Exception#cause]
|
23
|
+
def cause
|
24
|
+
wrapped_exception || super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
(
|
29
|
+
RMQConnectionError = Class.new(Error)
|
30
|
+
).name
|
31
|
+
|
32
|
+
(
|
33
|
+
RMQQueueConfigurationConflict = Class.new(Error)
|
34
|
+
).name
|
35
|
+
|
36
|
+
(
|
37
|
+
BadlyFormattedPayload = Class.new(Error)
|
38
|
+
).name
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
module Cuniculus
|
5
|
+
class JobQueue
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@base_queue, :subscribe
|
9
|
+
|
10
|
+
def initialize(base_queue, retry_queue_names)
|
11
|
+
@base_queue = base_queue
|
12
|
+
@retry_queue_names = retry_queue_names
|
13
|
+
end
|
14
|
+
|
15
|
+
def retry_queue(retry_count)
|
16
|
+
@retry_queue_names[retry_count]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
module Cuniculus
|
7
|
+
class Logger < ::Logger
|
8
|
+
def initialize(*args, **kwargs)
|
9
|
+
super
|
10
|
+
self.formatter = Formatters::Standard.new
|
11
|
+
end
|
12
|
+
|
13
|
+
module Formatters
|
14
|
+
class Base
|
15
|
+
def tid
|
16
|
+
Thread.current["cuniculus_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Standard < Base
|
21
|
+
def call(severity, time, _program_name, message)
|
22
|
+
"#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid} #{severity}: #{message}\n"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cuniculus
|
4
|
+
# Base plugin load and registration module.
|
5
|
+
module Plugins
|
6
|
+
|
7
|
+
# Store for registered plugins
|
8
|
+
@plugins = {}
|
9
|
+
|
10
|
+
# Method that loads a plugin file. It should not be called directly; instead
|
11
|
+
# use {Cuniculus.plugin} method to add and configure a plugin.
|
12
|
+
#
|
13
|
+
# @param name [Symbol] name of plugin, also matching its file name.
|
14
|
+
#
|
15
|
+
# @return [Module]
|
16
|
+
def self.load_plugin(name)
|
17
|
+
h = @plugins
|
18
|
+
unless plugin = h[name]
|
19
|
+
require "cuniculus/plugins/#{name}"
|
20
|
+
raise Cuniculus::Error, "Plugin was not registered with 'register_plugin'" unless plugin = h[name]
|
21
|
+
end
|
22
|
+
plugin
|
23
|
+
end
|
24
|
+
|
25
|
+
# Include plugin module into a Hash so it can be referenced by its name.
|
26
|
+
# This method should be called by the plugin itself, so that when it is required
|
27
|
+
# (by {Cuniculus::Plugins.load_plugin}), it can be found.
|
28
|
+
#
|
29
|
+
# @param name [Symbol] Name of the plugin, matching its file name.
|
30
|
+
# @param mod [Module] The plugin module.
|
31
|
+
#
|
32
|
+
# @example Register a plugin named `my_plugin`
|
33
|
+
# # file: my_plugin.rb
|
34
|
+
# module Cuniculus
|
35
|
+
# module Plugins
|
36
|
+
# module MyPlugin
|
37
|
+
# end
|
38
|
+
# register_plugin(:my_plugin, MyPlugin)
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
def self.register_plugin(name, mod)
|
42
|
+
@plugins[name] = mod
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "thread"
|
5
|
+
|
6
|
+
module Cuniculus
|
7
|
+
module Plugins
|
8
|
+
# The HealthCheck plugin starts a TCP server together with consumers for health probing.
|
9
|
+
# It currently does not perform any additional checks returns '200 OK' regardless of whether
|
10
|
+
# - the node can connect to RabbitMQ;
|
11
|
+
# - consumers are stuck.
|
12
|
+
#
|
13
|
+
# The healthcheck stays up as long as the supervisor module is also running.
|
14
|
+
#
|
15
|
+
# Enable the plugin with:
|
16
|
+
# ```ruby
|
17
|
+
# Cuniculus.plugin(:health_check)
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# Options may be passed as well (use `String` keys):
|
21
|
+
# ```ruby
|
22
|
+
# opts = {
|
23
|
+
# "bind_to" => "127.0.0.1", # Default: "0.0.0.0"
|
24
|
+
# "port" => 8080 # Default: 3000
|
25
|
+
# }
|
26
|
+
# Cuniculus.plugin(:health_check, opts)
|
27
|
+
# ```
|
28
|
+
# This starts the server bound to 127.0.0.1 and port 8080.
|
29
|
+
#
|
30
|
+
# Note that the request path is not considered. The server responds with 200 to any path.
|
31
|
+
module HealthCheck
|
32
|
+
HEALTH_CHECK_RESPONSE = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK"
|
33
|
+
|
34
|
+
DEFAULTS = {
|
35
|
+
"bind_to" => "0.0.0.0",
|
36
|
+
"port" => 3000,
|
37
|
+
"server" => "webrick",
|
38
|
+
"block" => nil
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
OPTS_KEY = "__health_check_opts" # Key in the global plugin options where `:health_check` plugin options are stored.
|
42
|
+
|
43
|
+
# Configure `health_check` plugin
|
44
|
+
#
|
45
|
+
# @param plugins_cfg [Hash] Global plugin config hash, passed by Cuniculus. This should not be used by plugin users.
|
46
|
+
# @param opts [Hash] Plugin specific options.
|
47
|
+
# @option opts [String] "bind_to" IP address to bind to (default: "0.0.0.0")
|
48
|
+
# @option opts [Numeric] "port" Port number to bind to (default: 3000)
|
49
|
+
def self.configure(plugins_cfg, opts = {}, &block)
|
50
|
+
invalid_opts = opts.keys - DEFAULTS.keys
|
51
|
+
raise Cuniculus::Error, "Invalid option keys for :health_check plugin: #{invalid_opts}" unless invalid_opts.empty?
|
52
|
+
|
53
|
+
plugins_cfg[OPTS_KEY] = h = opts.slice("bind_to", "port", "server")
|
54
|
+
h["block"] = block if block
|
55
|
+
DEFAULTS.each do |k, v|
|
56
|
+
h[k] = v if v && !h.key?(k)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module SupervisorMethods
|
61
|
+
def start
|
62
|
+
hc_rd, @hc_wr = IO.pipe
|
63
|
+
start_health_check_server(hc_rd)
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop
|
68
|
+
@hc_wr << "a"
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def start_health_check_server(pipe_reader)
|
76
|
+
opts = config.opts[OPTS_KEY]
|
77
|
+
server = ::TCPServer.new(opts["bind_to"], opts["port"])
|
78
|
+
|
79
|
+
# If port was assigned by OS (when 'port' option was given as 0),
|
80
|
+
# now override input value with it.
|
81
|
+
opts["port"] = server.addr[1]
|
82
|
+
@hc_thread = Thread.new do
|
83
|
+
sock = nil
|
84
|
+
done = false
|
85
|
+
loop do
|
86
|
+
begin
|
87
|
+
break if done
|
88
|
+
sock = server.accept_nonblock
|
89
|
+
rescue IO::WaitReadable, Errno::EINTR
|
90
|
+
io = IO.select([server, pipe_reader])
|
91
|
+
done = true if io.first.include?(pipe_reader)
|
92
|
+
retry
|
93
|
+
end
|
94
|
+
|
95
|
+
sock.print HEALTH_CHECK_RESPONSE
|
96
|
+
sock.shutdown
|
97
|
+
end
|
98
|
+
|
99
|
+
sock&.close if sock && !sock.closed?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
register_plugin(:health_check, HealthCheck)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cuniculus/core"
|
4
|
+
require "cuniculus/exceptions"
|
5
|
+
require "cuniculus/job_queue"
|
6
|
+
|
7
|
+
module Cuniculus
|
8
|
+
class QueueConfig
|
9
|
+
OPTS = {}.freeze
|
10
|
+
|
11
|
+
DEFAULT_MAX_RETRY = 4
|
12
|
+
|
13
|
+
attr_reader :max_retry, :name, :thread_pool_size
|
14
|
+
|
15
|
+
def initialize(opts = OPTS)
|
16
|
+
@name = read_opt(opts, "name") || "cun_default"
|
17
|
+
@max_retry = read_opt(opts, "max_retry") || DEFAULT_MAX_RETRY
|
18
|
+
@thread_pool_size = read_opt(opts, "thread_pool_size")
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_opt(opts, key)
|
22
|
+
opts[key.to_s] || opts[key.to_sym]
|
23
|
+
end
|
24
|
+
|
25
|
+
def declare!(channel)
|
26
|
+
queue_name = name
|
27
|
+
base_q = channel.queue(
|
28
|
+
queue_name,
|
29
|
+
durable: true,
|
30
|
+
exclusive: false,
|
31
|
+
arguments: { "x-dead-letter-exchange" => Cuniculus::CUNICULUS_DLX_EXCHANGE }
|
32
|
+
)
|
33
|
+
base_q.bind(Cuniculus::CUNICULUS_EXCHANGE, { routing_key: name })
|
34
|
+
|
35
|
+
retry_queue_names = (1..max_retry).map { |i| "#{name}_#{i}" }
|
36
|
+
max_retry.times do |i|
|
37
|
+
queue_name = retry_queue_names[i]
|
38
|
+
|
39
|
+
q = channel.queue(
|
40
|
+
queue_name,
|
41
|
+
durable: true,
|
42
|
+
exclusive: false,
|
43
|
+
arguments: {
|
44
|
+
"x-dead-letter-exchange" => Cuniculus::CUNICULUS_EXCHANGE,
|
45
|
+
"x-dead-letter-routing-key" => name,
|
46
|
+
"x-message-ttl" => ((i**4) + (15 * (i + 1))) * 1000
|
47
|
+
}
|
48
|
+
)
|
49
|
+
q.bind(Cuniculus::CUNICULUS_EXCHANGE, { routing_key: queue_name })
|
50
|
+
end
|
51
|
+
|
52
|
+
Cuniculus::JobQueue.new(base_q, retry_queue_names)
|
53
|
+
rescue Bunny::PreconditionFailed => e
|
54
|
+
raise Cuniculus.convert_exception_class(e, Cuniculus::RMQQueueConfigurationConflict), "Declaration failed for queue '#{queue_name}'"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bunny"
|
4
|
+
require "connection_pool"
|
5
|
+
require "cuniculus/core"
|
6
|
+
require "cuniculus/config"
|
7
|
+
|
8
|
+
module Cuniculus
|
9
|
+
class RMQPool
|
10
|
+
ENFORCED_CONN_OPTS = {
|
11
|
+
threaded: false # No need for a reader thread, since this connection is only used for publishing
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure(config)
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def config
|
20
|
+
@config ||= Cuniculus::Config.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def init!
|
24
|
+
@conn = ::Bunny.new(@config.rabbitmq_opts.merge(ENFORCED_CONN_OPTS))
|
25
|
+
@conn.start
|
26
|
+
@channel_pool = ConnectionPool.new(timeout: 1, size: @config.pub_thr_pool_size) do
|
27
|
+
ch = @conn.create_channel
|
28
|
+
ch.direct(Cuniculus::CUNICULUS_EXCHANGE, { durable: true })
|
29
|
+
ch.fanout(Cuniculus::CUNICULUS_DLX_EXCHANGE, { durable: true })
|
30
|
+
ch
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_exchange(&block)
|
35
|
+
init! unless @channel_pool
|
36
|
+
@channel_pool.with do |ch|
|
37
|
+
block.call(ch.exchanges["cuniculus"])
|
38
|
+
ensure
|
39
|
+
ch.open if ch.closed?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bunny"
|
4
|
+
|
5
|
+
require "cuniculus/core"
|
6
|
+
require "cuniculus/exceptions"
|
7
|
+
require "cuniculus/consumer"
|
8
|
+
|
9
|
+
module Cuniculus
|
10
|
+
module SupervisorMethods
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
conn = connect(config.rabbitmq_opts)
|
16
|
+
@consumers = create_consumers(conn, config.queues)
|
17
|
+
@consumer_lock = Mutex.new
|
18
|
+
@done = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@consumers.each(&:start)
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
@done = true
|
27
|
+
@consumers.each(&:stop)
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect(conn_opts)
|
31
|
+
conn = ::Bunny.new(conn_opts)
|
32
|
+
conn.start
|
33
|
+
conn
|
34
|
+
rescue StandardError => e
|
35
|
+
raise Cuniculus.convert_exception_class(e, Cuniculus::RMQConnectionError)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_consumers(conn, queues)
|
39
|
+
consumers = []
|
40
|
+
consumer_pool_size = 5
|
41
|
+
queues.each do |_name, q_cfg|
|
42
|
+
ch = conn.create_channel(nil, consumer_pool_size)
|
43
|
+
consumers << Cuniculus::Consumer.new(q_cfg, ch)
|
44
|
+
end
|
45
|
+
consumers
|
46
|
+
end
|
47
|
+
|
48
|
+
def consumer_exception(consumer, _ex)
|
49
|
+
@consumer_lock.synchronize do
|
50
|
+
@consumers.delete(consumer)
|
51
|
+
unless @done
|
52
|
+
# Reuse channel
|
53
|
+
ch = consumer.channel
|
54
|
+
name = consumer.queue.name
|
55
|
+
c = Cuniculus::Consumer.new(self, name, ch)
|
56
|
+
@consumers << c
|
57
|
+
c.start
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Supervisor
|
64
|
+
include SupervisorMethods
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Cuniculus
|
4
|
+
# The major version of Cuniculus. Only bumped for major changes.
|
5
|
+
MAJOR = 0
|
6
|
+
|
7
|
+
# The minor version of Cuniculus. Bumped for every non-patch level
|
8
|
+
# release.
|
9
|
+
MINOR = 0
|
10
|
+
|
11
|
+
# The tiny version of Cuniculus. Usually 0, only bumped for bugfix
|
12
|
+
# releases that fix regressions from previous versions.
|
13
|
+
TINY = 1
|
14
|
+
|
15
|
+
# The version of Cuniculus you are using, as a string (e.g. "2.11.0")
|
16
|
+
VERSION = [MAJOR, MINOR, TINY].join(".").freeze
|
17
|
+
|
18
|
+
# The version of Cuniculus you are using, as a number (2.11.0 -> 20110)
|
19
|
+
VERSION_NUMBER = MAJOR * 10_000 + MINOR * 10 + TINY
|
20
|
+
|
21
|
+
# The version of Cuniculus you are using, as a string (e.g. "2.11.0")
|
22
|
+
def self.version
|
23
|
+
VERSION
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cuniculus/core"
|
4
|
+
require "cuniculus/rmq_pool"
|
5
|
+
|
6
|
+
module Cuniculus
|
7
|
+
module Worker
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def perform_async(*args)
|
14
|
+
publish({ "class" => self, "args" => args })
|
15
|
+
end
|
16
|
+
|
17
|
+
def publish(item)
|
18
|
+
routing_key = "cun_default"
|
19
|
+
payload = normalize_item(item)
|
20
|
+
Cuniculus::RMQPool.with_exchange do |x|
|
21
|
+
x.publish(payload, { routing_key: routing_key, persistent: true })
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def normalize_item(item)
|
26
|
+
Cuniculus.dump_job(item)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cuniculus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marcelo Pereira
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bunny
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.15.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.15.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: connection_pool
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.2.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.2.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: redcarpet
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: warning
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Job queue processing backed by RabbitMQ
|
126
|
+
email:
|
127
|
+
executables:
|
128
|
+
- cuniculus
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files:
|
131
|
+
- README.md
|
132
|
+
- CHANGELOG.md
|
133
|
+
files:
|
134
|
+
- CHANGELOG.md
|
135
|
+
- LICENSE
|
136
|
+
- README.md
|
137
|
+
- bin/cuniculus
|
138
|
+
- lib/cuniculus.rb
|
139
|
+
- lib/cuniculus/cli.rb
|
140
|
+
- lib/cuniculus/config.rb
|
141
|
+
- lib/cuniculus/consumer.rb
|
142
|
+
- lib/cuniculus/core.rb
|
143
|
+
- lib/cuniculus/exceptions.rb
|
144
|
+
- lib/cuniculus/job_queue.rb
|
145
|
+
- lib/cuniculus/logger.rb
|
146
|
+
- lib/cuniculus/plugins.rb
|
147
|
+
- lib/cuniculus/plugins/health_check.rb
|
148
|
+
- lib/cuniculus/queue_config.rb
|
149
|
+
- lib/cuniculus/rmq_pool.rb
|
150
|
+
- lib/cuniculus/supervisor.rb
|
151
|
+
- lib/cuniculus/version.rb
|
152
|
+
- lib/cuniculus/worker.rb
|
153
|
+
homepage: https://github.com/MarcPer/cuniculus
|
154
|
+
licenses:
|
155
|
+
- BSD-2-Clause
|
156
|
+
metadata:
|
157
|
+
source_code_uri: https://github.com/MarcPer/cuniculus
|
158
|
+
bug_tracker_uri: https://github.com/MarcPer/cuniculus/issues
|
159
|
+
changelog_uri: https://github.com/MarcPer/cuniculus/CHANGELOG.md
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options:
|
162
|
+
- "--quiet"
|
163
|
+
- "--line-numbers"
|
164
|
+
- "--inline-source"
|
165
|
+
- "--title"
|
166
|
+
- 'Cuniculus: Background job processing with RabbitMQ'
|
167
|
+
- "--main"
|
168
|
+
- README.rdoc
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '2.6'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubygems_version: 3.1.4
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Job queue processing backed by RabbitMQ
|
186
|
+
test_files: []
|