emissary 1.3.20 → 1.3.21

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