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