smith 0.5.7

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.
Files changed (50) hide show
  1. data/bin/agency +55 -0
  2. data/bin/smithctl +102 -0
  3. data/lib/smith.rb +237 -0
  4. data/lib/smith/acl_compiler.rb +74 -0
  5. data/lib/smith/agent.rb +207 -0
  6. data/lib/smith/agent_cache.rb +40 -0
  7. data/lib/smith/agent_config.rb +22 -0
  8. data/lib/smith/agent_monitoring.rb +52 -0
  9. data/lib/smith/agent_process.rb +181 -0
  10. data/lib/smith/application/agency.rb +126 -0
  11. data/lib/smith/bootstrap.rb +153 -0
  12. data/lib/smith/cache.rb +61 -0
  13. data/lib/smith/command.rb +128 -0
  14. data/lib/smith/commands/agency/agents.rb +28 -0
  15. data/lib/smith/commands/agency/common.rb +18 -0
  16. data/lib/smith/commands/agency/kill.rb +13 -0
  17. data/lib/smith/commands/agency/list.rb +65 -0
  18. data/lib/smith/commands/agency/logger.rb +56 -0
  19. data/lib/smith/commands/agency/metadata.rb +14 -0
  20. data/lib/smith/commands/agency/restart.rb +39 -0
  21. data/lib/smith/commands/agency/start.rb +62 -0
  22. data/lib/smith/commands/agency/state.rb +14 -0
  23. data/lib/smith/commands/agency/stop.rb +70 -0
  24. data/lib/smith/commands/agency/version.rb +23 -0
  25. data/lib/smith/commands/smithctl/cat.rb +70 -0
  26. data/lib/smith/commands/smithctl/pop.rb +76 -0
  27. data/lib/smith/commands/smithctl/rm.rb +36 -0
  28. data/lib/smith/commands/smithctl/smithctl_version.rb +23 -0
  29. data/lib/smith/commands/smithctl/top.rb +42 -0
  30. data/lib/smith/commands/template.rb +9 -0
  31. data/lib/smith/config.rb +32 -0
  32. data/lib/smith/logger.rb +91 -0
  33. data/lib/smith/messaging/acl/agency_command.proto +5 -0
  34. data/lib/smith/messaging/acl/agent_command.proto +5 -0
  35. data/lib/smith/messaging/acl/agent_config_request.proto +4 -0
  36. data/lib/smith/messaging/acl/agent_config_update.proto +5 -0
  37. data/lib/smith/messaging/acl/agent_keepalive.proto +6 -0
  38. data/lib/smith/messaging/acl/agent_lifecycle.proto +12 -0
  39. data/lib/smith/messaging/acl/agent_stats.proto +14 -0
  40. data/lib/smith/messaging/acl/default.rb +51 -0
  41. data/lib/smith/messaging/acl/search.proto +9 -0
  42. data/lib/smith/messaging/amqp_options.rb +55 -0
  43. data/lib/smith/messaging/endpoint.rb +116 -0
  44. data/lib/smith/messaging/exceptions.rb +7 -0
  45. data/lib/smith/messaging/payload.rb +102 -0
  46. data/lib/smith/messaging/queue_factory.rb +67 -0
  47. data/lib/smith/messaging/receiver.rb +237 -0
  48. data/lib/smith/messaging/responder.rb +15 -0
  49. data/lib/smith/messaging/sender.rb +61 -0
  50. metadata +239 -0
@@ -0,0 +1,116 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ module Messaging
4
+ class Endpoint
5
+ include Logger
6
+
7
+ attr_accessor :denomalized_queue_name, :queue_name
8
+
9
+ def initialize(queue_name, options)
10
+ @denomalized_queue_name = queue_name
11
+ @queue_name = normalise(queue_name)
12
+ @message_counts = Hash.new(0)
13
+ @options = options
14
+ end
15
+
16
+ def ready(&blk)
17
+ Smith.channel.direct(@queue_name, options.exchange) do |exchange|
18
+ @exchange = exchange
19
+
20
+ exchange.on_return do |basic_return,metadata,payload|
21
+ logger.error { "#{ACL::Payload.decode(payload.clone, metadata.type)} was returned! Exchange: #{reply_code.exchange}, reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}" }
22
+ logger.error { "Properties: #{metadata.properties}" }
23
+ end
24
+
25
+ logger.verbose { "Creating queue: [queue]:#{denomalized_queue_name} [options]:#{options.queue}" }
26
+
27
+ Smith.channel.queue(queue_name, options.queue) do |queue|
28
+ @queue = queue
29
+ @options.queue_name = queue_name
30
+ queue.bind(exchange, :routing_key => queue_name)
31
+ blk.call(self)
32
+ end
33
+ end
34
+ end
35
+
36
+ def number_of_messages
37
+ @queue.status do |num_messages, num_consumers|
38
+ yield num_messages
39
+ end
40
+ end
41
+
42
+ def number_of_consumers
43
+ @queue.status do |num_messages, num_consumers|
44
+ yield num_consumers
45
+ end
46
+ end
47
+
48
+ # Return the total number of messages sent or received for the named queue.
49
+ def counter
50
+ @message_counts[queue_name]
51
+ end
52
+
53
+ def messages?(blk=nil, err=proc {logger.debug { "No messages on #{@denomalized_queue_name}" } })
54
+ number_of_messages do |n|
55
+ if n > 0
56
+ if blk.respond_to? :call
57
+ blk.call(self)
58
+ else
59
+ yield self
60
+ end
61
+ else
62
+ err.call
63
+ end
64
+ end
65
+ end
66
+
67
+ def consumers?(blk=nil, err=proc {logger.debug { "Nothing listening on #{@denomalized_queue_name}" } })
68
+ number_of_consumers do |n|
69
+ if n > 0
70
+ if blk.respond_to? :call
71
+ blk.call(self)
72
+ else
73
+ yield self
74
+ end
75
+ else
76
+ if err.respond_to? :call
77
+ err.call
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def timeout(timeout, blk=nil, &block)
84
+ cancel_timeout
85
+ blk ||= block
86
+ @timeout = EventMachine::Timer.new(timeout, blk)
87
+ end
88
+
89
+ protected
90
+
91
+ attr_accessor :exchange, :queue, :options
92
+
93
+ def increment_counter(value=1)
94
+ @message_counts[queue_name] += value
95
+ end
96
+
97
+ def denormalise(name)
98
+ name.sub(/#{Regexp.escape("#{Smith.config.smith.namespace}.")}/, '')
99
+ end
100
+
101
+ def normalise(name)
102
+ "#{Smith.config.smith.namespace}.#{name}"
103
+ end
104
+
105
+ def cancel_timeout
106
+ @timeout.cancel if @timeout
107
+ end
108
+
109
+ private
110
+
111
+ def random(prefix = '', suffix = '')
112
+ "#{prefix}#{SecureRandom.hex(8)}#{suffix}"
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ module Messaging
4
+ class IncompletePayload < RuntimeError; end
5
+ class IncorrectPayloadType < RuntimeError; end
6
+ end
7
+ end
@@ -0,0 +1,102 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ module ACL
4
+
5
+ module ACLInstanceMethods
6
+ def inspect
7
+ "<#{self.class.to_s}> -> #{self.to_hash}"
8
+ end
9
+
10
+ def as_json
11
+ Yajl.dump(self.to_hash)
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def content_class(e)
17
+ @@acl_classes ||= {:default => Default}
18
+
19
+ e = e.to_sym
20
+
21
+ if @@acl_classes.include?(e)
22
+ @@acl_classes[e]
23
+ else
24
+ class_name = Extlib::Inflection.camelize(e)
25
+ if ACL.constants.include?(class_name)
26
+ logger.error { "Shouldn't get here." }
27
+ else
28
+ logger.debug { "#{class_name} Loaded from #{e}.pb.rb" }
29
+ ACL.const_get(class_name).tap do |clazz|
30
+ # Override the inspect method
31
+ @@acl_classes[e] = clazz.send(:include, ACLInstanceMethods)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ class Payload
39
+ include Logger
40
+
41
+ include ClassMethods
42
+ extend ClassMethods
43
+
44
+ # content can be an existing ACL class.
45
+ def initialize(type=:default, opts={})
46
+ if opts[:from]
47
+ @type = opts[:from].class.to_s.split(/::/).last.snake_case
48
+ @content = opts[:from]
49
+ else
50
+ @type = type
51
+ @clazz = content_class(type)
52
+ end
53
+ end
54
+
55
+ # Add content to the content or get the content from a payload
56
+ def content(*content, &block)
57
+ if content.empty?
58
+ if block.nil?
59
+ return @content
60
+ else
61
+ @content = @clazz.new
62
+ block.call(@content)
63
+ end
64
+ else
65
+ @content = @clazz.new(content.first)
66
+ end
67
+ self
68
+ end
69
+
70
+ # The type of content.
71
+ def type
72
+ @type.to_s
73
+ end
74
+
75
+ # Returns a hash of the payload.
76
+ def to_hash
77
+ @content.to_hash
78
+ end
79
+
80
+ # Encode the content, returning the encoded data.
81
+ def encode
82
+ @content.serialize_to_string
83
+ end
84
+
85
+ # Returns true if the payload has all its required fields set.
86
+ def initialized?
87
+ raise RuntimeError, "You probably forgot to call #content or give the :from option when instantiating the object." if @content.nil?
88
+ @content.initialized?
89
+ end
90
+
91
+ # Convert the payload to a pretty string.
92
+ def to_s
93
+ @content.inspect
94
+ end
95
+
96
+ # Decode the content using the specified decoder.
97
+ def self.decode(payload, decoder=:default)
98
+ content_class(decoder).new.parse_from_string(payload)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Smith
4
+ class QueueFactory
5
+ def initialize
6
+ @cache = Cache.new
7
+ end
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.
34
+ def sender(queue_name, opts={}, &blk)
35
+ queue(queue_name, :sender, opts) { |sender| blk.call(sender) }
36
+ end
37
+
38
+ # Convenience method that returns a Receiver object. #ready is called by
39
+ # this method.
40
+ def receiver(queue_name, opts={}, &blk)
41
+ queue(queue_name, :receiver, opts) { |receiver| blk.call(receiver) }
42
+ end
43
+
44
+ # Passes each queue to the supplied block.
45
+ def each_queue
46
+ @cache.each do |queue|
47
+ yield queue
48
+ end
49
+ end
50
+
51
+ # Returns all queues as a hash, with the queue name being the key.
52
+ def queues
53
+ @cache
54
+ end
55
+
56
+ private
57
+
58
+ def update_cache(queue_name, opts, &blk)
59
+ dont_cache = (opts.has_key?(:dont_cache)) ? opts.delete(:dont_cache) : false
60
+ if dont_cache
61
+ blk.call(opts)
62
+ else
63
+ @cache.update(queue_name, blk.call(opts))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,237 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ module Messaging
4
+ class Receiver < Endpoint
5
+
6
+ include Logger
7
+
8
+ attr_accessor :options
9
+
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 : []
14
+
15
+ super(queue_name, AmqpOptions.new(opts))
16
+ end
17
+
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]:#{denomalized_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: #{denomalized_queue_name} [options]:#{opts}" }
37
+ end
38
+ end
39
+ else
40
+ logger.error { "Queue is already subscribed too. Not listening on: #{denormalise_queue_name}" }
41
+ end
42
+ end
43
+
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)
52
+ end
53
+ end
54
+ end
55
+
56
+ def threading?
57
+ @threading
58
+ end
59
+
60
+ def auto_ack?
61
+ @auto_ack
62
+ end
63
+
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]: #{denomalized_queue_name}: #{threading?}" }
72
+ logger.verbose { "auto_ack: [queue]: #{denomalized_queue_name}: #{auto_ack?}" }
73
+ if threading?
74
+ EM.defer do
75
+ block.call(reply)
76
+ reply.ack if auto_ack?
77
+ end
78
+ else
79
+ block.call(reply)
80
+ reply.ack if auto_ack?
81
+ end
82
+ end
83
+
84
+ # I'm not terribly happy about this class. It's publicaly visable 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
93
+
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
100
+
101
+ if undecoded_payload
102
+ @payload = ACL::Payload.decode(undecoded_payload, metadata.type)
103
+ logger.verbose { "Received content on: [queue]: #{denomalized_queue_name}." }
104
+ logger.verbose { "Payload content: [queue]: #{denomalized_queue_name}, [metadata type]: #{metadata.type}, [message]: #{payload.inspect}" }
105
+ else
106
+ logger.verbose { "Received nil content on: [queue]: #{denomalized_queue_name}." }
107
+ @payload = nil
108
+ @nil_message = true
109
+ end
110
+ end
111
+
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))
120
+ 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: #{denomalized_queue_name}." }
126
+ logger.verbose { "Queue options: #{@metadata.exchange}." }
127
+ end
128
+ 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
+ end
137
+
138
+ # reject the message. Optionally requeuing it.
139
+ def reject(opts={})
140
+ @metadata.reject(opts) unless @nil_message
141
+ end
142
+
143
+ def reply_to
144
+ @metadata.reply_to
145
+ end
146
+
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
160
+ end
161
+
162
+ logger.verbose { "Requeuing to: #{denomalized_queue_name}. [options]: #{opts}" }
163
+ logger.verbose { "Requeuing to: #{denomalized_queue_name}. [message]: #{ACL::Payload.decode(@undecoded_payload, metadata.type)}" }
164
+
165
+ @receiver.send(:exchange).publish(@undecoded_payload, opts)
166
+ end
167
+ end
168
+
169
+ def on_requeue_error(&block)
170
+ @on_requeue_error = block
171
+ end
172
+
173
+ def on_requeue(&block)
174
+ @on_requeue = block
175
+ end
176
+
177
+ def current_requeue_number
178
+ metadata.headers['requeue'] || 0
179
+ end
180
+
181
+ # The payload type. This returns the protocol buffers class name as a string.
182
+ def payload_type
183
+ @metadata.type
184
+ end
185
+
186
+ def queue_name
187
+ denomalized_queue_name
188
+ end
189
+
190
+ private
191
+
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
+ cummulative_delay = send(method, delay)
197
+ @on_requeue.call(cummulative_delay, current_requeue_number + 1)
198
+ EM.add_timer(cummulative_delay) do
199
+ block.call(cummulative_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(cummulative_delay, current_requeue_number)
206
+ end
207
+ end
208
+
209
+ def exponential_no_initial_delay_strategy(delay)
210
+ delay * (2 ** current_requeue_number - 1)
211
+ end
212
+
213
+ def exponential_strategy(delay)
214
+ delay * (2 ** current_requeue_number)
215
+ end
216
+
217
+ def linear_strategy(delay)
218
+ delay * (current_requeue_number + 1)
219
+ end
220
+
221
+ def denomalized_queue_name
222
+ @receiver.denomalized_queue_name
223
+ end
224
+
225
+ def normalised_queue_name
226
+ @receiver.queue_name
227
+ end
228
+
229
+ def increment_requeue_count(headers)
230
+ headers.tap do |m|
231
+ m['requeue'] = (m['requeue']) ? m['requeue'] + 1 : 1
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end