manageiq-messaging 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|