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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e3aa34b3f6e19e3490fcba113867feb0326fd10e8597b9323d8f384a36caa55
4
- data.tar.gz: 972c341f20e6bcd7d84c7cae441938f6f0f8fef2451bcb84f41f9e1d4b331e0b
3
+ metadata.gz: a55e06c9742638b1eb1a44d65d98031f5c366adddd3fcb196064075f0c32eb30
4
+ data.tar.gz: 499acd59987bb9a6d4548c444e0829117ecc16289c719461e0b5e3be255df02c
5
5
  SHA512:
6
- metadata.gz: 6b6df93150a9535d8acff68628c548958e9f22f23cb87d183460d38672d0a135e718e840df92fd70899e2deea5e8482a17d11b5ae6b6862510a8bec3cc853855
7
- data.tar.gz: 5100eab0c16d895dbc0c8e0dbf0217a49d2d37e965ab8a955c543eb657f87eab1b281d09c80908b641720b792a891cc1dd1d5db8b232144627410eea167e7bf0
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.20)
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.3)
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
- Then run ```bundle exec rake rabbit_carrots:eat```.
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, :rabbitmq_port, :rabbitmq_user, :rabbitmq_password, :rabbitmq_vhost, :routing_key_mappings, :rabbitmq_exchange_name
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 'Listener for Queue'
2
+ desc 'Rake task for standalone RabbitCarrots mode'
5
3
  task eat: :environment do
6
4
  Rails.application.eager_load!
7
5
 
8
- # rubocop:disable Lint/ConstantDefinitionInBlock
9
- DatabaseAgonsticNotNullViolation = defined?(ActiveRecord) ? ActiveRecord::NotNullViolation : RabbitCarrots::EventHandlers::Errors::PlaceholderError
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
- queue.subscribe(block: false, manual_ack: true, prefetch: 10) do |delivery_info, properties, payload|
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
- Rails.logger.info 'RUN TASK ENDED'
11
+ core_service.start(kill_to_restart_on_standard_error: true)
76
12
  end
77
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RabbitCarrots
4
- VERSION = '0.1.20'
4
+ VERSION = '1.0.1'
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'rabbit_carrots/version'
4
4
  require 'rabbit_carrots/connection'
5
+ require 'rabbit_carrots/core'
5
6
  require 'rabbit_carrots/configuration'
6
7
  require 'rabbit_carrots/railtie' if defined?(Rails)
7
8
 
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.20
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: 2023-12-14 00:00:00.000000000 Z
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.4.19
90
+ rubygems_version: 3.5.9
89
91
  signing_key:
90
92
  specification_version: 4
91
93
  summary: A simple RabbitMQ consumer task