emissary 1.3.0

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