rabbitmq-actors 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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