emissary 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,274 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'monitor'
17
+ require 'work_queue'
18
+
19
+ module Emissary
20
+ module OperatorStatistics
21
+ RX_COUNT_MUTEX = Mutex.new
22
+ TX_COUNT_MUTEX = Mutex.new
23
+
24
+ attr_reader :rx_count, :tx_count
25
+ def increment_tx_count
26
+ TX_COUNT_MUTEX.synchronize {
27
+ @tx_count = (@tx_count + 1 rescue 1)
28
+ }
29
+ end
30
+
31
+ def tx_count
32
+ count = 0
33
+ TX_COUNT_MUTEX.synchronize {
34
+ count = @tx_count
35
+ @tx_count = 0
36
+ }
37
+ count
38
+ end
39
+
40
+ def increment_rx_count
41
+ RX_COUNT_MUTEX.synchronize {
42
+ @rx_count = (@rx_count + 1 rescue 1)
43
+ }
44
+ end
45
+
46
+ def rx_count
47
+ count = 0
48
+ RX_COUNT_MUTEX.synchronize {
49
+ count = @rx_count
50
+ @rx_count = 0
51
+ }
52
+ count
53
+ end
54
+ end
55
+
56
+ class Operator
57
+ include Emissary::OperatorStatistics
58
+
59
+ DEFAULT_STATUS_INTERVAL = 3600
60
+ DEFAULT_MAX_WORKERS = 50
61
+ MAX_WORKER_TTL = 60
62
+
63
+ attr_reader :config, :shutting_down, :signature
64
+
65
+ # Override .new so subclasses don't have to call super and can ignore
66
+ # connection-specific arguments
67
+ #
68
+ def self.new(config, *args)
69
+ allocate.instance_eval do
70
+ # Store signature
71
+ @signature = config[:signature]
72
+
73
+ # Call a superclass's #initialize if it has one
74
+ initialize(config, *args)
75
+
76
+ # post initialize callback
77
+ post_init
78
+
79
+ # set signature nil
80
+ @signature ||= Digest::MD5.hexdigest(config.to_s)
81
+
82
+ self
83
+ end
84
+ end
85
+
86
+ def initialize(config, *args)
87
+ @config = config
88
+ @workers = (args[0][:max_workers] || DEFAULT_MAX_WORKERS rescue DEFAULT_MAX_WORKERS)
89
+
90
+ @agents = WorkQueue.new(@workers, nil, MAX_WORKER_TTL)
91
+ @publisher = WorkQueue.new(@workers, nil, MAX_WORKER_TTL)
92
+
93
+ @timer = nil
94
+ @stats = WorkQueue.new(1, nil, MAX_WORKER_TTL)
95
+
96
+ @rx_count = 0
97
+ @tx_count = 0
98
+
99
+ @shutting_down = false
100
+ @connected = false
101
+ end
102
+
103
+ def connected?() @connected; end
104
+
105
+ def post_init
106
+ end
107
+
108
+ def connect
109
+ raise NotImplementedError, 'The connect method must be defined by the operator module'
110
+ end
111
+
112
+ def subscribe
113
+ raise NotImplementedError, 'The subscrie method must be defined by the operator module'
114
+ end
115
+
116
+ def unsubscribe
117
+ raise NotImplementedError, 'The unsubscribe method must be defined by the operator module'
118
+ end
119
+
120
+ def acknowledge message
121
+ end
122
+
123
+ def reject message, requeue = true
124
+ end
125
+
126
+ def send_data
127
+ raise NotImplementedError, 'The send_data method must be defined by the operator module'
128
+ end
129
+
130
+ def close
131
+ raise NotImplementedError, 'The close method must be defined by the operator module'
132
+ end
133
+
134
+ def run
135
+ @connected = !!connect
136
+ subscribe
137
+ schedule_statistics_gatherer
138
+ notify :startup
139
+ connected?
140
+ end
141
+
142
+ def disconnect
143
+ close
144
+ @connected = false
145
+ end
146
+
147
+ def shutting_down?() @shutting_down; end
148
+
149
+ def shutdown!
150
+ unless shutting_down?
151
+ @shutting_down = true
152
+
153
+ Emissary.logger.info "Cancelling periodic timer for statistics gatherer..."
154
+ @timer.cancel
155
+
156
+ Emissary.logger.notice "Shutting down..."
157
+ notify :shutdown
158
+
159
+ Emissary.logger.info "Shutting down agent workqueue..."
160
+ @agents.join
161
+
162
+ Emissary.logger.info "Shutting down publisher workqueue..."
163
+ @publisher.join
164
+
165
+ Emissary.logger.info "Disconnecting..."
166
+ disconnect
167
+ end
168
+ end
169
+
170
+ def enabled? what
171
+ unless [ :startup, :shutdown, :stats ].include? what.to_sym
172
+ Emissary.logger.debug "Testing '#{what}' - it's disabled. Not a valid option."
173
+ return false
174
+ end
175
+
176
+ unless config[what]
177
+ Emissary.logger.debug "Testing '#{what}' - it's disabled. Missing from configuration."
178
+ return false
179
+ end
180
+
181
+ if (config[:disable]||[]).include? what.to_s
182
+ Emissary.logger.debug "Testing '#{what}' - it's disabled. Listed in 'disable' configuration option."
183
+ return false
184
+ end
185
+
186
+ Emissary.logger.debug "Testing '#{what}' - it's enabled.."
187
+ return true
188
+ end
189
+
190
+ def received message
191
+ acknowledge message
192
+ end
193
+
194
+ def rejected message, opts = { :requeue => true }
195
+ reject message, opts
196
+ end
197
+
198
+ def receive message
199
+ @agents.enqueue_b {
200
+ begin
201
+ raise message.errors.first unless message.errors.empty? or not message.errors.first.is_a? Exception
202
+ Emissary.logger.debug " ---> [DISPATCHER] Dispatching new message ... "
203
+ Emissary.dispatch(message, config, self).activate
204
+ # ack message if need be (operator dependant)
205
+ received message
206
+ rescue ::Emissary::Error::InvalidMessageFormat => e
207
+ Emissary.logger.warning e.message
208
+ rejected message, :requeue => true
209
+ # if it was an encoding error, then we are done - nothing more we can do
210
+ rescue Exception => e
211
+ Emissary.logger.error "AgentThread Error: #{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
212
+ send message.error(e)
213
+ rejected message, :requeue => true
214
+ else
215
+ increment_rx_count
216
+ end
217
+ Emissary.logger.debug " ---> [DISPATCHER] tasks/workers: #{@agents.cur_tasks}/#{@agents.cur_threads}"
218
+ }
219
+ end
220
+
221
+ def send message
222
+ @publisher.enqueue_b {
223
+ Emissary.logger.debug " ---> [PUBLISHER] Sending new message ... "
224
+ begin
225
+ unless message.will_loop?
226
+ Emissary.logger.debug "[PUBLISHER] -- Sending message..."
227
+ send_data message
228
+ increment_tx_count
229
+ else
230
+ Emissary.logger.notice "Not sending message destined for myself - would loop."
231
+ end
232
+ rescue Exception => e
233
+ Emissary.logger.error "PublisherThread Error: #{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
234
+ @shutting_down = true
235
+ end
236
+ Emissary.logger.debug " ---> [PUBLISHER] tasks/workers: #{@publisher.cur_tasks}/#{@publisher.cur_threads}"
237
+ }
238
+ end
239
+
240
+ def notify type
241
+ return unless enabled? type and EM.reactor_running?
242
+
243
+ message = Emissary::Message.new(:data => { :agent => :emissary, :method => type })
244
+ case type
245
+ when :startup, :shutdown
246
+ message.recipient = config[type]
247
+ when :stats
248
+ message.agent = :stats
249
+ message.method = :gather
250
+ end
251
+
252
+ Emissary.logger.notice "Running #{type.to_s.capitalize} Notifier"
253
+ receive message
254
+ end
255
+
256
+ def schedule_statistics_gatherer
257
+ stats_interval = enabled?(:stats) && config[:stats][:interval] ? config[:stats][:interval].to_i : DEFAULT_STATUS_INTERVAL
258
+
259
+ # setup agent to process sending of messages
260
+ @timer = EM.add_periodic_timer(stats_interval) do
261
+ rx = rx_count; tx = tx_count
262
+ rx_throughput = sprintf "%0.4f", (rx.to_f / stats_interval.to_f)
263
+ tx_throughput = sprintf "%0.4f", (tx.to_f / stats_interval.to_f)
264
+
265
+ Emissary.logger.notice "[statistics] publisher tasks/workers: #{@publisher.cur_tasks}/#{@publisher.cur_threads}"
266
+ Emissary.logger.notice "[statistics] dispatcher tasks/workers: #{@agents.cur_tasks}/#{@agents.cur_threads}"
267
+ Emissary.logger.notice "[statistics] #{tx} in #{stats_interval} seconds - tx rate: #{tx_throughput}/sec"
268
+ Emissary.logger.notice "[statistics] #{rx} in #{stats_interval} seconds - rx rate: #{rx_throughput}/sec"
269
+
270
+ notify :stats
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,203 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'eventmachine'
17
+ require 'mq'
18
+ require 'uri'
19
+
20
+ module Emissary
21
+ class Operator
22
+ module AMQP
23
+
24
+ class InvalidExchange < ArgumentError; end
25
+ class InvalidConfig < StandardError; end
26
+
27
+ REQUIRED_KEYS = [ :uri, :subscriptions ]
28
+ VALID_EXCHANGES = [ :headers, :topic, :direct, :fanout ]
29
+
30
+ attr_accessor :subscriptions
31
+ attr_accessor :not_acked
32
+
33
+ @@queue_count = 1
34
+
35
+ def validate_config!
36
+ errors = []
37
+ errors << 'config not a hash!' unless config.instance_of? Hash
38
+
39
+ REQUIRED_KEYS.each do |key|
40
+ errors << "missing required option '#{key}'" unless config.has_key? key
41
+ end
42
+
43
+ u = ::URI.parse(config[:uri])
44
+ errors << "URI scheme /must/ be one of 'amqp' or 'amqps'" unless !!u.scheme.match(/^amqps{0,1}$/)
45
+ [ :user, :password, :host, :path ].each do |v|
46
+ errors << "invalid value 'nil' for URI part [#{v}]" if u.respond_to? v and u.send(v).nil?
47
+ end
48
+
49
+ raise errors.join("\n") unless errors.empty?
50
+ return true
51
+ end
52
+
53
+ def post_init
54
+ uri = ::URI.parse @config[:uri]
55
+ ssl = (uri.scheme.to_sym == :amqps)
56
+
57
+ @connect_details = {
58
+ :host => uri.host,
59
+ :ssl => ssl,
60
+ :user => (::URI.decode(uri.user) rescue nil) || 'guest',
61
+ :pass => (::URI.decode(uri.password) rescue nil) || 'guest',
62
+ :vhost => (! uri.path.empty? ? uri.path : '/nimbul'),
63
+ :port => uri.port || (ssl ? 5671 : 5672),
64
+ :logging => !!@config[:debug],
65
+ }
66
+
67
+ # normalize the subscriptions
68
+ @subscriptions = @config[:subscriptions].inject({}) do |hash,queue|
69
+ key, type = queue.split(':')
70
+ type = type.nil? ? DEFAULT_EXCHANGE : (VALID_EXCHANGES.include?(type.to_sym) ? type.to_sym : DEFAULT_EXCHANGE)
71
+ (hash[type] ||= []) << key
72
+ hash
73
+ end
74
+
75
+ # one unique receiving queue per connection
76
+ @queue_name = "#{Emissary.identity.queue_name}.#{@@queue_count}"
77
+ @@queue_count += 1
78
+
79
+ @not_acked = {}
80
+ end
81
+
82
+ def connect
83
+ if @connect_details[:ssl] and not EM.ssl?
84
+ raise ::Emissary::Error::ConnectionError ,
85
+ "Requested SSL connection but EventMachine not compiled with SSL support - quitting!"
86
+ end
87
+
88
+ @message_pool = Queue.new
89
+
90
+ @connection = ::AMQP.connect(@connect_details)
91
+ @channel = ::MQ.new(@connection)
92
+
93
+ @queue_config = {
94
+ :durable => @config[:queue_durable].nil? ? false : @config[:queue_durable],
95
+ :auto_delete => @config[:queue_auto_delete].nil? ? true : @config[:queue_auto_delete],
96
+ :exclusive => @config[:queue_exclusive].nil? ? true : @config[:queue_exclusive]
97
+ }
98
+
99
+ @queue = ::MQ::Queue.new(@channel, @queue_name, @queue_config)
100
+
101
+ @exchanges = {}
102
+ @exchanges[:topic] = ::MQ::Exchange.new(@channel, :topic, 'amq.topic')
103
+ @exchanges[:fanout] = ::MQ::Exchange.new(@channel, :fanout, 'amq.fanout')
104
+ @exchanges[:direct] = ::MQ::Exchange.new(@channel, :direct, 'amq.direct')
105
+
106
+ true
107
+ end
108
+
109
+ def subscribe
110
+ @subscriptions.each do |exchange, keys|
111
+ keys.map do |routing_key|
112
+ Emissary.logger.debug "Subscribing To Key: '#{routing_key}' on Exchange '#{exchange}'"
113
+ @queue.bind(@exchanges[exchange], :key => routing_key)
114
+ end
115
+ end
116
+
117
+ # now bind to our name directly so we get messages that are
118
+ # specifically for us
119
+ @queue.bind(@exchanges[:direct], :key => Emissary.identity.queue_name)
120
+
121
+ @queue.subscribe(:ack => true) do |info, message|
122
+ begin
123
+ message = Emissary::Message.decode(message).stamp_received!
124
+ rescue ::Emissary::Error::InvalidMessageFormat => e
125
+ message = Emissary::Message.new
126
+ message.errors << e
127
+ end
128
+
129
+ @not_acked[message.uuid] = info
130
+ Emissary.logger.debug "Received through '#{info.exchange}' and routing key '#{info.routing_key}'"
131
+
132
+ receive message
133
+ end
134
+ end
135
+
136
+ def unsubscribe
137
+ @subscriptions.each do |exchange, keys|
138
+ keys.map do |routing_key|
139
+ Emissary.logger.info "Unsubscribing from '#{routing_key}' on Exchange '#{exchange}'"
140
+ @queue.unbind(@exchanges[exchange], :key => routing_key)
141
+ end
142
+ end
143
+
144
+ Emissary.logger.info "Unsubscribing from my own queue."
145
+ @queue.unbind(@exchanges[:direct], :key => Emissary.identity.queue_name)
146
+
147
+ Emissary.logger.info "Cancelling all subscriptions."
148
+ @queue.unsubscribe # could get away with only calling this but, the above "feels" cleaner
149
+ end
150
+
151
+ def send_data msg
152
+ begin
153
+ Emissary.logger.debug "Sending message through exchange '#{msg.exchange_type.to_s}' with routing key '#{msg.routing_key}'"
154
+ Emissary.logger.debug "Message Originator: #{msg.originator} - Recipient: #{msg.recipient}"
155
+ @exchanges[msg.exchange_type].publish msg.stamp_sent!.encode, :routing_key => msg.routing_key
156
+ rescue NoMethodError
157
+ raise InvalidExchange, "publish request on invalid exchange '#{msg.exchange_type}' with routing key '#{msg.routing_key}'"
158
+ end
159
+ end
160
+
161
+ def acknowledge message
162
+ unless message.kind_of? Emissary::Message
163
+ Emissary.logger.warning "Can't acknowledge message not deriving from Emissary::Message class"
164
+ end
165
+
166
+ @not_acked.delete(message.uuid).ack
167
+ Emissary.logger.debug "Acknowledged Message ID: #{message.uuid}"
168
+ rescue NoMethodError
169
+ Emissary.logger.warning "Message with UUID #{message.uuid} not acknowledged."
170
+ rescue Exception => e
171
+ Emissary.logger.error "Error in Emissary::Operator::AMQP#acknowledge: #{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
172
+ end
173
+
174
+ def reject message, opts = { :requeue => true }
175
+ return true # currently not implemented in RabbitMQ 1.7.x (coming in versions supporting 0.9.1 AMQP spec)
176
+ unless message.kind_of? Emissary::Message
177
+ Emissary.logger.warning "Unable to reject message not deriving from Emissary::Message class"
178
+ end
179
+
180
+ @not_acked.delete(message.uuid).reject(opts)
181
+ Emissary.logger.debug "Rejected Message ID: #{message.uuid}"
182
+ rescue Exception => e
183
+ Emissary.logger.error "Error in AMQP::Reject: #{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
184
+ end
185
+
186
+ def close
187
+ # Note: NOT currently supported by current version of RabbitMQ (1.7.x)
188
+ #Emissary.logger.info "Requeueing unacknowledged messages"
189
+ #@not_acked.each { |i| i.reject :requeue => true }
190
+
191
+ # unfortunately, due to the nature of amqp's deferred asyncronous handling of data send/recv
192
+ # and no ability to determine whether our shutdown message was /actually/ sent, we have to resort
193
+ # to sleeping for 1s to ensure our message went out
194
+ sleep 1 # XXX: HACK HACK HACK - BAD BAD BAD :-(
195
+ unsubscribe
196
+ ::AMQP.stop
197
+ end
198
+
199
+ def status
200
+ end
201
+ end
202
+ end
203
+ end