rabbit_carrots 0.1.20 → 1.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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +12 -5
- data/lib/puma/plugin/rabbit_carrots.rb +85 -0
- data/lib/rabbit_carrots/configuration.rb +10 -1
- data/lib/rabbit_carrots/connection.rb +5 -1
- data/lib/rabbit_carrots/core.rb +113 -0
- data/lib/rabbit_carrots/tasks/rmq.rake +5 -69
- data/lib/rabbit_carrots/version.rb +1 -1
- data/lib/rabbit_carrots.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a55e06c9742638b1eb1a44d65d98031f5c366adddd3fcb196064075f0c32eb30
|
4
|
+
data.tar.gz: 499acd59987bb9a6d4548c444e0829117ecc16289c719461e0b5e3be255df02c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99f30870c37f1d334673fd6b6d5083cd4b2d4af419384590c204db60cddf6f16993582eeecdcf924a97302b13f5713772bdb9554d2d86757625c9aa689cd3456
|
7
|
+
data.tar.gz: 29586c246e82ab514b1fb8de9ffc4013865fa7e72ee07e6ddfec86fb7a4084c597272b1809b90708dab793325f5ff4c7129fcc79d301a743ae4238e2d60bad23
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rabbit_carrots (0.1
|
4
|
+
rabbit_carrots (1.0.1)
|
5
5
|
bunny (>= 2.22)
|
6
6
|
connection_pool (~> 2.4)
|
7
7
|
|
@@ -80,7 +80,7 @@ GEM
|
|
80
80
|
rubocop-ast (>= 1.30.0, < 2.0)
|
81
81
|
ruby-progressbar (1.13.0)
|
82
82
|
ruby2_keywords (0.0.5)
|
83
|
-
set (1.0
|
83
|
+
set (1.1.0)
|
84
84
|
sorted_set (1.0.3)
|
85
85
|
rbtree
|
86
86
|
set (~> 1.0)
|
data/README.md
CHANGED
@@ -33,6 +33,9 @@ RabbitCarrots.configure do |c|
|
|
33
33
|
c.rabbitmq_password = ENV.fetch('RABBITMQ__PASSWORD', nil)
|
34
34
|
c.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST', nil)
|
35
35
|
c.rabbitmq_exchange_name = ENV.fetch('RABBITMQ__EXCHANGE_NAME', nil)
|
36
|
+
c.automatically_recover = true
|
37
|
+
c.network_recovery_interval = 5
|
38
|
+
c.recovery_attempts = 5
|
36
39
|
c.routing_key_mappings = [
|
37
40
|
{ routing_keys: ['RK1', 'RK2'], queue: 'QUEUE_NAME', handler: 'CLASS HANDLER IN STRING' },
|
38
41
|
{ routing_keys: ['RK1', 'RK2'], queue: 'QUEUE_NAME', handler: 'CLASS HANDLER IN STRING' }
|
@@ -41,8 +44,6 @@ end
|
|
41
44
|
|
42
45
|
```
|
43
46
|
|
44
|
-
|
45
|
-
|
46
47
|
Note that handler is a class that must implement a method named ```handle!``` that takes 4 parameters as follow:
|
47
48
|
|
48
49
|
```ruby
|
@@ -53,8 +54,6 @@ class DummyEventHandler
|
|
53
54
|
end
|
54
55
|
```
|
55
56
|
|
56
|
-
|
57
|
-
|
58
57
|
Inside the handle message, you can NACK the message without re-queuing by raising ```RabbitCarrots::EventHandlers::Errors::NackMessage``` exception.
|
59
58
|
|
60
59
|
To NACK and re-queue, raise ```RabbitCarrots::EventHandlers::Errors::NackAndRequeueMessage``` exception.
|
@@ -65,8 +64,16 @@ Note: Any other unrescued exception raised inside ```handle!``` the that is a su
|
|
65
64
|
|
66
65
|
### Running
|
67
66
|
|
68
|
-
|
67
|
+
For better scalability and improved performance, you can run rabbit_carrots in standalone mode by invoking the following command:
|
68
|
+
```bundle exec rake rabbit_carrots:eat```.
|
69
|
+
|
70
|
+
#### Puma
|
71
|
+
|
72
|
+
For small and medium sized projects, you can delegate the management of the rabbit_carrots to the Puma web server. To achieve that, add the following line to your puma.rb
|
73
|
+
|
74
|
+
```plugin :rabbit_carrots```
|
69
75
|
|
76
|
+
This will make sure that Puma will manage rabbit carrots as a background service and will gracefully terminate if rabbit_carrots eventually loses connection after multiple automatic recovery.
|
70
77
|
## Development
|
71
78
|
|
72
79
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# rabbit_carrots.rb
|
2
|
+
|
3
|
+
require 'puma/plugin'
|
4
|
+
require 'rabbit_carrots'
|
5
|
+
|
6
|
+
Puma::Plugin.create do
|
7
|
+
attr_reader :puma_pid, :rabbit_carrots_pid, :log_writer, :core_service
|
8
|
+
|
9
|
+
def start(launcher)
|
10
|
+
@log_writer = launcher.log_writer
|
11
|
+
@puma_pid = $$
|
12
|
+
|
13
|
+
@core_service = RabbitCarrots::Core.new(logger: log_writer)
|
14
|
+
|
15
|
+
in_background do
|
16
|
+
monitor_rabbit_carrots
|
17
|
+
end
|
18
|
+
|
19
|
+
launcher.events.on_booted do
|
20
|
+
@rabbit_carrots_pid = fork do
|
21
|
+
Thread.new { monitor_puma }
|
22
|
+
start_rabbit_carrots_consumer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
launcher.events.on_stopped { stop_rabbit_carrots }
|
27
|
+
launcher.events.on_restart { stop_rabbit_carrots }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def start_rabbit_carrots_consumer
|
33
|
+
core_service.start(kill_to_restart_on_standard_error: true)
|
34
|
+
rescue StandardError => e
|
35
|
+
Rails.logger.error "Error starting Rabbit Carrots: #{e.message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop_rabbit_carrots
|
39
|
+
return unless rabbit_carrots_pid
|
40
|
+
|
41
|
+
log 'Stopping Rabbit Carrots...'
|
42
|
+
core_service.request_shutdown
|
43
|
+
Process.kill('TERM', rabbit_carrots_pid)
|
44
|
+
Process.wait(rabbit_carrots_pid)
|
45
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
46
|
+
end
|
47
|
+
|
48
|
+
def monitor_puma
|
49
|
+
monitor(:puma_dead?, 'Detected Puma has gone away, stopping Rabbit Carrots...')
|
50
|
+
end
|
51
|
+
|
52
|
+
def monitor_rabbit_carrots
|
53
|
+
monitor(:rabbit_carrots_dead?, 'Rabbits Carrot is dead, stopping Puma...')
|
54
|
+
end
|
55
|
+
|
56
|
+
def monitor(process_dead, message)
|
57
|
+
loop do
|
58
|
+
if send(process_dead)
|
59
|
+
log message
|
60
|
+
Process.kill('TERM', $$)
|
61
|
+
break
|
62
|
+
end
|
63
|
+
sleep 2
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def rabbit_carrots_dead?
|
68
|
+
Process.waitpid(rabbit_carrots_pid, Process::WNOHANG) if rabbit_carrots_started?
|
69
|
+
false
|
70
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def rabbit_carrots_started?
|
75
|
+
rabbit_carrots_pid.present?
|
76
|
+
end
|
77
|
+
|
78
|
+
def puma_dead?
|
79
|
+
Process.ppid != puma_pid
|
80
|
+
end
|
81
|
+
|
82
|
+
def log(...)
|
83
|
+
log_writer.log(...)
|
84
|
+
end
|
85
|
+
end
|
@@ -9,6 +9,15 @@ module RabbitCarrots
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class Configuration
|
12
|
-
attr_accessor :rabbitmq_host,
|
12
|
+
attr_accessor :rabbitmq_host,
|
13
|
+
:rabbitmq_port,
|
14
|
+
:rabbitmq_user,
|
15
|
+
:rabbitmq_password,
|
16
|
+
:rabbitmq_vhost,
|
17
|
+
:routing_key_mappings,
|
18
|
+
:rabbitmq_exchange_name,
|
19
|
+
:automatically_recover,
|
20
|
+
:network_recovery_interval,
|
21
|
+
:recovery_attempts
|
13
22
|
end
|
14
23
|
end
|
@@ -11,7 +11,11 @@ module RabbitCarrots
|
|
11
11
|
port: RabbitCarrots.configuration.rabbitmq_port,
|
12
12
|
user: RabbitCarrots.configuration.rabbitmq_user,
|
13
13
|
password: RabbitCarrots.configuration.rabbitmq_password,
|
14
|
-
vhost: RabbitCarrots.configuration.rabbitmq_vhost
|
14
|
+
vhost: RabbitCarrots.configuration.rabbitmq_vhost,
|
15
|
+
automatically_recover: RabbitCarrots.configuration.automatically_recover || true,
|
16
|
+
network_recovery_interval: RabbitCarrots.configuration.network_recovery_interval || 5,
|
17
|
+
recovery_attempts: RabbitCarrots.configuration.recovery_attempts || 5,
|
18
|
+
recovery_attempts_exhausted: -> { Process.kill('TERM', Process.pid) }
|
15
19
|
)
|
16
20
|
|
17
21
|
@connection.start
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module RabbitCarrots
|
2
|
+
class Core
|
3
|
+
attr_reader :logger
|
4
|
+
|
5
|
+
DatabaseAgonsticNotNullViolation = defined?(ActiveRecord) ? ActiveRecord::NotNullViolation : RabbitCarrots::EventHandlers::Errors::PlaceholderError
|
6
|
+
DatabaseAgonsticConnectionNotEstablished = defined?(ActiveRecord) ? ActiveRecord::ConnectionNotEstablished : Mongo::Error::SocketError
|
7
|
+
DatabaseAgnosticRecordInvalid = defined?(ActiveRecord) ? ActiveRecord::RecordInvalid : Mongoid::Errors::Validations
|
8
|
+
|
9
|
+
def initialize(logger: nil)
|
10
|
+
@logger = logger || Logger.new(Rails.env.production? ? '/proc/self/fd/1' : $stdout)
|
11
|
+
@threads = []
|
12
|
+
@running = true
|
13
|
+
@shutdown_requested = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def start(kill_to_restart_on_standard_error: false)
|
17
|
+
channels = RabbitCarrots.configuration.routing_key_mappings.map do |mapping|
|
18
|
+
{ **mapping, handler: mapping[:handler].constantize }
|
19
|
+
end
|
20
|
+
|
21
|
+
channels.each do |channel|
|
22
|
+
handler_class = channel[:handler]
|
23
|
+
raise "#{handler_class.name} must respond to `handle!`" unless handler_class.respond_to?(:handle!)
|
24
|
+
|
25
|
+
@threads << Thread.new do
|
26
|
+
run_task(
|
27
|
+
queue_name: channel[:queue],
|
28
|
+
handler_class:,
|
29
|
+
routing_keys: channel[:routing_keys],
|
30
|
+
queue_arguments: channel[:arguments],
|
31
|
+
kill_to_restart_on_standard_error:
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Signal.trap('INT') { request_shutdown }
|
37
|
+
Signal.trap('TERM') { request_shutdown }
|
38
|
+
|
39
|
+
while @running
|
40
|
+
if @shutdown_requested
|
41
|
+
request_shutdown
|
42
|
+
sleep 1
|
43
|
+
break
|
44
|
+
end
|
45
|
+
sleep 1
|
46
|
+
end
|
47
|
+
|
48
|
+
@threads.each(&:join)
|
49
|
+
rescue StandardError => e
|
50
|
+
logger.error "Error starting Rabbit Carrots: #{e.message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def request_shutdown
|
54
|
+
# Workaround to a known issue with Signal Traps and logs
|
55
|
+
Thread.start do
|
56
|
+
logger.log 'Shutting down Rabbit Carrots service...'
|
57
|
+
end
|
58
|
+
@shutdown_requested = true
|
59
|
+
@threads.each(&:kill)
|
60
|
+
stop
|
61
|
+
end
|
62
|
+
|
63
|
+
def stop
|
64
|
+
# Workaround to a known issue with Signal Traps and logs
|
65
|
+
Thread.start do
|
66
|
+
logger.log 'Stoppig the Rabbit Carrots service...'
|
67
|
+
end
|
68
|
+
@running = false
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_task(queue_name:, handler_class:, routing_keys:, queue_arguments: {}, kill_to_restart_on_standard_error: false)
|
72
|
+
RabbitCarrots::Connection.instance.channel.with do |channel|
|
73
|
+
exchange = channel.topic(RabbitCarrots.configuration.rabbitmq_exchange_name, durable: true)
|
74
|
+
|
75
|
+
logger.log "Listening on QUEUE: #{queue_name} for ROUTING KEYS: #{routing_keys}"
|
76
|
+
queue = channel.queue(queue_name, durable: true, arguments: queue_arguments)
|
77
|
+
|
78
|
+
routing_keys.map(&:strip).each { |k| queue.bind(exchange, routing_key: k) }
|
79
|
+
|
80
|
+
queue.subscribe(block: false, manual_ack: true, prefetch: 10) do |delivery_info, properties, payload|
|
81
|
+
break if @shutdown_requested
|
82
|
+
|
83
|
+
logger.log "Received from queue: #{queue_name}, Routing Keys: #{routing_keys}"
|
84
|
+
handler_class.handle!(channel, delivery_info, properties, payload)
|
85
|
+
channel.ack(delivery_info.delivery_tag, false)
|
86
|
+
rescue RabbitCarrots::EventHandlers::Errors::NackMessage, JSON::ParserError => _e
|
87
|
+
logger.log "Nacked message: #{payload}"
|
88
|
+
channel.nack(delivery_info.delivery_tag, false, false)
|
89
|
+
rescue RabbitCarrots::EventHandlers::Errors::NackAndRequeueMessage => _e
|
90
|
+
logger.log "Nacked and Requeued message: #{payload}"
|
91
|
+
channel.nack(delivery_info.delivery_tag, false, true)
|
92
|
+
rescue DatabaseAgonsticNotNullViolation, DatabaseAgnosticRecordInvalid => e
|
93
|
+
logger.log "Null constraint or Invalid violation: #{payload}. Error: #{e.message}"
|
94
|
+
channel.ack(delivery_info.delivery_tag, false)
|
95
|
+
rescue DatabaseAgonsticConnectionNotEstablished => e
|
96
|
+
logger.log "Error connection not established to the database: #{payload}. Error: #{e.message}"
|
97
|
+
sleep 3
|
98
|
+
channel.nack(delivery_info.delivery_tag, false, true)
|
99
|
+
rescue StandardError => e
|
100
|
+
logger.log "Error handling message: #{payload}. Error: #{e.message}"
|
101
|
+
sleep 3
|
102
|
+
channel.nack(delivery_info.delivery_tag, false, true)
|
103
|
+
Process.kill('SIGTERM', Process.pid) if kill_to_restart_on_standard_error
|
104
|
+
end
|
105
|
+
|
106
|
+
logger.log "Ending task for queue: #{queue_name}"
|
107
|
+
end
|
108
|
+
rescue StandardError => e
|
109
|
+
logger.error "Bunny session error: #{e.message}"
|
110
|
+
request_shutdown
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -1,77 +1,13 @@
|
|
1
|
-
require 'bunny'
|
2
|
-
|
3
1
|
namespace :rabbit_carrots do
|
4
|
-
desc '
|
2
|
+
desc 'Rake task for standalone RabbitCarrots mode'
|
5
3
|
task eat: :environment do
|
6
4
|
Rails.application.eager_load!
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
DatabaseAgonsticConnectionNotEstablished = defined?(ActiveRecord) ? ActiveRecord::ConnectionNotEstablished : Mongo::Error::SocketError
|
11
|
-
DatabaseAgnosticRecordInvalid = defined?(ActiveRecord) ? ActiveRecord::RecordInvalid : Mongoid::Errors::Validations
|
12
|
-
# rubocop:enable Lint/ConstantDefinitionInBlock
|
13
|
-
|
14
|
-
channels = RabbitCarrots.configuration.routing_key_mappings.map do |mapping|
|
15
|
-
# This will be supplied in initializer. At that time, the Handler will not be available to be loaded and will throw Uninitialized Constant
|
16
|
-
{ **mapping, handler: mapping[:handler].constantize }
|
17
|
-
end
|
18
|
-
|
19
|
-
Rails.logger = Logger.new(Rails.env.production? ? '/proc/self/fd/1' : $stdout)
|
20
|
-
|
21
|
-
# Run RMQ Subscriber for each channel
|
22
|
-
channels.each do |channel|
|
23
|
-
handler_class = channel[:handler]
|
24
|
-
|
25
|
-
raise "#{handler_class.name} must respond to `handle!`" unless handler_class.respond_to?(:handle!)
|
26
|
-
|
27
|
-
run_task(queue_name: channel[:queue], handler_class:, routing_keys: channel[:routing_keys], queue_arguments: channel[:arguments])
|
28
|
-
end
|
29
|
-
|
30
|
-
# Infinite loop to keep the process running
|
31
|
-
loop do
|
32
|
-
sleep 1
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def run_task(queue_name:, handler_class:, routing_keys:, queue_arguments: {})
|
38
|
-
RabbitCarrots::Connection.instance.channel.with do |channel|
|
39
|
-
exchange = channel.topic(RabbitCarrots.configuration.rabbitmq_exchange_name, durable: true)
|
40
|
-
|
41
|
-
Rails.logger.info "Listening on QUEUE: #{queue_name} for ROUTING KEYS: #{routing_keys}"
|
42
|
-
queue = channel.queue(queue_name, durable: true, arguments: queue_arguments)
|
43
|
-
|
44
|
-
routing_keys.map(&:strip).each { |k| queue.bind(exchange, routing_key: k) }
|
6
|
+
logger = Logger.new(Rails.env.production? ? '/proc/self/fd/1' : $stdout)
|
7
|
+
logger.level = Logger::INFO
|
45
8
|
|
46
|
-
|
47
|
-
Rails.logger.info "Received from queue: #{queue_name}, Routing Keys: #{routing_keys}"
|
48
|
-
handler_class.handle!(channel, delivery_info, properties, payload)
|
49
|
-
channel.ack(delivery_info.delivery_tag, false)
|
50
|
-
rescue RabbitCarrots::EventHandlers::Errors::NackMessage, JSON::ParserError => _e
|
51
|
-
Rails.logger.info "Nacked message: #{payload}"
|
52
|
-
channel.nack(delivery_info.delivery_tag, false, false)
|
53
|
-
rescue RabbitCarrots::EventHandlers::Errors::NackAndRequeueMessage => _e
|
54
|
-
Rails.logger.info "Nacked and Requeued message: #{payload}"
|
55
|
-
channel.nack(delivery_info.delivery_tag, false, true)
|
56
|
-
rescue DatabaseAgonsticNotNullViolation, DatabaseAgnosticRecordInvalid => e
|
57
|
-
# on null constraint violation, we want to ack the message
|
58
|
-
Rails.logger.error "Null constraint or Invalid violation: #{payload}. Error: #{e.message}"
|
59
|
-
channel.ack(delivery_info.delivery_tag, false)
|
60
|
-
rescue DatabaseAgonsticConnectionNotEstablished => e
|
61
|
-
# on connection not established, we want to requeue the message and sleep for 3 seconds
|
62
|
-
Rails.logger.error "Error connection not established to the database: #{payload}. Error: #{e.message}"
|
63
|
-
# delay for 3 seconds before requeuing
|
64
|
-
sleep 3
|
65
|
-
channel.nack(delivery_info.delivery_tag, false, true)
|
66
|
-
rescue StandardError => e
|
67
|
-
Rails.logger.error "Error handling message: #{payload}. Error: #{e.message}"
|
68
|
-
# requeue the message then kill the container
|
69
|
-
sleep 3
|
70
|
-
channel.nack(delivery_info.delivery_tag, false, true)
|
71
|
-
# kill the container with sigterm
|
72
|
-
Process.kill('SIGTERM', Process.pid)
|
73
|
-
end
|
9
|
+
core_service = RabbitCarrots::Core.new(logger:)
|
74
10
|
|
75
|
-
|
11
|
+
core_service.start(kill_to_restart_on_standard_error: true)
|
76
12
|
end
|
77
13
|
end
|
data/lib/rabbit_carrots.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rabbit_carrots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brusk Awat
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bunny
|
@@ -54,9 +54,11 @@ files:
|
|
54
54
|
- LICENSE.txt
|
55
55
|
- README.md
|
56
56
|
- Rakefile
|
57
|
+
- lib/puma/plugin/rabbit_carrots.rb
|
57
58
|
- lib/rabbit_carrots.rb
|
58
59
|
- lib/rabbit_carrots/configuration.rb
|
59
60
|
- lib/rabbit_carrots/connection.rb
|
61
|
+
- lib/rabbit_carrots/core.rb
|
60
62
|
- lib/rabbit_carrots/railtie.rb
|
61
63
|
- lib/rabbit_carrots/tasks/rmq.rake
|
62
64
|
- lib/rabbit_carrots/version.rb
|
@@ -85,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
87
|
- !ruby/object:Gem::Version
|
86
88
|
version: '0'
|
87
89
|
requirements: []
|
88
|
-
rubygems_version: 3.
|
90
|
+
rubygems_version: 3.5.9
|
89
91
|
signing_key:
|
90
92
|
specification_version: 4
|
91
93
|
summary: A simple RabbitMQ consumer task
|