maestro_common 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,2 +1,2 @@
1
- require File.join(File.dirname(__FILE__), 'utils', 'retryable.rb')
2
- require File.join(File.dirname(__FILE__), 'helpers', 'mq_helper.rb')
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
@@ -1,5 +1,5 @@
1
1
  module Maestro
2
2
  module Common
3
- VERSION = '0.0.2'
3
+ VERSION = '0.0.3'
4
4
  end
5
5
  end
@@ -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
@@ -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.2
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-07-16 00:00:00 Z
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: bundler
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: "1.3"
66
+ version: 0.12.10
67
67
  requirement: *id005
68
68
  prerelease: false
69
- type: :development
69
+ type: :runtime
70
70
  - !ruby/object:Gem::Dependency
71
- name: rake
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: "0"
77
+ version: "1.3"
78
78
  requirement: *id006
79
79
  prerelease: false
80
80
  type: :development
81
81
  - !ruby/object:Gem::Dependency
82
- name: jruby-openssl
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: rspec
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: 2.13.0
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