pipeline_toolkit 1.0.4 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENSE.markdown +20 -0
  2. data/README.markdown +98 -0
  3. data/bin/msg_generator +13 -0
  4. data/bin/msg_probe +13 -0
  5. data/bin/msg_push +13 -0
  6. data/bin/msg_sink +14 -0
  7. data/bin/msg_subscribe +13 -0
  8. data/lib/pipeline_toolkit.rb +17 -0
  9. data/lib/pipeline_toolkit/amqp/abstract.rb +95 -0
  10. data/lib/pipeline_toolkit/amqp/reader.rb +64 -0
  11. data/lib/pipeline_toolkit/amqp/writer.rb +54 -0
  12. data/lib/pipeline_toolkit/commands/msg_generator/cli.rb +45 -0
  13. data/lib/pipeline_toolkit/commands/msg_probe/cli.rb +46 -0
  14. data/lib/pipeline_toolkit/commands/msg_push/cli.rb +58 -0
  15. data/lib/pipeline_toolkit/commands/msg_sink/cli.rb +41 -0
  16. data/lib/pipeline_toolkit/commands/msg_subscribe/cli.rb +58 -0
  17. data/lib/pipeline_toolkit/default_logger.rb +126 -11
  18. data/lib/pipeline_toolkit/handlers/message_handler.rb +38 -0
  19. data/lib/pipeline_toolkit/message_coder.rb +18 -8
  20. data/lib/pipeline_toolkit/message_command.rb +138 -61
  21. data/lib/pipeline_toolkit/message_generator.rb +21 -0
  22. data/lib/pipeline_toolkit/message_probe.rb +6 -6
  23. data/lib/pipeline_toolkit/message_pusher.rb +51 -54
  24. data/lib/pipeline_toolkit/message_sink.rb +1 -1
  25. data/lib/pipeline_toolkit/message_subscriber.rb +182 -201
  26. data/lib/pipeline_toolkit/monitoring/monitor_server.rb +124 -0
  27. data/spec/eventmachine_helper.rb +44 -0
  28. data/spec/message_subscriber_spec.rb +64 -0
  29. data/spec/spec_helper.rb +15 -0
  30. metadata +202 -47
  31. data/.gitignore +0 -5
  32. data/README.rdoc +0 -70
  33. data/Rakefile +0 -40
  34. data/VERSION +0 -1
  35. data/bin/msg_generator.rb +0 -0
  36. data/bin/msg_probe.rb +0 -15
  37. data/bin/msg_push.rb +0 -25
  38. data/bin/msg_sink.rb +0 -11
  39. data/bin/msg_subscribe.rb +0 -27
  40. data/monitor/munin.rb +0 -91
  41. data/pipeline_toolkit.gemspec +0 -72
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ class MessageGenerator
4
+
5
+ def initialize(opts)
6
+ @delay = opts.delay
7
+ @msg = JSON.parse(opts.msg)
8
+ end
9
+
10
+ def start
11
+ loop do
12
+ self.send_msg(@msg)
13
+ sleep(@delay) unless @delay.nil?
14
+ end
15
+ end
16
+
17
+ def send_msg(msg)
18
+ # puts MessageCoder.encode(@msg)
19
+ $stdout.flush
20
+ end
21
+ end
@@ -13,16 +13,16 @@ class MessageProbe
13
13
  self.init_dnssd if opts.dnssd
14
14
  end
15
15
 
16
- def init_loop
17
- EM.start_server('0.0.0.0', @http_port, ProbeHttpRequest, self)
18
- EM.add_periodic_timer(@interval) { self.tick }
19
- end
20
-
21
16
  def init_dnssd
22
17
  require 'dnssd'
23
18
  DNSSD.register!("#{@name} probe", "_http._tcp", nil, @http_port)
24
19
  end
25
20
 
21
+ def initialize_machine
22
+ EM.start_server('0.0.0.0', @http_port, ProbeHttpRequest, self)
23
+ EM.add_periodic_timer(@interval) { self.tick }
24
+ end
25
+
26
26
  def tick
27
27
  @time_delta = Time.now - @prev_time
28
28
  @mps = @count / @time_delta
@@ -40,7 +40,7 @@ class MessageProbe
40
40
 
41
41
  # OPTIMIZE. can improve performance by overriding base process_line method
42
42
  # saving us the unnecessary marshal step.
43
- def process_message(msg)
43
+ def process_standard(msg)
44
44
  @count += 1
45
45
  msg
46
46
  end
@@ -1,61 +1,58 @@
1
1
  require "mq"
2
2
 
3
+ ##
4
+ # A Message Queue machine to handle messages that has to be published
5
+ # back into a message queue.
6
+ #
3
7
  class MessagePusher
4
8
  include MessageCommand
5
-
6
- def initialize(opts)
7
- @key_eval = opts.key_eval
8
- if opts.key_file
9
- @key_file = opts.key_file
10
- load_route(@key_file)
11
- self.init_route
12
- end
13
- @exchange_names = opts.exchanges.map { |str| str.split(":") }
14
- @msg_server_config = opts.select_keys(:host, :port, :user, :pass, :vhost)
15
- end
16
-
17
- def init_loop
18
- @msg_server = MQ.new(AMQP.connect(@msg_server_config))
19
- self.setup_exchanges
20
- end
21
-
22
- def setup_exchanges
23
- @exchanges = []
24
- @exchange_names.each do |name, type|
25
- type ||= :fanout
26
- @exchanges << MQ::Exchange.new(@msg_server, type.to_sym, name, :durable => true, :passive => false)
27
- end
28
- end
29
-
30
- def syspipe_open
31
- # Send back details of where messages are going
32
- msg = {:msg_type => :pipe_desc, :exchanges => @exchange_names.join(",")}
33
- self.sys_pipe.syswrite(MessageCoder.encode(msg) << "\n")
34
- end
35
-
36
- def load_route(key_file)
37
- require key_file
38
- self.extend eval(classify(key_file.gsub(".rb", "")))
39
- end
40
-
41
- # Turn path Class or Module name (i.e. strip directories and turn into camel-case)
42
- def classify(str)
43
- str.gsub(/^.*\//, '').gsub(".rb","").gsub(/(?:^|_)(.)/) { $1.upcase }
44
- end
45
-
46
- # is overriden by included fork_file if specified
47
- def route_key(msg)
48
- @key_eval ? eval(@key_eval) : nil
49
- end
50
-
51
- def process_message(msg)
52
- @exchanges.each do |exchange|
53
- key = route_key(msg)
54
- exchange.publish(msg.to_yaml)
55
- # OPTIMIZE. Using MessageCoder.encode(msg) instead of to_yaml is 2x faster. But won't be easy to
56
- # debug. Worth it?
57
- end
58
- :ack
9
+ include Amqp::Writer
10
+
11
+ ##
12
+ # Initializes a new intance
13
+ #
14
+ # @param options<Hash> Options hash for the message pusher.
15
+ #
16
+ def initialize(options = {})
17
+ super(options)
18
+ DefaultLogger.debug("MessagePusher#initialize(options = {})") if options[:env] == "development"
19
+
20
+ queues_string = options[:queues].map { |name, type| "#{name} (key:#{type})" }.join(",")
21
+ DefaultLogger.info "================================================================"
22
+ DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
23
+ DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
24
+ DefaultLogger.info "Queues: #{queues_string}"
25
+ DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
26
+ DefaultLogger.info ""
27
+ end
28
+
29
+ ##
30
+ # Initialize the AMQP server connection and exchange so we can write messages to the queue.
31
+ #
32
+ def initialize_machine
33
+ DefaultLogger.debug("MessagePusher#initialize_machine") if options[:env] == "development"
34
+ initialize_writer
35
+ initialize_queues
36
+ end
37
+
38
+ ##
39
+ # Notify back down the pipeline information about this machine.
40
+ #
41
+ def report_back
42
+ DefaultLogger.debug("MessagePusher#open_acknowledgement_pipe") if options[:env] == "development"
43
+ message = { :msg_type => :pipe_desc, :queues => queue_names.join(",") }
44
+ write_to_pipe(message, @ack_pipe)
45
+ end
46
+
47
+ ##
48
+ # Handle the messages by writing them into the AMQP server exchange.
49
+ #
50
+ # @param message<Hash> The message to write to the exchange.
51
+ #
52
+ def process_standard(message)
53
+ DefaultLogger.debug("MessagePusher#process_standard(message)") if options[:env] == "development"
54
+ publish(message)
55
+ acknowledge(message) if options[:ack]
59
56
  end
60
57
 
61
58
  end
@@ -1,7 +1,7 @@
1
1
  class MessageSink
2
2
  include MessageCommand
3
3
 
4
- def process_message(msg)
4
+ def process_standard(msg)
5
5
  :ack
6
6
  end
7
7
 
@@ -3,246 +3,227 @@ require "mq"
3
3
  require "time"
4
4
  require "socket"
5
5
 
6
+ ##
7
+ # The message subscriber is used to subscribe to a AMQP server queue
8
+ # and handle a single message at a time.
9
+ #
6
10
  class MessageSubscriber
7
- include DefaultLogger
8
11
 
9
- attr_reader :name, :start_time, :mps, :queue_name
10
- attr_accessor :exchanges_out
12
+ include Amqp::Reader
13
+
14
+ attr_reader :start_time
15
+ attr_reader :mps
16
+ attr_reader :options
17
+ attr_accessor :queues_out
11
18
 
12
19
  PIPE_PATH = "/tmp"
13
20
 
14
- def initialize(opts)
15
- @exchange_name, @exchange_type = opts[:exchange_bind].split(":")
16
- @queue_name = opts[:queue]
17
- @use_ack = opts[:ack]
18
- @topic = opts[:topic]
19
- @msg_server_opts = opts.select_keys(:host, :port, :user, :pass, :vhost)
20
- @unackd_msgs = {}
21
- @max_unackd = opts[:max_unackd]
22
-
23
- @http_port = opts.http_port || Socket.select_random_port(10_000, 11_000)
24
- @name = opts.name || (Socket.gethostname + '_' + Process.pid.to_s)
25
- self.init_dnssd if opts.dnssd
26
- self.reset
27
- end
28
-
29
- def init_dnssd
30
- # FIXME. DNS-SD breaks when under load. Not sure what problem is yet, have raised it with
31
- # gem author:
32
- # http://github.com/tenderlove/dnssd/issues#issue/3
33
- require 'dnssd'
34
- DNSSD.register!("#{@name}_pipe", "_http._tcp", nil, @http_port)
21
+ ##
22
+ # Initializes a new instance
23
+ #
24
+ # @param options<Hash> An options hash, see command line interface.
25
+ #
26
+ def initialize(options = {})
27
+
28
+ DefaultLogger.init_logger(options)
29
+
30
+ @options = options
31
+
32
+ @options[:http_port] ||= Socket.select_random_port(10_000, 11_000)
33
+ @options[:name] ||= (Socket.gethostname + '_' + Process.pid.to_s)
34
+
35
+ DefaultLogger.info "================================================================"
36
+ DefaultLogger.info "Booting #{self.class.name} (#{options[:env]})"
37
+ DefaultLogger.info "Exchange: #{options[:exchange]} (#{options[:type]} passive:#{options[:passive]} durable:#{options[:durable]})"
38
+ DefaultLogger.info "Queue: #{options[:queue]} (ack:#{options[:ack]})"
39
+ DefaultLogger.info "amqp://#{options[:user]}:#{options[:pass]}@#{options[:host]}:#{options[:port]}#{options[:vhost]}"
40
+ DefaultLogger.info "Monitoring: http://localhost:#{options[:http_port]}/ (#{options[:content_type]} name:#{options[:name]} DNS-SD:#{options[:dnssd]})"
41
+ DefaultLogger.info ""
42
+
43
+ @queues_out = ''
44
+
45
+ initialize_dnssd
46
+
47
+ reset_message_statistics
35
48
  end
36
49
 
50
+ ##
51
+ # Starts the subscriber reactor loop
52
+ #
37
53
  def start
38
- # FIXME. Need to do more investigation on if/when messages are lost with shutdowns
39
- Signal.trap('INT') { AMQP.stop{ EM.stop } }
40
- Signal.trap('TERM') { AMQP.stop{ EM.stop } }
41
54
 
55
+ Signal.trap('INT') { EM.stop }
56
+ Signal.trap('TERM') { EM.stop }
57
+
58
+ DefaultLogger.debug("MessageSubscriber#start")
42
59
  begin
43
- self.create_sys_pipe
44
- AMQP.start(@msg_server_opts) do
60
+ create_ack_pipe
61
+
62
+ EM.run do
45
63
  @start_time = Time.now
46
- self.setup_queue
47
- # NB. prefetch limits the amount of unknowledged messages that come down the pipe.
48
- MQ.prefetch(@max_unackd)
49
- @queue.subscribe(:ack => @use_ack) do |header, body|
50
- self.process_message(header, body)
51
- end
52
-
53
- conn = EM.watch(@sys_pipe, HandleCtlMessages, @sys_pipe, self)
64
+
65
+ initialize_reader
66
+ queue_subscribe
67
+
68
+ conn = EM.watch(@ack_pipe, Handlers::MessageHandler, self, options)
69
+ # must call this to setup callback to notify_readable
54
70
  conn.notify_readable = true
55
-
56
- EM.start_server('0.0.0.0', @http_port, PipeHttpRequest, self)
57
- EM.add_periodic_timer(5) { self.calc_stats }
71
+
72
+ EM.start_server('0.0.0.0', options[:http_port], Monitoring::MonitorServer, self, options)
73
+ EM.add_periodic_timer(5) { calculate_message_statistics }
58
74
  end
59
75
  rescue StandardError => e
60
- log.error e.message + " " + e.backtrace.join("\n")
76
+ DefaultLogger.error "#{e.class.name}: #{e.message}\n" << e.backtrace.join("\n")
61
77
  raise e
62
78
  ensure
63
- self.shutdown
79
+ shutdown
64
80
  end
65
81
  end
66
-
67
- def calc_stats
68
- @time_delta = Time.now - @prev_time
69
- @mps = @count / @time_delta
70
- self.reset
71
- end
72
-
73
- def reset
74
- @count = 0
75
- @prev_time = Time.now
76
- end
77
-
82
+
83
+ ##
84
+ # Stop the subscriber
85
+ #
78
86
  def shutdown
79
- log.info "Shutting down"
80
- self.destroy_sys_pipe
81
- end
82
-
83
- def setup_queue
84
- # If a queue_name is given then we treat the queue as fixed, otherwise as temporary
85
- @queue = @queue_name ? MQ.queue(@queue_name, :durable => true) :
86
- self.generate_temporary_queue
87
- if @exchange_name
88
- create_exchange(@exchange_name, (@exchange_type || :fanout))
89
- log.info("Binding to exchange:#{@exchange_str} #{@topic ? "using topic:" + @topic : ""}")
90
- @queue.bind(@exchange_str, :key => @topic)
87
+ DefaultLogger.info("Shutting down #{self.class.name}")
88
+ DefaultLogger.info "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"
89
+ destroy_ack_pipe
90
+ end
91
+
92
+ ##
93
+ # Callback for the Handlers::MessageHandler when it receives a message
94
+ #
95
+ # @param message<Hash> The decoded message
96
+ #
97
+ def process(message)
98
+ case message[:msg_type]
99
+ when :ack
100
+ perform_acknowledgement(message[:ack_id])
101
+ when :pipe_desc
102
+ DefaultLogger.debug("AcknowledgementHandler#queues_out")
103
+ @queues_out = message[:queues]
104
+ else
105
+ raise Exception.new("Unknown control message received: #{message}")
91
106
  end
92
107
  end
93
-
94
- def create_exchange(name, type)
95
- MQ::Exchange.new(MQ.default, type.to_sym, @exchange_name, :durable => true, :passive => false)
96
- end
97
-
98
- def create_sys_pipe
99
- log.debug("creating sys-pipe")
100
- name = File.join(PIPE_PATH, "sys_pipe_#{self.generate_guid}")
101
- `mkfifo #{name}`
102
- @sys_pipe = File.new(name, "r+")
103
- $stdout.puts(MessageCoder.encode({:msg_type => :system,
104
- :sys_pipe => name,
105
- :use_ack => @use_ack,
106
- :max_unackd => @max_unackd}))
107
- $stdout.flush
108
- end
109
-
110
- def destroy_sys_pipe
111
- `rm #{@sys_pipe.path}`
112
- end
113
108
 
114
- def generate_temporary_queue
115
- qname = "#{Socket.gethostname}_#{self.generate_guid}"
116
- log.debug("Binding temporary queue #{qname}")
117
- MQ.queue(qname, :auto_delete => true, :durable => false)
109
+ ##
110
+ # Support templating of member data.
111
+ #
112
+ def get_binding
113
+ binding
118
114
  end
119
115
 
120
- # Generates a guid. Stolen from EM.
121
- def generate_guid
122
- # Cache uuidgen seed for better performance
123
- if @ix and @ix >= 10_000
124
- @ix = nil
125
- @seed = nil
126
- end
127
-
128
- # NB. This will only work on *nix platforms
129
- @seed ||= `uuidgen`.chomp.gsub(/-/,"")
130
- @ix ||= 0
131
-
132
- "#{@seed}#{@ix += 1}"
133
- end
134
-
135
- def process_message(header, body)
136
- msg = YAML.load(body)
137
- store_ack(msg, header) if @use_ack
138
- write_msg(msg)
116
+ # :nodoc:
117
+ def name
118
+ options[:name] || ''
139
119
  end
140
120
 
141
- def write_msg(msg)
142
- @count += 1
143
- $stdout.syswrite(MessageCoder.encode(msg) << "\n")
121
+ # :nodoc:
122
+ def queue_name
123
+ options[:name] || ''
144
124
  end
145
125
 
146
- def perform_ack(ack_id)
147
- header = @unackd_msgs.delete(ack_id)
148
- header.ack
149
- end
126
+ protected
150
127
 
151
- def store_ack(msg, header)
152
- msg.ack_id = header.delivery_tag.to_s
153
- @unackd_msgs[msg.ack_id] = header
128
+ ##
129
+ # Process the message received from AMQP server
130
+ #
131
+ # @param header<MQ::Header> The header of the message
132
+ # @param body Description
133
+ #
134
+ def process_queue_message(header, body)
135
+ DefaultLogger.debug("MessageSubscriber#process_queue_message(header, body)")
136
+ unless body.is_a?(Hash)
137
+ message = { :msg_type => :standard, :raw => body }
138
+ else
139
+ message = { :msg_type => :standard }.merge(body)
140
+ end
141
+ store_acknowledgement(message, header) if options[:ack]
142
+ write(message)
154
143
  end
144
+
145
+ private
146
+
147
+ ##
148
+ # Reset the instance variables used to store message statistics
149
+ #
150
+ def reset_message_statistics
151
+ @count = 0
152
+ @prev_time = Time.now
153
+ end
154
+
155
+ ##
156
+ # Calculate the statistics on message handling
157
+ #
158
+ def calculate_message_statistics
159
+ @time_delta = Time.now - @prev_time
160
+ @mps = @count / @time_delta
161
+ reset_message_statistics
162
+ end
155
163
 
156
- # Handles msg acks
157
- module HandleCtlMessages
158
- include DefaultLogger
159
-
160
- def initialize(sys_pipe, msg_sub)
161
- @sys_pipe = sys_pipe
162
- @msg_sub = msg_sub
164
+ ##
165
+ # Create the acknowledgement named pipe
166
+ #
167
+ def create_ack_pipe
168
+ name = File.join(PIPE_PATH, "sys_pipe_#{generate_guid}")
169
+ `mkfifo #{name}`
170
+ @ack_pipe = File.new(name, "r+")
171
+ message = { :msg_type => :system,
172
+ :sys_pipe => name,
173
+ :ack => options[:ack] }
174
+ write_to_pipe(message)
175
+ end
176
+
177
+ ##
178
+ # Destroy the acknowledgement named pipe
179
+ #
180
+ def destroy_ack_pipe
181
+ `rm #{@ack_pipe.path}`
163
182
  end
164
183
 
165
- def notify_readable
166
- msg = MessageCoder.decode(@sys_pipe.gets.chomp!)
167
- case msg[:msg_type]
168
- when :ack
169
- @msg_sub.perform_ack(msg.ack_id)
170
- when :pipe_desc
171
- @msg_sub.exchanges_out = msg.exchanges
172
- else
173
- raise Exception.new("Unknown control message received: #{message}")
184
+ ##
185
+ # Generates a unique guid to use as part of the acknowledgement named pipe name.
186
+ # Stolen from EM.
187
+ #
188
+ def generate_guid
189
+ # Cache uuidgen seed for better performance
190
+ if @ix and @ix >= 10_000
191
+ @ix = nil
192
+ @seed = nil
174
193
  end
175
- end
176
- end
177
194
 
178
- class PipeHttpRequest < EM::Connection
179
- include EM::HttpServer
195
+ # NB. This will only work on *nix platforms
196
+ @seed ||= `uuidgen`.chomp.gsub(/-/,"")
197
+ @ix ||= 0
180
198
 
181
- def initialize(msg_sub)
182
- @msg_sub = msg_sub
199
+ "#{@seed}#{@ix += 1}"
183
200
  end
184
201
 
185
- def post_init
186
- super
187
- no_environment_strings
202
+ ##
203
+ # Write the message to STDOUT
204
+ #
205
+ # @param message<Hash> The message to write
206
+ #
207
+ def write(message)
208
+ DefaultLogger.debug("MessageSubscriber#write(message)")
209
+ @count += 1
210
+ write_to_pipe(message)
188
211
  end
189
212
 
190
- def process_http_request
191
- response = EM::DelegatedHttpResponse.new(self)
192
- response.status = 200
193
- response.content_type 'text/html'
194
- response.content = <<-EOL
195
- <html>
196
- <head>
197
- <title>Message Probe</title>
198
- <style type="text/css">
199
- body {
200
- background: black;
201
- color: #80c0c0;
202
- }
203
- h1 {
204
- font: 12pt Monospace;
205
- text-align:center;
206
- }
207
- table {
208
- font: 10pt Monospace;
209
- margin-left:auto;
210
- margin-right:auto;
211
- text-align:right;
212
- }
213
- .page {
214
- position:relative;
215
- top: 20%;
216
- # border-style:solid;
217
- # border-width:5px;
218
- width: 30%;
219
- margin-left:auto;
220
- margin-right:auto;
221
- }
222
- </style>
223
- </head>
224
- <body>
225
- <div class=page>
226
- <h1><span class="name">#{@msg_sub.name}</span></h1>
227
- <table>
228
- <tr>
229
- <td>Structure:</td><td><span class="queue_in">#{@msg_sub.queue_name}</span> -> <span class="exchanges_out">#{@msg_sub.exchanges_out}</span></td>
230
- </tr>
231
- <tr>
232
- <td>Throughput:</td><td><span class="mps">#{@msg_sub.mps.to_i}</span></td>
233
- </tr>
234
- <tr>
235
- <td>Uptime:</td><td><span class="uptime">#{(Time.now - @msg_sub.start_time).to_i / 60}mins</span></td>
236
- </tr>
237
- </table>
238
- </div>
239
- </body>
240
- </html>
241
- EOL
242
-
243
- response.send_response
213
+ ##
214
+ # Initialize the DNS Service Discovery (aka Bonjour, MDNS).
215
+ #
216
+ def initialize_dnssd
217
+ return unless options[:dnssd]
218
+ # FIXME. DNS-SD breaks when under load. Not sure what problem is yet, have raised it with
219
+ # gem author:
220
+ # http://github.com/tenderlove/dnssd/issues#issue/3
221
+ require 'dnssd'
222
+ DNSSD.register!("#{options[:name]}_pipe", "_http._tcp", nil, options[:http_port])
244
223
  end
245
- end
246
-
247
- end
248
224
 
225
+ def write_to_pipe(message, pipe=$stdout)
226
+ pipe.syswrite(MessageCoder.encode(message) << "\n")
227
+ end
228
+
229
+ end