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.
@@ -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