manageiq-messaging 0.1.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/.codeclimate.yml +47 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +5 -0
- data/.rubocop_local.yml +2 -0
- data/.travis.yml +10 -0
- data/CHANGES +2 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +171 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/README.md +16 -0
- data/examples/background_job.rb +36 -0
- data/examples/common.rb +40 -0
- data/examples/message.rb +42 -0
- data/lib/manageiq-messaging.rb +1 -0
- data/lib/manageiq/messaging.rb +24 -0
- data/lib/manageiq/messaging/client.rb +205 -0
- data/lib/manageiq/messaging/common.rb +36 -0
- data/lib/manageiq/messaging/kafka.rb +7 -0
- data/lib/manageiq/messaging/kafka/background_job.rb +13 -0
- data/lib/manageiq/messaging/kafka/client.rb +91 -0
- data/lib/manageiq/messaging/kafka/common.rb +105 -0
- data/lib/manageiq/messaging/kafka/queue.rb +41 -0
- data/lib/manageiq/messaging/kafka/topic.rb +28 -0
- data/lib/manageiq/messaging/null_logger.rb +11 -0
- data/lib/manageiq/messaging/received_message.rb +11 -0
- data/lib/manageiq/messaging/stomp.rb +7 -0
- data/lib/manageiq/messaging/stomp/background_job.rb +61 -0
- data/lib/manageiq/messaging/stomp/client.rb +85 -0
- data/lib/manageiq/messaging/stomp/common.rb +84 -0
- data/lib/manageiq/messaging/stomp/queue.rb +53 -0
- data/lib/manageiq/messaging/stomp/topic.rb +37 -0
- data/lib/manageiq/messaging/version.rb +5 -0
- data/manageiq-messaging.gemspec +33 -0
- metadata +210 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Kafka
|
4
|
+
module Common
|
5
|
+
require 'manageiq/messaging/common'
|
6
|
+
include ManageIQ::Messaging::Common
|
7
|
+
|
8
|
+
GROUP_FOR_QUEUE_MESSAGES = 'manageiq_messaging_queue_group'.freeze
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def producer
|
13
|
+
@producer ||= kafka_client.producer
|
14
|
+
end
|
15
|
+
|
16
|
+
def topic_consumer(persist_ref)
|
17
|
+
# persist_ref enables consumer to receive messages sent when consumer is temporarily offline
|
18
|
+
# it also enables consumers to do load balancing when multiple consumers join the with the same ref.
|
19
|
+
@consumer.try(:stop) unless @persist_ref == persist_ref
|
20
|
+
@persist_ref = persist_ref
|
21
|
+
@topic_consumer ||= kafka_client.consumer(:group_id => persist_ref)
|
22
|
+
end
|
23
|
+
|
24
|
+
def queue_consumer
|
25
|
+
# all queue consumers join the same group so that each message can be processed by one and only one consumer
|
26
|
+
@queue_consumer ||= kafka_client.consumer(:group_id => GROUP_FOR_QUEUE_MESSAGES)
|
27
|
+
end
|
28
|
+
|
29
|
+
trap("TERM") do
|
30
|
+
@consumer.try(:stop)
|
31
|
+
@consumer = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def raw_publish(commit, body, options)
|
35
|
+
producer.produce(encode_body(options[:headers], body), options)
|
36
|
+
producer.deliver_messages if commit
|
37
|
+
logger.info("Published to topic(#{options[:topic]}), msg(#{payload_log(body.inspect)})")
|
38
|
+
end
|
39
|
+
|
40
|
+
def queue_for_publish(options)
|
41
|
+
body, kafka_opts = for_publish(options)
|
42
|
+
kafka_opts[:headers][:message_type] = options[:message] if options[:message]
|
43
|
+
kafka_opts[:headers][:class_name] = options[:class_name] if options[:class_name]
|
44
|
+
|
45
|
+
[body, kafka_opts]
|
46
|
+
end
|
47
|
+
|
48
|
+
def topic_for_publish(options)
|
49
|
+
body, kafka_opts = for_publish(options)
|
50
|
+
kafka_opts[:headers][:event_type] = options[:event] if options[:event]
|
51
|
+
|
52
|
+
[body, kafka_opts]
|
53
|
+
end
|
54
|
+
|
55
|
+
def for_publish(options)
|
56
|
+
kafka_opts = {:topic => address(options), :headers => {}}
|
57
|
+
kafka_opts[:partition_key] = options[:group_name] if options[:group_name]
|
58
|
+
kafka_opts[:headers][:sender] = options[:sender] if options[:sender]
|
59
|
+
|
60
|
+
body = options[:payload] || ''
|
61
|
+
|
62
|
+
[body, kafka_opts]
|
63
|
+
end
|
64
|
+
|
65
|
+
def address(options)
|
66
|
+
if options[:affinity]
|
67
|
+
"#{options[:service]}.#{options[:affinity]}"
|
68
|
+
else
|
69
|
+
options[:service]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_queue_message(queue, message)
|
74
|
+
payload = decode_body(message.headers, message.value)
|
75
|
+
sender, message_type, class_name = parse_message_headers(message.headers)
|
76
|
+
logger.info("Message received: queue(#{queue}), message(#{payload_log(payload)}), sender(#{sender}), type(#{message_type})")
|
77
|
+
[sender, message_type, class_name, payload]
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_topic_message(topic, message)
|
81
|
+
begin
|
82
|
+
payload = decode_body(message.headers, message.value)
|
83
|
+
sender, event_type = parse_event_headers(message.headers)
|
84
|
+
logger.info("Event received: topic(#{topic}), event(#{payload_log(payload)}), sender(#{sender}), type(#{event_type})")
|
85
|
+
yield sender, event_type, payload
|
86
|
+
logger.info("Event processed")
|
87
|
+
rescue StandardError => e
|
88
|
+
logger.error("Event processing error: #{e.message}")
|
89
|
+
logger.error(e.backtrace.join("\n"))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_message_headers(headers)
|
94
|
+
return [nil, nil, nil] unless headers.kind_of?(Hash)
|
95
|
+
headers.values_at('sender', 'message_type', 'class_name')
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_event_headers(headers)
|
99
|
+
return [nil, nil] unless headers.kind_of?(Hash)
|
100
|
+
headers.values_at('sender', 'event_type')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Kafka
|
4
|
+
module Queue
|
5
|
+
private
|
6
|
+
|
7
|
+
def publish_message_impl(options)
|
8
|
+
raise ArgumentError, "Kafka messaging implementation does not take a block" if block_given?
|
9
|
+
raw_publish(true, *queue_for_publish(options))
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish_messages_impl(messages)
|
13
|
+
messages.each { |msg_options| raw_publish(false, *queue_for_publish(msg_options)) }
|
14
|
+
producer.deliver_messages
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe_messages_impl(options)
|
18
|
+
topic = address(options)
|
19
|
+
|
20
|
+
consumer = queue_consumer
|
21
|
+
consumer.subscribe(topic)
|
22
|
+
consumer.each_batch do |batch|
|
23
|
+
logger.info("Batch message received: queue(#{topic})")
|
24
|
+
begin
|
25
|
+
messages = batch.messages.collect do |message|
|
26
|
+
sender, message_type, _class_name, payload = process_queue_message(topic, message)
|
27
|
+
ManageIQ::Messaging::ReceivedMessage.new(sender, message_type, payload, nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
yield messages
|
31
|
+
rescue StandardError => e
|
32
|
+
logger.error("Event processing error: #{e.message}")
|
33
|
+
logger.error(e.backtrace.join("\n"))
|
34
|
+
end
|
35
|
+
logger.info("Batch message processed")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Kafka
|
4
|
+
module Topic
|
5
|
+
private
|
6
|
+
|
7
|
+
def publish_topic_impl(options)
|
8
|
+
raw_publish(true, *topic_for_publish(options))
|
9
|
+
end
|
10
|
+
|
11
|
+
def subscribe_topic_impl(options, &block)
|
12
|
+
topic = address(options)
|
13
|
+
persist_ref = options[:persist_ref]
|
14
|
+
|
15
|
+
if persist_ref
|
16
|
+
consumer = topic_consumer(persist_ref)
|
17
|
+
consumer.subscribe(topic, :start_from_beginning => false)
|
18
|
+
consumer.each_message { |message| process_topic_message(topic, message, &block) }
|
19
|
+
else
|
20
|
+
kafka_client.each_message(:topic => topic, :start_from_beginning => false) do |message|
|
21
|
+
process_topic_message(topic, message, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
class ReceivedMessage
|
4
|
+
attr_accessor :sender, :message, :payload, :ack_ref
|
5
|
+
|
6
|
+
def initialize(sender, message, payload, ack_ref)
|
7
|
+
@sender, @message, @payload, @ack_ref = sender, message, payload, ack_ref
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Stomp
|
4
|
+
module BackgroundJob
|
5
|
+
private
|
6
|
+
|
7
|
+
def subscribe_background_job_impl(options)
|
8
|
+
queue_name, headers = queue_for_subscribe(options)
|
9
|
+
|
10
|
+
subscribe(queue_name, headers) do |msg|
|
11
|
+
begin
|
12
|
+
ack(msg)
|
13
|
+
assert_options(msg.headers, ['class_name', 'message_type'])
|
14
|
+
|
15
|
+
msg_options = decode_body(msg.headers, msg.body)
|
16
|
+
msg_options = {} if msg_options.empty?
|
17
|
+
logger.info("Processing background job: queue(#{queue_name}), job(#{msg_options.inspect}), headers(#{msg.headers})")
|
18
|
+
result = run_job(msg_options.merge(:class_name => msg.headers['class_name'], :method_name => msg.headers['message_type']))
|
19
|
+
logger.info("Background job completed")
|
20
|
+
|
21
|
+
correlation_ref = msg.headers['correlation_id']
|
22
|
+
send_response(options[:service], correlation_ref, result) if correlation_ref
|
23
|
+
rescue Timeout::Error
|
24
|
+
logger.warn("Background job timed out")
|
25
|
+
if Object.const_defined?('ActiveRecord::Base')
|
26
|
+
begin
|
27
|
+
logger.info("Reconnecting to DB after timeout error during queue deliver")
|
28
|
+
ActiveRecord::Base.connection.reconnect!
|
29
|
+
rescue => err
|
30
|
+
logger.error("Error encountered during <ActiveRecord::Base.connection.reconnect!> error:#{err.class.name}: #{err.message}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
logger.error("Background job error: #{e.message}")
|
35
|
+
logger.error(e.backtrace.join("\n"))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_job(options)
|
41
|
+
assert_options(options, [:class_name, :method_name])
|
42
|
+
|
43
|
+
instance_id = options[:instance_id]
|
44
|
+
args = options[:args]
|
45
|
+
miq_callback = options[:miq_callback]
|
46
|
+
|
47
|
+
obj = Object.const_get(options[:class_name])
|
48
|
+
obj = obj.find(instance_id) if instance_id
|
49
|
+
|
50
|
+
msg_timeout = 600 # TODO: configurable per message
|
51
|
+
result = Timeout.timeout(msg_timeout) do
|
52
|
+
obj.send(options[:method_name], *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
run_job(miq_callback) if miq_callback
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Stomp
|
4
|
+
# Messaging client implementation using Stomp protocol with ActiveMQ Artemis being
|
5
|
+
# the underlying supporting system.
|
6
|
+
# Do not directly instantiate an instance from this class. Use
|
7
|
+
# +ManageIQ::Messaging::Client.open+ method.
|
8
|
+
#
|
9
|
+
# Artemis specific connection options accepted by +open+ method:
|
10
|
+
# * :client_ref (A reference string to identify the client)
|
11
|
+
# * :host (Single host name)
|
12
|
+
# * :port (host port number)
|
13
|
+
# * :username
|
14
|
+
# * :password
|
15
|
+
# * :heartbeat (Whether the client should do heart-beating. Default to true)
|
16
|
+
#
|
17
|
+
# Artemis specific +publish_message+ options:
|
18
|
+
# * :expires_on
|
19
|
+
# * :deliver_on
|
20
|
+
# * :priority
|
21
|
+
# * :group_name
|
22
|
+
#
|
23
|
+
# Artemis specific +publish_topic+ options:
|
24
|
+
# * :expires_on
|
25
|
+
# * :deliver_on
|
26
|
+
# * :priority
|
27
|
+
#
|
28
|
+
# Artemis specific +subscribe_topic+ options:
|
29
|
+
# * :persist_ref
|
30
|
+
#
|
31
|
+
# +:persist_ref+ must be paired with +:client_ref+ option in +Client.open+ method.
|
32
|
+
# They jointly create a unique group name. Without such group every topic subscriber
|
33
|
+
# receives a copy of each message only when they are active. This is the default.
|
34
|
+
# If multiple topic subscribers join with the same group each message is consumed
|
35
|
+
# by only one of the subscribers. This allows a load balancing among the subscribers.
|
36
|
+
# Also any messages sent when all members of the group are offline will be persisted
|
37
|
+
# and delivered when any member in the group is back online. Each message is still
|
38
|
+
# copied and delivered to other subscribes belongs to other groups or no group.
|
39
|
+
#
|
40
|
+
# Artemis specific +subscribe_messages+ options:
|
41
|
+
# * :limit ()
|
42
|
+
class Client < ManageIQ::Messaging::Client
|
43
|
+
require 'stomp'
|
44
|
+
require 'manageiq/messaging/stomp/common'
|
45
|
+
require 'manageiq/messaging/stomp/queue'
|
46
|
+
require 'manageiq/messaging/stomp/background_job'
|
47
|
+
require 'manageiq/messaging/stomp/topic'
|
48
|
+
|
49
|
+
include Common
|
50
|
+
include Queue
|
51
|
+
include BackgroundJob
|
52
|
+
include Topic
|
53
|
+
|
54
|
+
private *delegate(:subscribe, :unsubscribe, :publish, :to => :stomp_client)
|
55
|
+
delegate :ack, :close, :to => :stomp_client
|
56
|
+
|
57
|
+
attr_accessor :encoding
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :stomp_client
|
62
|
+
|
63
|
+
def initialize(options)
|
64
|
+
host = options.slice(:host, :port)
|
65
|
+
host[:passcode] = options[:password] if options[:password]
|
66
|
+
host[:login] = options[:username] if options[:username]
|
67
|
+
|
68
|
+
headers = {}
|
69
|
+
if options[:heartbeat].nil? || options[:heartbeat]
|
70
|
+
headers.merge!(
|
71
|
+
:host => options[:host],
|
72
|
+
:"accept-version" => "1.2",
|
73
|
+
:"heart-beat" => "2000,0"
|
74
|
+
)
|
75
|
+
end
|
76
|
+
headers[:"client-id"] = options[:client_ref] if options[:client_ref]
|
77
|
+
|
78
|
+
@encoding = options[:encoding] || 'yaml'
|
79
|
+
require "json" if @encoding == "json"
|
80
|
+
@stomp_client = ::Stomp::Client.new(:hosts => [host], :connect_headers => headers)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Stomp
|
4
|
+
module Common
|
5
|
+
require 'manageiq/messaging/common'
|
6
|
+
include ManageIQ::Messaging::Common
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def raw_publish(address, body, headers)
|
11
|
+
publish(address, encode_body(headers, body), headers)
|
12
|
+
logger.info("Published to address(#{address}), msg(#{payload_log(body.inspect)}), headers(#{headers.inspect})")
|
13
|
+
end
|
14
|
+
|
15
|
+
def queue_for_publish(options)
|
16
|
+
affinity = options[:affinity] || 'none'
|
17
|
+
address = "queue/#{options[:service]}.#{affinity}"
|
18
|
+
|
19
|
+
headers = {:"destination-type" => 'ANYCAST', :persistent => true}
|
20
|
+
headers[:expires] = options[:expires_on].to_i * 1000 if options[:expires_on]
|
21
|
+
headers[:AMQ_SCHEDULED_TIME] = options[:deliver_on].to_i * 1000 if options[:deliver_on]
|
22
|
+
headers[:priority] = options[:priority] if options[:priority]
|
23
|
+
headers[:_AMQ_GROUP_ID] = options[:group_name] if options[:group_name]
|
24
|
+
|
25
|
+
[address, headers]
|
26
|
+
end
|
27
|
+
|
28
|
+
def queue_for_subscribe(options)
|
29
|
+
affinity = options[:affinity] || 'none'
|
30
|
+
queue_name = "queue/#{options[:service]}.#{affinity}"
|
31
|
+
|
32
|
+
headers = {:"subscription-type" => 'ANYCAST', :ack => 'client'}
|
33
|
+
|
34
|
+
[queue_name, headers]
|
35
|
+
end
|
36
|
+
|
37
|
+
def topic_for_publish(options)
|
38
|
+
address = "topic/#{options[:service]}"
|
39
|
+
|
40
|
+
headers = {:"destination-type" => 'MULTICAST', :persistent => true}
|
41
|
+
headers[:expires] = options[:expires_on].to_i * 1000 if options[:expires_on]
|
42
|
+
headers[:AMQ_SCHEDULED_TIME] = options[:deliver_on].to_i * 1000 if options[:deliver_on]
|
43
|
+
headers[:priority] = options[:priority] if options[:priority]
|
44
|
+
|
45
|
+
[address, headers]
|
46
|
+
end
|
47
|
+
|
48
|
+
def topic_for_subscribe(options)
|
49
|
+
queue_name = "topic/#{options[:service]}"
|
50
|
+
|
51
|
+
headers = {:"subscription-type" => 'MULTICAST', :ack => 'client'}
|
52
|
+
headers[:"durable-subscription-name"] = options[:persist_ref] if options[:persist_ref]
|
53
|
+
|
54
|
+
[queue_name, headers]
|
55
|
+
end
|
56
|
+
|
57
|
+
def send_response(service, correlation_ref, result)
|
58
|
+
response_options = {
|
59
|
+
:service => "#{service}.response",
|
60
|
+
:affinity => correlation_ref
|
61
|
+
}
|
62
|
+
address, response_headers = queue_for_publish(response_options)
|
63
|
+
raw_publish(address, result || '', response_headers.merge(:correlation_id => correlation_ref))
|
64
|
+
end
|
65
|
+
|
66
|
+
def receive_response(service, correlation_ref)
|
67
|
+
response_options = {
|
68
|
+
:service => "#{service}.response",
|
69
|
+
:affinity => correlation_ref
|
70
|
+
}
|
71
|
+
queue_name, response_headers = queue_for_subscribe(response_options)
|
72
|
+
subscribe(queue_name, response_headers) do |msg|
|
73
|
+
ack(msg)
|
74
|
+
begin
|
75
|
+
yield decode_body(msg.headers, msg.body)
|
76
|
+
ensure
|
77
|
+
unsubscribe(queue_name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module Messaging
|
3
|
+
module Stomp
|
4
|
+
module Queue
|
5
|
+
private
|
6
|
+
|
7
|
+
def publish_message_impl(options, &block)
|
8
|
+
address, headers = queue_for_publish(options)
|
9
|
+
headers[:sender] = options[:sender] if options[:sender]
|
10
|
+
headers[:message_type] = options[:message] if options[:message]
|
11
|
+
headers[:class_name] = options[:class_name] if options[:class_name]
|
12
|
+
headers[:correlation_id] = Time.now.to_i.to_s if block_given?
|
13
|
+
|
14
|
+
raw_publish(address, options[:payload] || '', headers)
|
15
|
+
|
16
|
+
return unless block_given?
|
17
|
+
|
18
|
+
receive_response(options[:service], headers[:correlation_id], &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def publish_messages_impl(messages)
|
22
|
+
messages.each { |msg_options| publish_message(msg_options) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe_messages_impl(options)
|
26
|
+
queue_name, headers = queue_for_subscribe(options)
|
27
|
+
|
28
|
+
# for STOMP we can get message one at a time
|
29
|
+
subscribe(queue_name, headers) do |msg|
|
30
|
+
begin
|
31
|
+
sender = msg.headers['sender']
|
32
|
+
message_type = msg.headers['message_type']
|
33
|
+
message_body = decode_body(msg.headers, msg.body)
|
34
|
+
logger.info("Message received: queue(#{queue_name}), msg(#{payload_log(message_body)}), headers(#{msg.headers})")
|
35
|
+
|
36
|
+
result = yield [ManageIQ::Messaging::ReceivedMessage.new(sender, message_type, message_body, msg)]
|
37
|
+
logger.info("Message processed")
|
38
|
+
|
39
|
+
correlation_ref = msg.headers['correlation_id']
|
40
|
+
if correlation_ref
|
41
|
+
result = result.first if result.kind_of?(Array)
|
42
|
+
send_response(options[:service], correlation_ref, result)
|
43
|
+
end
|
44
|
+
rescue => e
|
45
|
+
logger.error("Message processing error: #{e.message}")
|
46
|
+
logger.error(e.backtrace.join("\n"))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|