maestro_common 0.0.2 → 0.0.3
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.
- data/lib/maestro_common/common.rb +2 -2
- data/lib/maestro_common/mq/messenger.rb +254 -0
- data/lib/maestro_common/version.rb +1 -1
- data/maestro_common.gemspec +1 -0
- data/spec/retryable_spec.rb +19 -0
- data/spec/spec_helper.rb +14 -0
- metadata +27 -12
@@ -1,2 +1,2 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), 'utils', 'retryable
|
2
|
-
require File.join(File.dirname(__FILE__), '
|
1
|
+
require File.join(File.dirname(__FILE__), 'utils', 'retryable')
|
2
|
+
require File.join(File.dirname(__FILE__), 'mq', 'messenger')
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# Copyright 2011(c) MaestroDev. All rights reserved.
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'helpers', 'mq_helper')
|
3
|
+
|
4
|
+
module Maestro
|
5
|
+
class MessageSendError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Messenger
|
9
|
+
VALID_QUEUE_REGEX = "^/(?:queue|topic)/[A-Za-z0-9_]*$"
|
10
|
+
|
11
|
+
DEFAULT_SEND_OPTIONS = { :persistent => true, :content_type => 'application/json' }
|
12
|
+
DEFAULT_SYNC_TIMEOUT = 5
|
13
|
+
DEFAULT_CUSTOM_TEXT = 'message'
|
14
|
+
|
15
|
+
attr_accessor :from_name, :debug
|
16
|
+
attr_reader :seq, :queues
|
17
|
+
|
18
|
+
class Message < Hash
|
19
|
+
def initialize(msg_type, content, reply_to = nil)
|
20
|
+
super()
|
21
|
+
self['__msg_type__'] = msg_type
|
22
|
+
self['__msg_content__'] = content
|
23
|
+
self['__msg_reply_to__'] = reply_to if reply_to
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Queue
|
28
|
+
attr_accessor :name, :default_handler, :handlers, :messenger, :connected, :opts, :use_eventmachine
|
29
|
+
|
30
|
+
# Minimum requirements - a name, and a handler for messages we don't know about
|
31
|
+
def initialize(name, default_handler = nil, opts = {})
|
32
|
+
raise ArgumentError, "#{name} cannot be nil" unless name
|
33
|
+
raise ArgumentError, "#{name} must match the following regex #{VALID_QUEUE_REGEX}, '#{name}' does not" unless name.match(/#{VALID_QUEUE_REGEX}/)
|
34
|
+
|
35
|
+
self.handlers = {}
|
36
|
+
self.connected = false
|
37
|
+
self.default_handler = default_handler
|
38
|
+
self.name = name
|
39
|
+
self.opts = opts
|
40
|
+
self.use_eventmachine = true
|
41
|
+
|
42
|
+
if default_handler
|
43
|
+
register_handler(default_handler)
|
44
|
+
else
|
45
|
+
Maestro.log.info "No default handler for queue '#{name}'. Unknown messages will be dropped"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def register_handler(handler, msg_types = nil)
|
50
|
+
msg_types = handler.get_handled_message_types unless msg_types
|
51
|
+
|
52
|
+
if msg_types && !msg_types.empty?
|
53
|
+
msg_types.each do |t|
|
54
|
+
handlers[t] = handler
|
55
|
+
Maestro.log.debug "Registering message type '#{t}' to handler '#{handler.class.name}' on queue #{name}"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
Maestro.log.debug "No message types registered for queue '#{name}' all messages will be #{default_handler ? 'sent to "on_unhandled_message"' : 'dropped'}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_message_async(message, options = DEFAULT_SEND_OPTIONS, &block)
|
63
|
+
messenger.send_message_async(name, message, options, block)
|
64
|
+
end
|
65
|
+
alias :send_message :send_message_async
|
66
|
+
|
67
|
+
def send_message_sync(message, timeout = DEFAULT_SYNC_TIMEOUT, options = DEFAULT_SEND_OPTIONS, &block)
|
68
|
+
messenger.send_message_sync(name, message, timeout, options, block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_incoming_message(message)
|
72
|
+
# All consumers expect message data to be json and parse... maybe just parse it here and pass parsed data
|
73
|
+
# then if we use different encoding noone will be any the wiser
|
74
|
+
begin
|
75
|
+
Maestro.log.debug "Received Message #{message.body}" if messenger.debug
|
76
|
+
|
77
|
+
# Only pass messages that have not expired
|
78
|
+
now = Time.now.to_i * 1000
|
79
|
+
expiration = message.headers['expires'].to_i
|
80
|
+
|
81
|
+
if (expiration > 0 && now > expiration)
|
82
|
+
timing = "Message expired at: #{expiration} [#{Time.at expiration/1000}], Current time: #{now} [#{Time.at now/1000}]"
|
83
|
+
Maestro.log.warn "Skipping agent queue expired message. #{timing}: #{message.body}"
|
84
|
+
# drop message
|
85
|
+
else
|
86
|
+
hash = JSON.parse(message.body)
|
87
|
+
msg_type = hash['__msg_type__'] || '-legacy-'
|
88
|
+
hash['__msg_from__'] = name
|
89
|
+
handler = handlers[msg_type]
|
90
|
+
|
91
|
+
if handler && handler.respond_to?(:on_incoming_message)
|
92
|
+
# Depending on arity, send either (self, type, content) or (self, type, content, raw)
|
93
|
+
case handler.method(:on_incoming_message).arity
|
94
|
+
when 3
|
95
|
+
handler.on_incoming_message(self, msg_type, hash['__msg_content__'])
|
96
|
+
when 4
|
97
|
+
handler.on_incoming_message(self, msg_type, hash['__msg_content__'], hash)
|
98
|
+
end
|
99
|
+
else
|
100
|
+
if default_handler && default_handler.respond_to?(:on_unhandled_message)
|
101
|
+
default_handler.on_unhandled_message(self, msg_type, hash['__msg_content__'], hash)
|
102
|
+
else
|
103
|
+
Maestro.log.warn "Dumping unhandled '#{msg_type}' message (no default handler): #{hash}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Maestro.log.debug("Processed #{msg_type}: #{message.body}") if messenger.debug
|
108
|
+
end
|
109
|
+
rescue Exception => e
|
110
|
+
# something happened executing processing the message/running the plugin
|
111
|
+
backtrace = e.backtrace.join("\n")
|
112
|
+
# set the error with the exception message and backtrace
|
113
|
+
Maestro.log.error "Error processing message - #{e.class}:#{e}\n#{backtrace}"
|
114
|
+
ensure
|
115
|
+
if opts && opts[:ack] == 'client'
|
116
|
+
Maestro::MQHelper.connection.ack(message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Override attr setter so we can resubscribe, etc
|
122
|
+
def name=(name)
|
123
|
+
if !@name || @name != name
|
124
|
+
disconnect
|
125
|
+
@name = name
|
126
|
+
connect
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def messenger=(messenger)
|
131
|
+
disconnect unless messenger
|
132
|
+
@messenger = messenger
|
133
|
+
connect
|
134
|
+
end
|
135
|
+
|
136
|
+
def connect
|
137
|
+
if messenger
|
138
|
+
# If eventmachine enabled will use it to defer delivery so we can receive the message and continue with life
|
139
|
+
# That can be an issue sometimes as different threads may execute at different rates, causing unintentional
|
140
|
+
# bursts of chronometric radiation that can cause messages to appear to execute in non-sequential order.
|
141
|
+
# (Actually they are, but since multiple may be executing in parallel it can have unintended consequences)
|
142
|
+
# Set the 'use_eventmachine' property of the queue connection to 'false' to ensure message processing completes
|
143
|
+
# in the order of reception. This will effectively block incoming messages, so the consequences of that should
|
144
|
+
# be taken into account.
|
145
|
+
Maestro::MQHelper.subscribe(@name, opts) { |message| use_eventmachine ? EventMachine.defer { handle_incoming_message(message) } : handle_incoming_message(message) } unless connected?
|
146
|
+
self.connected = true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def disconnect
|
151
|
+
Maestro::MQHelper.unsubscribe(@name) if @name && connected?
|
152
|
+
self.connected = false
|
153
|
+
end
|
154
|
+
|
155
|
+
def connected?
|
156
|
+
return connected
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
def initialize
|
162
|
+
@lock = Monitor.new
|
163
|
+
@seq = 0
|
164
|
+
@queues = {}
|
165
|
+
@from_name =
|
166
|
+
Maestro::MQHelper.connect
|
167
|
+
end
|
168
|
+
|
169
|
+
def connected?
|
170
|
+
return MQHelper.connection && MQHelper.connection.connected?
|
171
|
+
end
|
172
|
+
|
173
|
+
def register_queue(queue)
|
174
|
+
queues[queue.name] = queue
|
175
|
+
queue.messenger = self
|
176
|
+
|
177
|
+
return queue
|
178
|
+
end
|
179
|
+
|
180
|
+
def deregister_queue(queue)
|
181
|
+
queue = queues[queue] if queue.is_a?(String)
|
182
|
+
|
183
|
+
if queue
|
184
|
+
queues.delete(queue.name)
|
185
|
+
queue.destroy
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def stop
|
190
|
+
queues.each { |q| deregister_queue(queue) }
|
191
|
+
MQHelper.disconnect
|
192
|
+
end
|
193
|
+
|
194
|
+
def reconnect
|
195
|
+
MQHelper.reconnect unless connected?
|
196
|
+
end
|
197
|
+
|
198
|
+
def disconnect
|
199
|
+
MQHelper.disconnect if connected?
|
200
|
+
end
|
201
|
+
|
202
|
+
def send_message_async(destination_queue, message, options = DEFAULT_SEND_OPTIONS, &block)
|
203
|
+
my_seq = seq
|
204
|
+
|
205
|
+
message['__msg_seq__'] = my_seq
|
206
|
+
|
207
|
+
custom_text = message.has_key?('__msg_type__') ? message['__msg_type__'] : DEFAULT_CUSTOM_TEXT
|
208
|
+
|
209
|
+
Maestro.log.debug "[seq #{my_seq}] Sending #{custom_text}" if debug
|
210
|
+
|
211
|
+
Maestro::MQHelper.connection.send( destination_queue, message.to_json, options ) do |r|
|
212
|
+
Maestro.log.debug "[seq #{my_seq}] Sent #{custom_text} to '#{destination_queue}'" if debug
|
213
|
+
|
214
|
+
yield(r) if block
|
215
|
+
end
|
216
|
+
end
|
217
|
+
alias :send_message :send_message_async
|
218
|
+
|
219
|
+
def send_message_sync(destination_queue, message, timeout = DEFAULT_SYNC_TIMEOUT, options = DEFAULT_SEND_OPTIONS, &block)
|
220
|
+
ackd = false
|
221
|
+
rr = nil
|
222
|
+
|
223
|
+
send_message_async(destination_queue, message, options) do |r|
|
224
|
+
rr = r
|
225
|
+
ackd = true
|
226
|
+
end
|
227
|
+
|
228
|
+
begin
|
229
|
+
Timeout::timeout(timeout) do
|
230
|
+
while !ackd
|
231
|
+
sleep 0.25
|
232
|
+
end
|
233
|
+
|
234
|
+
ackd = true
|
235
|
+
|
236
|
+
yield(rr) if block
|
237
|
+
end
|
238
|
+
rescue Timeout::Error
|
239
|
+
raise MessageSendError, "Message ##{message['__msg_seq__']} not ack'd by MQ"
|
240
|
+
end
|
241
|
+
|
242
|
+
return ackd
|
243
|
+
end
|
244
|
+
|
245
|
+
def seq
|
246
|
+
@lock.synchronize do
|
247
|
+
@seq += 1
|
248
|
+
|
249
|
+
return @seq
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
data/maestro_common.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency 'logging', '>= 1.8.0'
|
23
23
|
spec.add_dependency 'rubyzip', '>= 0.9.8'
|
24
24
|
spec.add_dependency 'json', '>= 1.4.6'
|
25
|
+
spec.add_dependency 'eventmachine', ">=0.12.10"
|
25
26
|
|
26
27
|
spec.add_development_dependency 'bundler', '>= 1.3'
|
27
28
|
spec.add_development_dependency 'rake'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Maestro::Utils do
|
4
|
+
|
5
|
+
describe '#retryable' do
|
6
|
+
|
7
|
+
it 'should retry the correct number of times' do
|
8
|
+
# Can't do this until the logging is sorted
|
9
|
+
# attempts = 0
|
10
|
+
# Maestro::Utils.retryable({:tries => 5, :sleep => 1}) do
|
11
|
+
# attempts += 1
|
12
|
+
# raise 'A ruckus'
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# attempts.should == 5
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') unless $LOAD_PATH.include?(File.dirname(__FILE__) + '/../lib')
|
6
|
+
|
7
|
+
require File.join('maestro_common', 'common')
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: maestro_common
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.3
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Doug Henderson
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-08-19 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: onstomp
|
@@ -57,29 +57,29 @@ dependencies:
|
|
57
57
|
prerelease: false
|
58
58
|
type: :runtime
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
|
-
name:
|
60
|
+
name: eventmachine
|
61
61
|
version_requirements: &id005 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ">="
|
65
65
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
66
|
+
version: 0.12.10
|
67
67
|
requirement: *id005
|
68
68
|
prerelease: false
|
69
|
-
type: :
|
69
|
+
type: :runtime
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
|
-
name:
|
71
|
+
name: bundler
|
72
72
|
version_requirements: &id006 !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
75
|
- - ">="
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version: "
|
77
|
+
version: "1.3"
|
78
78
|
requirement: *id006
|
79
79
|
prerelease: false
|
80
80
|
type: :development
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
|
-
name:
|
82
|
+
name: rake
|
83
83
|
version_requirements: &id007 !ruby/object:Gem::Requirement
|
84
84
|
none: false
|
85
85
|
requirements:
|
@@ -90,16 +90,27 @@ dependencies:
|
|
90
90
|
prerelease: false
|
91
91
|
type: :development
|
92
92
|
- !ruby/object:Gem::Dependency
|
93
|
-
name:
|
93
|
+
name: jruby-openssl
|
94
94
|
version_requirements: &id008 !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
96
96
|
requirements:
|
97
97
|
- - ">="
|
98
98
|
- !ruby/object:Gem::Version
|
99
|
-
version:
|
99
|
+
version: "0"
|
100
100
|
requirement: *id008
|
101
101
|
prerelease: false
|
102
102
|
type: :development
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec
|
105
|
+
version_requirements: &id009 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.13.0
|
111
|
+
requirement: *id009
|
112
|
+
prerelease: false
|
113
|
+
type: :development
|
103
114
|
description: A bunch of utility classes that are used in multiple places
|
104
115
|
email:
|
105
116
|
- dhenderson@maestrodev.com
|
@@ -118,9 +129,12 @@ files:
|
|
118
129
|
- lib/maestro_common/common.rb
|
119
130
|
- lib/maestro_common/helpers/mq_helper.rb
|
120
131
|
- lib/maestro_common/logging/maestro_logging.rb
|
132
|
+
- lib/maestro_common/mq/messenger.rb
|
121
133
|
- lib/maestro_common/utils/retryable.rb
|
122
134
|
- lib/maestro_common/version.rb
|
123
135
|
- maestro_common.gemspec
|
136
|
+
- spec/retryable_spec.rb
|
137
|
+
- spec/spec_helper.rb
|
124
138
|
homepage: https://github.com/maestrodev/maestro-ruby-common
|
125
139
|
licenses:
|
126
140
|
- Apache 2.0
|
@@ -154,5 +168,6 @@ rubygems_version: 1.8.24
|
|
154
168
|
signing_key:
|
155
169
|
specification_version: 3
|
156
170
|
summary: Maestro common classes
|
157
|
-
test_files:
|
158
|
-
|
171
|
+
test_files:
|
172
|
+
- spec/retryable_spec.rb
|
173
|
+
- spec/spec_helper.rb
|