rabbitmq-actors 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +101 -0
- data/.rspec +5 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/README.md +602 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rabbitmq/actors.rb +13 -0
- data/lib/rabbitmq/actors/base/agent.rb +108 -0
- data/lib/rabbitmq/actors/base/consumer.rb +105 -0
- data/lib/rabbitmq/actors/base/producer.rb +83 -0
- data/lib/rabbitmq/actors/patterns.rb +10 -0
- data/lib/rabbitmq/actors/patterns/headers/headers_consumer.rb +106 -0
- data/lib/rabbitmq/actors/patterns/headers/headers_producer.rb +64 -0
- data/lib/rabbitmq/actors/patterns/master_workers/master_producer.rb +61 -0
- data/lib/rabbitmq/actors/patterns/master_workers/worker.rb +63 -0
- data/lib/rabbitmq/actors/patterns/publish_subscribe/publisher.rb +72 -0
- data/lib/rabbitmq/actors/patterns/publish_subscribe/subscriber.rb +70 -0
- data/lib/rabbitmq/actors/patterns/routing/routing_consumer.rb +99 -0
- data/lib/rabbitmq/actors/patterns/routing/routing_producer.rb +75 -0
- data/lib/rabbitmq/actors/patterns/topics/topic_consumer.rb +105 -0
- data/lib/rabbitmq/actors/patterns/topics/topic_producer.rb +64 -0
- data/lib/rabbitmq/actors/testing.rb +8 -0
- data/lib/rabbitmq/actors/testing/rspec.rb +8 -0
- data/lib/rabbitmq/actors/testing/rspec/stub.rb +52 -0
- data/lib/rabbitmq/actors/version.rb +5 -0
- data/lib/rabbitmq/server.rb +18 -0
- data/rabbitmq-actors.gemspec +32 -0
- metadata +185 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rabbitmq/actors"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'bunny'
|
5
|
+
require_relative 'server'
|
6
|
+
require_relative 'actors/version'
|
7
|
+
require_relative 'actors/patterns'
|
8
|
+
require_relative 'actors/testing'
|
9
|
+
|
10
|
+
module RabbitMQ
|
11
|
+
module Actors
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RabbitMQ
|
2
|
+
module Actors
|
3
|
+
module Base
|
4
|
+
|
5
|
+
# The base class for RabbitMQ producers, workers, publishers, consumers...
|
6
|
+
# @abstract Subclass and override #pre_initialize and/or #post_initialize to define
|
7
|
+
# actual agent classes.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# module RabbitMQ
|
11
|
+
# module Actors
|
12
|
+
# module Base
|
13
|
+
#
|
14
|
+
# class Producer < Agent
|
15
|
+
# def post_initialize(queue_name:, opts = {})
|
16
|
+
# set_reply_queue(opts[:reply_queue_name])
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def publish(message, correlation_key: true)
|
20
|
+
# options = { routing_key: queue.name, durable: durable }
|
21
|
+
# options.merge!(reply_to: reply_queue.name) if reply_queue
|
22
|
+
# options.merge!(correlation_id: SecureRandom.hex(10)) if correlation_key
|
23
|
+
# exchange.publish(message, options)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def close!
|
27
|
+
# channel.close
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
class Agent
|
35
|
+
attr_reader :queue
|
36
|
+
|
37
|
+
# Instantiate a new agent.
|
38
|
+
# #pre_initialize and #post_initialize methods are called just at the beginning
|
39
|
+
# and end respectively.
|
40
|
+
# Redefine them in your subclass to complete your subclass initialization process.
|
41
|
+
#
|
42
|
+
# @option opts [String] :queue_name the queue name to bind the agent with if any.
|
43
|
+
# @option opts [Boolean] :exclusive (true) if the queue can only be used by this agent and
|
44
|
+
# removed when this agent's connection is closed.
|
45
|
+
# @option opts [Boolean] :auto_delete (true) if the queue will be deleted when
|
46
|
+
# there are no more consumers subscribed to it.
|
47
|
+
# @option opts [Logger] :logger the logger where to output info about agent's activity.
|
48
|
+
# Rest of options required by your subclasses.
|
49
|
+
def initialize(**opts)
|
50
|
+
pre_initialize **opts
|
51
|
+
set_queue(opts[:queue_name], **opts) if opts[:queue_name]
|
52
|
+
set_logger(opts[:logger])
|
53
|
+
post_initialize **opts
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# @!attribute [r] logger
|
59
|
+
# @return [Logger] the logger where to log agent's activity
|
60
|
+
attr_reader :logger
|
61
|
+
|
62
|
+
# The connection object to the RabbitMQ server.
|
63
|
+
# @return [Bunny::Session] the connection object.
|
64
|
+
def connection
|
65
|
+
@connection ||= Server.connection
|
66
|
+
end
|
67
|
+
|
68
|
+
# The channel where to send messages through
|
69
|
+
# @return [Bunny::Channel] the talking channel to RabbitMQ server.
|
70
|
+
def channel
|
71
|
+
@channel ||= connection.create_channel
|
72
|
+
end
|
73
|
+
|
74
|
+
# Method called just at the beginning of initializing an instance.
|
75
|
+
# Redefine it in your class to do the specific init you want.
|
76
|
+
# @param args [Array], list of params received in the initialize call.
|
77
|
+
def pre_initialize(*args)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Method called just at the end of initializing an instance.
|
81
|
+
# Redefine it in your class to do the specific init you want.
|
82
|
+
# @param args [Array], list of params received in the initialize call.
|
83
|
+
def post_initialize(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create/open a durable queue of the given name.
|
87
|
+
# @param name [String] name of the queue.
|
88
|
+
# @param opts [Hash] properties of the queue.
|
89
|
+
# @raise [Exception] if queue already set.
|
90
|
+
# @return [Bunny::Queue] the queue where to address/get messages.
|
91
|
+
def set_queue(name, **opts)
|
92
|
+
raise "Queue already set" if queue
|
93
|
+
auto_delete = opts.fetch(:auto_delete, true)
|
94
|
+
exclusive = opts.fetch(:exclusive, true)
|
95
|
+
@queue = channel.queue(name, durable: true, auto_delete: auto_delete, exclusive: exclusive)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set logger to output agent info.
|
99
|
+
# If logger is nil, STDOUT with a DEBUG level of severity is set up as logger.
|
100
|
+
# @param logger [Logger] where to address output info.
|
101
|
+
# @return [Logger] the logger instance where to output info.
|
102
|
+
def set_logger(logger)
|
103
|
+
@logger = logger || Logger.new(STDOUT)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require_relative 'agent'
|
2
|
+
|
3
|
+
module RabbitMQ
|
4
|
+
module Actors
|
5
|
+
module Base
|
6
|
+
|
7
|
+
# The base class to define actual RabbitMQ message consumer classes.
|
8
|
+
# @abstract Subclass and override #perform to define actual consumer classes.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# module RabbitMQ
|
12
|
+
# module Actors
|
13
|
+
# class Worker < Base::Consumer
|
14
|
+
# def initialize(queue_name:, **opts)
|
15
|
+
# super(opts.merge(queue_name: queue_name))
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class Consumer < Agent
|
22
|
+
# @param :queue_name [String] the queue name to bind the consumer with.
|
23
|
+
# @option opts [Boolean] :manual_ack to tell RabbitMQ server not to remove the message from
|
24
|
+
# re-delivery until a manual acknowledgment from the consumer has been received.
|
25
|
+
# @option opts [Proc] :on_cancellation to be executed if the agent is cancelled
|
26
|
+
# Rest of options required by your subclasses.
|
27
|
+
def initialize(queue_name: '', **opts)
|
28
|
+
super(opts.merge(queue_name: queue_name))
|
29
|
+
end
|
30
|
+
|
31
|
+
# Start listening to the queue and block waiting for a new task to be assigned by the server.
|
32
|
+
# Perform the task, acknowledge if required and keep listening waiting for the message to come.
|
33
|
+
# @raise [Exception] if something goes wrong during the execution of the task.
|
34
|
+
def start!
|
35
|
+
@_cancelled = false
|
36
|
+
channel.prefetch(1)
|
37
|
+
queue.subscribe(block: true, manual_ack: manual_ack?, on_cancellation: cancellation_handler) do |delivery_info, properties, body|
|
38
|
+
begin
|
39
|
+
logger.info(self.class.name) { "#{self} received task: #{body}" }
|
40
|
+
self.perform_result = perform(delivery_info: delivery_info, properties: properties, body: body)
|
41
|
+
done!(delivery_info)
|
42
|
+
logger.info(self.class.name) { "#{self} performed task!" }
|
43
|
+
rescue Exception => e
|
44
|
+
logger.error "Error when #{self} performing task: #{e.message}"
|
45
|
+
cancellation_handler.call
|
46
|
+
fail(e)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
cancellation_handler.call
|
50
|
+
perform_result
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @!attribute [rw] perform_result
|
56
|
+
# @return [Object] the result of execution of the last task received by the consumer
|
57
|
+
attr_accessor :perform_result
|
58
|
+
|
59
|
+
# @!attribute [r] manual_ack
|
60
|
+
# @return [Boolean] whether to acknowledge execution of messages to the server
|
61
|
+
attr_reader :manual_ack
|
62
|
+
alias_method :manual_ack?, :manual_ack
|
63
|
+
|
64
|
+
# @!attribute [r] on_cancellation
|
65
|
+
# @return [Proc] the proc to be executed before the consumer is finished.
|
66
|
+
attr_reader :on_cancellation
|
67
|
+
|
68
|
+
# Set manual_ack flag based on options received.
|
69
|
+
# Also set the code to execute before terminating the consumer.
|
70
|
+
# @see #initialize for the list of options that can be received.
|
71
|
+
def pre_initialize(**opts)
|
72
|
+
@manual_ack = opts[:manual_ack].present?
|
73
|
+
@on_cancellation = opts[:on_cancellation] || ->{}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Set the code to execute when the consumer is cancelled before exit.
|
77
|
+
# Execute the provided code and close the connection to RabbitMQ server.
|
78
|
+
# @return [Proc] handler to execute before exit.
|
79
|
+
def cancellation_handler
|
80
|
+
lambda do
|
81
|
+
if not @_cancelled
|
82
|
+
on_cancellation.call
|
83
|
+
connection.close
|
84
|
+
@_cancelled = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Perform the assigned task
|
90
|
+
# @param task [Hash] the properties of the message received.
|
91
|
+
# @raise [Exception] Override this method in your subclass.
|
92
|
+
def perform(**task)
|
93
|
+
raise "No work defined! for this task: #{task[:body]}. Define #perform private method in your class"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hook to be executed after the consumer completes an assigned task.
|
97
|
+
# Send acknowledgement to the channel if required
|
98
|
+
# @param delivery_info [Bunny Object] contains delivery info about the message to acknowledge.
|
99
|
+
def done!(delivery_info)
|
100
|
+
channel.ack(delivery_info.delivery_tag) if manual_ack?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_relative 'agent'
|
2
|
+
|
3
|
+
module RabbitMQ
|
4
|
+
module Actors
|
5
|
+
module Base
|
6
|
+
|
7
|
+
# The base class to define actual RabbitMQ message producer classes.
|
8
|
+
# @abstract Subclass and override #pre_initialize and #exchange to define actual producer classes.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# module RabbitMQ::Actors
|
12
|
+
# class MasterProducer < Base::Producer
|
13
|
+
# def initialize(queue_name:, **opts)
|
14
|
+
# super(opts.merge(queue_name: queue_name))
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def publish(message, message_id:, **opts)
|
18
|
+
# super(message, opts.merge(message_id: message_id, routing_key: queue.name))
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# private
|
22
|
+
#
|
23
|
+
# def exchange
|
24
|
+
# @exchange ||= channel.default_exchange
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
class Producer < Agent
|
30
|
+
# Send a messages to RabbitMQ server.
|
31
|
+
# Log the action to the logger with info severity.
|
32
|
+
# @param message [String] the message body to be sent.
|
33
|
+
# @param :message_id [String] user-defined id for replies to refer to this message using :correlation_id
|
34
|
+
# @param :opts [Hash] receives extra options from your subclass
|
35
|
+
# @see Bunny::Exchange#publish for extra options
|
36
|
+
def publish(message, message_id:, **opts)
|
37
|
+
options = opts.merge(message_id: message_id).reverse_merge!(persistent: true)
|
38
|
+
options.merge!(reply_to: reply_queue.name) if reply_queue
|
39
|
+
logger.info(self.class.name) { "Just Before #{self} publishes message: #{message} with options: #{options}" }
|
40
|
+
exchange.publish(message, options)
|
41
|
+
logger.info(self.class.name) { "Just After #{self} publishes message: #{message} with options: #{options}" }
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
delegate :on_return, to: :exchange
|
46
|
+
|
47
|
+
# Close the connection channel to RabbitMQ.
|
48
|
+
# Log the action to the logger with info severity.
|
49
|
+
def close
|
50
|
+
logger.info(self.class.name) { "Just Before #{self} closes RabbitMQ channel!" }
|
51
|
+
channel.close
|
52
|
+
logger.info(self.class.name) { "Just After #{self} closes RabbitMQ channel!" }
|
53
|
+
end
|
54
|
+
alias_method :and_close, :close
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :reply_queue
|
59
|
+
|
60
|
+
# If opts[:reply_queue_name].present? set the queue where a consumer should reply.
|
61
|
+
# @see #initialize for the list of options that can be received.
|
62
|
+
def post_initialize(**opts)
|
63
|
+
set_reply_queue(opts[:reply_queue_name]) if opts[:reply_queue_name].present?
|
64
|
+
end
|
65
|
+
|
66
|
+
# The RabbitMQ exchange where to publish the message
|
67
|
+
# @raise [Exception] so it must be override in subclasses.
|
68
|
+
def exchange
|
69
|
+
raise "This is an abstract class. Override exchange method in descendant class"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the RabbitMQ queue where a consumer should send replies.
|
73
|
+
# @param name [String] name of the queue.
|
74
|
+
# @raise [Exception] if reply_queue already set.
|
75
|
+
# @return [Bunny::Queue]
|
76
|
+
def set_reply_queue(name)
|
77
|
+
raise "Reply Queue already set" if reply_queue
|
78
|
+
@reply_queue = channel.queue(name, durable: true, auto_delete: true, exclusive: false)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative 'patterns/master_workers/master_producer'
|
2
|
+
require_relative 'patterns/master_workers/worker'
|
3
|
+
require_relative 'patterns/publish_subscribe/publisher'
|
4
|
+
require_relative 'patterns/publish_subscribe/subscriber'
|
5
|
+
require_relative 'patterns/routing/routing_producer'
|
6
|
+
require_relative 'patterns/routing/routing_consumer'
|
7
|
+
require_relative 'patterns/topics/topic_producer'
|
8
|
+
require_relative 'patterns/topics/topic_consumer'
|
9
|
+
require_relative 'patterns/headers/headers_producer'
|
10
|
+
require_relative 'patterns/headers/headers_consumer'
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require_relative '../../base/consumer'
|
2
|
+
|
3
|
+
module RabbitMQ
|
4
|
+
module Actors
|
5
|
+
# A consumer of messages from RabbitMQ based on exchange and message headers matching.
|
6
|
+
# @abstract Subclass and override #perform to define your customized headers worker class.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class NewYorkBranchListener < RabbitMQ::Actors::HeadersConsumer
|
10
|
+
# def initialize
|
11
|
+
# super(headers_name: 'reports',
|
12
|
+
# binding_headers: { 'type' => :econony, 'area' => 'Usa', 'x-match' => 'any' },
|
13
|
+
# logger: Rails.logger,
|
14
|
+
# on_cancellation: ->{ ActiveRecord::Base.connection.close })
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# private
|
18
|
+
#
|
19
|
+
# def perform(**task)
|
20
|
+
# report_data = JSON.parse(task[:body])
|
21
|
+
# process_report(report_data)
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def process_report(data)
|
25
|
+
# ...
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class LondonBranchListener < RabbitMQ::Actors::HeadersConsumer
|
30
|
+
# def initialize
|
31
|
+
# super(headers_name: 'reports',
|
32
|
+
# binding_headers: { 'type' => :industry, 'area' => 'Europe', 'x-match' =>'any' },
|
33
|
+
# logger: Rails.logger,
|
34
|
+
# on_cancellation: ->{ ActiveRecord::Base.connection.close })
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# private
|
38
|
+
#
|
39
|
+
# def perform(**task)
|
40
|
+
# report_data = JSON.parse(task[:body])
|
41
|
+
# process_report(report_data)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def process_report(data)
|
45
|
+
# ...
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# RabbitMQ::Server.url = 'amqp://localhost'
|
50
|
+
#
|
51
|
+
# NewYorkBranchListener.new.start!
|
52
|
+
# LondonBranchListener.new.start!
|
53
|
+
#
|
54
|
+
class HeadersConsumer < Base::Consumer
|
55
|
+
# @!attribute [r] headers_name
|
56
|
+
# @return [Bunny::Exchange] the headers exchange where to get messages from.
|
57
|
+
attr_reader :headers_name
|
58
|
+
|
59
|
+
# @!attribute [r] binding_headers
|
60
|
+
# @return [Hash] the headers this worker is interested in.
|
61
|
+
# The header 'x-match' MUST be included with value
|
62
|
+
# 'any' (match if any message header value matches) or
|
63
|
+
# 'all' (all message header values must match)
|
64
|
+
attr_reader :binding_headers
|
65
|
+
|
66
|
+
# @param :headers_name [String] name of the headers exchange this worker will receive messages.
|
67
|
+
# @param :binding_headers [Hash] headers this worker is interested in.
|
68
|
+
# Default to all: '#'
|
69
|
+
# @option opts [Proc] :on_cancellation to be executed before the worker is terminated
|
70
|
+
# @option opts [Logger] :logger the logger where to output info about this agent's activity.
|
71
|
+
# Rest of options required by your subclass.
|
72
|
+
def initialize(headers_name:, binding_headers:, **opts)
|
73
|
+
super(opts.merge(headers_name: headers_name, binding_headers: binding_headers))
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Set headers exchange_name and binding_headers this worker is bound to.
|
79
|
+
# @see #initialize for the list of options that can be received.
|
80
|
+
def pre_initialize(**opts)
|
81
|
+
@headers_name = opts[:headers_name]
|
82
|
+
@binding_headers = opts[:binding_headers]
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
# Bind this worker's queue to the headers exchange and to the given binding_key patterns
|
87
|
+
# @see #initialize for the list of options that can be received.
|
88
|
+
def post_initialize(**opts)
|
89
|
+
bind_queue_to_exchange_routing_keys
|
90
|
+
super
|
91
|
+
end
|
92
|
+
|
93
|
+
# The durable RabbitMQ headers exchange from where messages are received
|
94
|
+
# @return [Bunny::Exchange]
|
95
|
+
def exchange
|
96
|
+
@exchange ||= channel.headers(headers_name, durable: true)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Bind this worker's listening queue to the headers exchange and receive only messages with headers
|
100
|
+
# matching all/any of the ones in binding_headers.
|
101
|
+
def bind_queue_to_exchange_routing_keys
|
102
|
+
queue.bind(exchange, arguments: binding_headers)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|