rabbit_carrots 0.1.20 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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