smith 0.5.12 → 0.5.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/bin/smithctl +16 -19
  2. data/lib/smith.rb +25 -41
  3. data/lib/smith/agent.rb +96 -66
  4. data/lib/smith/agent_monitoring.rb +3 -4
  5. data/lib/smith/agent_process.rb +16 -9
  6. data/lib/smith/amqp_errors.rb +53 -0
  7. data/lib/smith/application/agency.rb +23 -20
  8. data/lib/smith/bootstrap.rb +3 -3
  9. data/lib/smith/command.rb +2 -2
  10. data/lib/smith/command_base.rb +4 -0
  11. data/lib/smith/commands/agency/agents.rb +19 -19
  12. data/lib/smith/commands/agency/kill.rb +6 -2
  13. data/lib/smith/commands/agency/list.rb +2 -4
  14. data/lib/smith/commands/agency/logger.rb +27 -28
  15. data/lib/smith/commands/agency/metadata.rb +1 -5
  16. data/lib/smith/commands/agency/object_count.rb +13 -11
  17. data/lib/smith/commands/agency/restart.rb +18 -9
  18. data/lib/smith/commands/agency/start.rb +34 -25
  19. data/lib/smith/commands/agency/stop.rb +58 -41
  20. data/lib/smith/commands/agency/version.rb +10 -10
  21. data/lib/smith/commands/common.rb +7 -4
  22. data/lib/smith/commands/smithctl/acl.rb +46 -37
  23. data/lib/smith/commands/smithctl/commands.rb +1 -1
  24. data/lib/smith/commands/smithctl/firehose.rb +30 -0
  25. data/lib/smith/commands/smithctl/pop.rb +39 -32
  26. data/lib/smith/commands/smithctl/push.rb +70 -51
  27. data/lib/smith/commands/smithctl/rm.rb +32 -9
  28. data/lib/smith/commands/smithctl/subscribe.rb +36 -0
  29. data/lib/smith/commands/smithctl/top.rb +1 -1
  30. data/lib/smith/exceptions.rb +2 -0
  31. data/lib/smith/messaging/acl/agency_command.proto +4 -0
  32. data/lib/smith/messaging/acl/default.rb +8 -1
  33. data/lib/smith/messaging/amqp_options.rb +2 -2
  34. data/lib/smith/messaging/message_counter.rb +21 -0
  35. data/lib/smith/messaging/payload.rb +47 -49
  36. data/lib/smith/messaging/queue.rb +50 -0
  37. data/lib/smith/messaging/queue_definition.rb +18 -0
  38. data/lib/smith/messaging/queue_factory.rb +20 -29
  39. data/lib/smith/messaging/receiver.rb +211 -173
  40. data/lib/smith/messaging/requeue.rb +91 -0
  41. data/lib/smith/messaging/sender.rb +184 -28
  42. data/lib/smith/messaging/util.rb +72 -0
  43. data/lib/smith/queue_definitions.rb +11 -0
  44. data/lib/smith/version.rb +1 -1
  45. metadata +18 -10
  46. data/lib/smith/messaging/endpoint.rb +0 -116
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ module Messaging
4
+ class Queue
5
+
6
+ extend Util
7
+
8
+ class << self
9
+ def messages?(queue_name, &blk)
10
+ number_of_messages(queue_name) do |n|
11
+ yield n > 0
12
+ end
13
+ end
14
+
15
+ def consumers?(queue_name)
16
+ number_of_consumers(queue_name) do |n|
17
+ yield n > 0
18
+ end
19
+ end
20
+
21
+ def number_of_consumers(queue_name)
22
+ status(queue_name) do |_, number_consumers|
23
+ yield number_consumers
24
+ end
25
+ end
26
+
27
+ def number_of_messages(queue_name)
28
+ status(queue_name) do |number_messages, _|
29
+ yield number_messages
30
+ end
31
+ end
32
+
33
+ def status(queue_name)
34
+ open_channel do |channel, ok|
35
+ channel.queue(normalise(queue_name), :passive => true).status do |number_messages, number_consumers|
36
+ yield number_messages, number_consumers
37
+ channel.close
38
+ end
39
+ end
40
+ end
41
+
42
+ def channel
43
+ open_channel do |channel, ok|
44
+ yield channel
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Smith
4
+ class QueueDefinition
5
+
6
+ attr_reader :name, :options
7
+
8
+ def initialize(name, options)
9
+ @name = name
10
+ @options = options
11
+ end
12
+
13
+ # to_a is defined to make the splat operator work.
14
+ def to_a()
15
+ return @name, @options
16
+ end
17
+ end
18
+ end
@@ -6,39 +6,14 @@ module Smith
6
6
  @cache = Cache.new
7
7
  end
8
8
 
9
- def create(queue_name, type, opts={})
10
- key = "#{type}:#{queue_name}"
11
- if @cache[key]
12
- @cache[key]
13
- else
14
- update_cache(key, opts) do |o|
15
- case type
16
- when :receiver
17
- Messaging::Receiver.new(queue_name, o)
18
- when :sender
19
- Messaging::Sender.new(queue_name, o)
20
- else
21
- raise ArgumentError, "unknown queue type"
22
- end
23
- end
24
- end
25
- end
26
-
27
- # Simple wrapper around create that runs Endpoint#ready and calls the block
28
- def queue(queue_name, type, opts={}, &blk)
29
- create(queue_name, type, opts).ready { |queue| blk.call(queue) }
30
- end
31
-
32
- # Convenience method that returns a Sender object. #ready is called by
33
- # this method.
9
+ # Convenience method that returns a Sender object.
34
10
  def sender(queue_name, opts={}, &blk)
35
- queue(queue_name, :sender, opts) { |sender| blk.call(sender) }
11
+ create(queue_name, :sender, opts, &blk)
36
12
  end
37
13
 
38
- # Convenience method that returns a Receiver object. #ready is called by
39
- # this method.
14
+ # Convenience method that returns a Receiver object.
40
15
  def receiver(queue_name, opts={}, &blk)
41
- queue(queue_name, :receiver, opts) { |receiver| blk.call(receiver) }
16
+ create(queue_name, :receiver, opts, &blk)
42
17
  end
43
18
 
44
19
  # Passes each queue to the supplied block.
@@ -55,6 +30,22 @@ module Smith
55
30
 
56
31
  private
57
32
 
33
+ def create(queue_name, type, opts={}, &blk)
34
+ key = "#{type}:#{queue_name}"
35
+ if @cache[key]
36
+ blk.call(@cache[key])
37
+ else
38
+ update_cache(key, opts) do |o|
39
+ case type
40
+ when :receiver
41
+ Messaging::Receiver.new(queue_name, o, &blk)
42
+ when :sender
43
+ Messaging::Sender.new(queue_name, o, &blk)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
58
49
  def update_cache(queue_name, opts, &blk)
59
50
  dont_cache = (opts.has_key?(:dont_cache)) ? opts.delete(:dont_cache) : false
60
51
  if dont_cache
@@ -1,236 +1,274 @@
1
1
  # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'message_counter'
4
+ require_relative 'requeue'
5
+
2
6
  module Smith
3
7
  module Messaging
4
- class Receiver < Endpoint
8
+ class Receiver
5
9
 
6
10
  include Logger
11
+ include Util
7
12
 
8
- attr_accessor :options
13
+ attr_accessor :queue_name
9
14
 
10
- def initialize(queue_name, opts={})
11
- @auto_ack = (opts.has_key?(:auto_ack)) ? opts.delete(:auto_ack) : true
12
- @threading = (opts.has_key?(:threading)) ? opts.delete(:threading) : false
13
- @payload_type = (opts.key?(:type)) ? [opts.delete(:type)].flatten : []
15
+ def initialize(queue_definition, opts={}, &blk)
14
16
 
15
- super(queue_name, AmqpOptions.new(opts))
16
- end
17
+ @queue_name, opts = get_queue_name_and_options(queue_definition, opts)
17
18
 
18
- # Subscribes to a queue and passes the headers and payload into the
19
- # block. +subscribe+ will automatically acknowledge the message unless
20
- # the options sets :ack to false.
21
- def subscribe(&block)
22
- if !@queue.subscribed?
23
- opts = options.subscribe
24
- logger.verbose { "Subscribing to: [queue]:#{denormalized_queue_name} [options]:#{opts}" }
25
- queue.subscribe(opts) do |metadata,payload|
26
- if payload
27
- if @payload_type.empty? || @payload_type.include?(metadata.type.to_sym)
28
- thread(Reply.new(self, metadata, payload)) do |reply|
29
- increment_counter
30
- block.call(reply)
31
- end
32
- else
33
- raise IncorrectPayloadType, "This queue can only accept the following payload types: #{@payload_type.to_a.to_s}"
34
- end
35
- else
36
- logger.verbose { "Received null message on: #{denormalized_queue_name} [options]:#{opts}" }
37
- end
19
+ @normalised_queue_name = normalise(@queue_name)
20
+
21
+ @foo_options = {
22
+ :auto_ack => option_or_default(opts, :auto_ack, true),
23
+ :threading => option_or_default(opts, :threading, false)}
24
+
25
+ @payload_type = option_or_default(opts, :type, []) {|v| [v].flatten }
26
+
27
+ prefetch = option_or_default(opts, :prefetch, Smith.config.agent.prefetch)
28
+
29
+ @options = AmqpOptions.new(opts)
30
+ @options.routing_key = @normalised_queue_name
31
+
32
+ @message_counter = MessageCounter.new(@queue_name)
33
+
34
+ @channel_completion = EM::Completion.new
35
+ @queue_completion = EM::Completion.new
36
+ @exchange_completion = EM::Completion.new
37
+ @requeue_options_completion = EM::Completion.new
38
+
39
+ @reply_queues = {}
40
+
41
+ open_channel(:prefetch => prefetch) do |channel|
42
+ @channel_completion.succeed(channel)
43
+ channel.direct(@normalised_queue_name, @options.exchange) do |exchange|
44
+ @exchange_completion.succeed(exchange)
38
45
  end
39
- else
40
- logger.error { "Queue is already subscribed too. Not listening on: #{denormalise_queue_name}" }
41
46
  end
42
- end
43
47
 
44
- # pops a message off the queue and passes the headers and payload
45
- # into the block. +pop+ will automatically acknowledge the message
46
- # unless the options sets :ack to false.
47
- def pop(&block)
48
- opts = options.pop
49
- @queue.pop(opts) do |metadata, payload|
50
- thread(Reply.new(self, metadata, (payload.nil?) ? nil : payload)) do |reply|
51
- block.call(reply)
48
+ open_channel(:prefetch => prefetch) do |channel|
49
+ channel.queue(@normalised_queue_name, @options.queue) do |queue|
50
+ @exchange_completion.completion do |exchange|
51
+ queue.bind(exchange, :routing_key => @queue_name)
52
+ @queue_completion.succeed(queue)
53
+ @requeue_options_completion.succeed(:exchange => exchange, :queue => queue)
54
+ end
52
55
  end
53
56
  end
54
- end
55
57
 
56
- def threading?
57
- @threading
58
- end
58
+ start_garbage_collection
59
59
 
60
- def auto_ack?
61
- @auto_ack
60
+ blk.call(self) if blk
62
61
  end
63
62
 
64
- private
65
-
66
- # Controls whether to use threads or not. Given that I need to ack in the
67
- # thread (TODO check this) I also need to pass in a flag to say whether
68
- # to auto ack or not. This is because it can get called twice and we don't
69
- # want to ack more than once or an error will be thrown.
70
- def thread(reply, &block)
71
- logger.verbose { "Threads: [queue]: #{denormalized_queue_name}: #{threading?}" }
72
- logger.verbose { "auto_ack: [queue]: #{denormalized_queue_name}: #{auto_ack?}" }
73
- if threading?
74
- EM.defer do
75
- block.call(reply)
76
- reply.ack if auto_ack?
63
+ def start_garbage_collection
64
+ logger.debug { "Starting the garbage collector." }
65
+ EM.add_periodic_timer(5) do
66
+ @reply_queues.each do |queue_name,queue|
67
+ queue.status do |number_of_messages, number_of_consumers|
68
+ if number_of_messages == 0 && number_of_consumers == 0
69
+ queue.delete do |delete_ok|
70
+ @reply_queues.delete(queue_name)
71
+ logger.debug { "Unused reply queue deleted: #{queue_name}" }
72
+ end
73
+ end
74
+ end
77
75
  end
78
- else
79
- block.call(reply)
80
- reply.ack if auto_ack?
81
76
  end
82
77
  end
83
78
 
84
- # I'm not terribly happy about this class. It's publicly visible and it contains
85
- # some gross violations of Ruby's protection mechanism. I suspect it's an indication
86
- # of a more fundamental design flaw. I will leave it as is for the time being but
87
- # this really needs to be reviewed. FIXME review this class.
88
- class Reply
89
-
90
- include Logger
91
-
92
- attr_reader :metadata, :payload, :time
79
+ def ack(multiple=false)
80
+ @channel_completion.completion {|channel| channel.ack(multiple) }
81
+ end
93
82
 
94
- def initialize(receiver, metadata, undecoded_payload)
95
- @undecoded_payload = undecoded_payload
96
- @receiver = receiver
97
- @exchange = receiver.send(:exchange)
98
- @metadata = metadata
99
- @time = Time.now
83
+ private :start_garbage_collection
100
84
 
101
- if undecoded_payload
102
- @payload = ACL::Payload.decode(undecoded_payload, metadata.type)
103
- logger.verbose { "Received content on: [queue]: #{denormalized_queue_name}." }
104
- logger.verbose { "Payload content: [queue]: #{denormalized_queue_name}, [metadata type]: #{metadata.type}, [message]: #{payload.inspect}" }
105
- else
106
- logger.verbose { "Received nil content on: [queue]: #{denormalized_queue_name}." }
107
- @payload = nil
108
- @nil_message = true
85
+ def setup_reply_queue(reply_queue_name, &blk)
86
+ if @reply_queues[reply_queue_name]
87
+ blk.call(@reply_queues[reply_queue_name])
88
+ else
89
+ @exchange_completion.completion do |exchange|
90
+ logger.debug { "Attaching to reply queue: #{reply_queue_name}" }
91
+ Smith::Messaging::Sender.new(reply_queue_name, :auto_delete => false, :immediate => true, :mandatory => true) do |sender|
92
+ @reply_queues[reply_queue_name] = sender
93
+ blk.call(sender)
94
+ end
109
95
  end
110
96
  end
97
+ end
111
98
 
112
- # Reply to a message. If reply_to header is not set a error will be logged
113
- def reply(&block)
114
- responder = Responder.new
115
- if reply_to
116
- responder.callback do |return_value|
117
- Sender.new(@metadata.reply_to, :auto_delete => true).ready do |sender|
118
- logger.verbose { "Replying on: #{@metadata.reply_to}" } if logger.level == 0
119
- sender.publish(ACL::Payload.new(:default).content(return_value), sender.options.publish(:correlation_id => @metadata.message_id))
99
+ # Subscribes to a queue and passes the headers and payload into the
100
+ # block. +subscribe+ will automatically acknowledge the message unless
101
+ # the options sets :ack to false.
102
+ def subscribe(&blk)
103
+ @queue_completion.completion do |queue|
104
+ @requeue_options_completion.completion do |requeue_options|
105
+ if !queue.subscribed?
106
+ opts = @options.subscribe
107
+ logger.debug { "Subscribing to: [queue]:#{@queue_name} [options]:#{opts}" }
108
+ queue.subscribe(opts) do |metadata,payload|
109
+ if payload
110
+ on_message(metadata, payload, requeue_options, &blk)
111
+ else
112
+ logger.verbose { "Received null message on: #{@queue_name} [options]:#{opts}" }
113
+ end
120
114
  end
121
- end
122
- else
123
- # Null responder. If a call on the responder is made log a warning. Something is wrong.
124
- responder.callback do |return_value|
125
- logger.error { "You are responding to a message that has no reply_to on queue: #{denormalized_queue_name}." }
126
- logger.verbose { "Queue options: #{@metadata.exchange}." }
115
+ else
116
+ logger.error { "Queue is already subscribed too. Not listening on: #{@queue_name}" }
127
117
  end
128
118
  end
129
-
130
- block.call(responder)
131
- end
132
-
133
- # acknowledge the message.
134
- def ack(multiple=false)
135
- @metadata.ack(multiple) unless @nil_message
136
119
  end
120
+ end
137
121
 
138
- # reject the message. Optionally requeuing it.
139
- def reject(opts={})
140
- @metadata.reject(opts) unless @nil_message
122
+ # pops a message off the queue and passes the headers and payload
123
+ # into the block. +pop+ will automatically acknowledge the message
124
+ # unless the options sets :ack to false.
125
+ def pop(&blk)
126
+ opts = @options.pop
127
+ @queue_completion.completion do |queue|
128
+ @requeue_options_completion.completion do |requeue_options|
129
+ queue.pop(opts) do |metadata, payload|
130
+ if payload
131
+ on_message(metadata, payload, requeue_options, &blk)
132
+ else
133
+ blk.call(nil,nil)
134
+ end
135
+ end
136
+ end
141
137
  end
138
+ end
142
139
 
143
- def reply_to
144
- @metadata.reply_to
140
+ # Define a channel error handler.
141
+ def on_error(chain=false, &blk)
142
+ # TODO Check that this chains callbacks
143
+ @channel_completion.completion do |channel|
144
+ channel.on_error(&blk)
145
145
  end
146
+ end
146
147
 
147
- # Republish the message to the end of the same queue. This is useful
148
- # for when the agent encounters an error and needs to requeue the message.
149
- def requeue(delay, count, strategy, &block)
150
- requeue_with_strategy(delay, count, strategy) do
151
-
152
- # Sort out the options. Receiver#queue is private hence the send. I know, I know.
153
- opts = @receiver.send(:queue).opts.tap do |o|
154
- o.delete(:queue)
155
- o.delete(:exchange)
156
-
157
- o[:headers] = increment_requeue_count(metadata.headers)
158
- o[:routing_key] = normalised_queue_name
159
- o[:type] = metadata.type
148
+ def on_message(metadata, payload, requeue_options, &blk)
149
+ if @payload_type.empty? || @payload_type.include?(metadata.type.to_sym)
150
+ @message_counter.increment_counter
151
+ if metadata.reply_to
152
+ setup_reply_queue(metadata.reply_to) do |queue|
153
+ Foo.new(metadata, payload, @foo_options.merge(:reply_queue => queue), requeue_options, &blk)
160
154
  end
161
-
162
- logger.verbose { "Requeuing to: #{denormalized_queue_name}. [options]: #{opts}" }
163
- logger.verbose { "Requeuing to: #{denormalized_queue_name}. [message]: #{ACL::Payload.decode(@undecoded_payload, metadata.type)}" }
164
-
165
- @receiver.send(:exchange).publish(@undecoded_payload, opts)
155
+ else
156
+ Foo.new(metadata, payload, @foo_options, requeue_options, &blk)
166
157
  end
158
+ else
159
+ raise IncorrectPayloadType, "This queue can only accept the following payload types: #{@payload_type.to_a.to_s}"
167
160
  end
161
+ end
168
162
 
169
- def on_requeue_error(&block)
170
- @on_requeue_error = block
163
+ private :on_message
164
+
165
+ # def delete(&blk)
166
+ # @queue_completion.completion do |queue|
167
+ # queue.delete do
168
+ # @exchange_completion.completion do |exchange|
169
+ # exchange.delete do
170
+ # @channel_completion.completion do |channel|
171
+ # channel.close(&blk)
172
+ # end
173
+ # end
174
+ # end
175
+ # end
176
+ # end
177
+ # end
178
+
179
+
180
+ # I've a feeling this should be replaced by the above code.
181
+ # TODO Check this is correct.
182
+ def delete(&blk)
183
+ @queue_completion.completion do |queue|
184
+ queue.delete(&blk)
171
185
  end
186
+ end
172
187
 
173
- def on_requeue(&block)
174
- @on_requeue = block
188
+ def status(&blk)
189
+ @queue_completion.completion do |queue|
190
+ queue.status do |num_messages, num_consumers|
191
+ blk.call(num_messages, num_consumers)
192
+ end
175
193
  end
194
+ end
176
195
 
177
- def current_requeue_number
178
- metadata.headers['requeue'] || 0
196
+ def requeue_parameters(opts={})
197
+ @requeue_options_completion.completion do |requeue_options|
198
+ requeue_options.merge!(opts)
179
199
  end
200
+ end
180
201
 
181
- # The payload type. This returns the protocol buffers class name as a string.
182
- def payload_type
183
- @metadata.type
202
+ def on_requeue(&blk)
203
+ @requeue_options_completion.completion do |requeue_options|
204
+ requeue_options.merge!(:on_requeue => blk)
184
205
  end
206
+ end
185
207
 
186
- def queue_name
187
- denormalized_queue_name
208
+ def on_requeue_limit(&blk)
209
+ @requeue_options_completion.completion do |requeue_options|
210
+ requeue_options.merge!(:on_requeue_limit => blk)
188
211
  end
212
+ end
213
+ end
189
214
 
190
- private
191
215
 
192
- def requeue_with_strategy(delay, count, strategy, &block)
193
- if current_requeue_number < count
194
- method = "#{strategy}_strategy".to_sym
195
- if respond_to?(method, true)
196
- cumulative_delay = send(method, delay)
197
- @on_requeue.call(cumulative_delay, current_requeue_number + 1)
198
- EM.add_timer(cumulative_delay) do
199
- block.call(cumulative_delay, current_requeue_number + 1)
200
- end
201
- else
202
- raise RuntimeError, "Unknown requeue strategy. #{method}"
203
- end
204
- else
205
- @on_requeue_error.call(cumulative_delay, current_requeue_number)
216
+ class Foo
217
+ include Smith::Logger
218
+
219
+ attr_accessor :metadata
220
+
221
+ def initialize(metadata, data, opts={}, requeue_opts, &blk)
222
+ @metadata = metadata
223
+ @reply_queue = opts[:reply_queue]
224
+ @requeue_opts = requeue_opts
225
+
226
+ @time = Time.now
227
+ @message = ACL::Payload.decode(data, metadata.type)
228
+
229
+ if opts[:threading]
230
+ EM.defer do
231
+ blk.call(@message, self)
232
+ ack if opts[:auto_ack]
206
233
  end
234
+ else
235
+ blk.call(@message, self)
236
+ ack if opts[:auto_ack]
207
237
  end
238
+ end
208
239
 
209
- def exponential_no_initial_delay_strategy(delay)
210
- delay * (2 ** current_requeue_number - 1)
211
- end
240
+ # Send a message to the reply_to queue as specified in the message header.
241
+ def reply(acl=nil, &blk)
242
+ raise ArgumentError, "you cannot supply an ACL and a blcok." if acl && blk
243
+ raise ArgumentError, "you must supply either an ACL or a blcok." if acl.nil? && blk.nil?
212
244
 
213
- def exponential_strategy(delay)
214
- delay * (2 ** current_requeue_number)
245
+ if @metadata.reply_to
246
+ @reply_queue.publish((blk) ? blk.call : acl, :correlation_id => @metadata.message_id)
247
+ else
248
+ logger.error { "You are replying to a message that has no reply_to: #{@metadata.exchange}." }
215
249
  end
250
+ end
216
251
 
217
- def linear_strategy(delay)
218
- delay * (current_requeue_number + 1)
219
- end
252
+ # Requeue the current mesage on the current queue. A requeue number is
253
+ # added to the message header which is used to ensure the correct number
254
+ # of requeues.
255
+ def requeue
256
+ Requeue.new(@message, @metadata, @requeue_opts).requeue
257
+ end
220
258
 
221
- def denormalized_queue_name
222
- @receiver.denormalized_queue_name
223
- end
259
+ # acknowledge the message.
260
+ def ack(multiple=false)
261
+ @metadata.ack(multiple)
262
+ end
224
263
 
225
- def normalised_queue_name
226
- @receiver.queue_name
227
- end
264
+ # reject the message. Optionally requeuing it.
265
+ def reject(opts={})
266
+ @metadata.reject(opts)
267
+ end
228
268
 
229
- def increment_requeue_count(headers)
230
- headers.tap do |m|
231
- m['requeue'] = (m['requeue']) ? m['requeue'] + 1 : 1
232
- end
233
- end
269
+ # the correlation_id
270
+ def correlation_id
271
+ @metadata.correlation_id
234
272
  end
235
273
  end
236
274
  end