pipeline_toolkit 1.0.4 → 1.0.6

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 (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