rabbitmq-actors 2.0.0
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/.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
|