pipeline_toolkit 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/README.rdoc +54 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/bin/msg_generator.rb +36 -0
- data/bin/msg_probe.rb +13 -0
- data/bin/msg_push.rb +25 -0
- data/bin/msg_sink.rb +11 -0
- data/bin/msg_subscribe.rb +21 -0
- data/lib/pipeline_toolkit.rb +8 -0
- data/lib/pipeline_toolkit/default_logger.rb +18 -0
- data/lib/pipeline_toolkit/message_coder.rb +19 -0
- data/lib/pipeline_toolkit/message_command.rb +97 -0
- data/lib/pipeline_toolkit/message_probe.rb +107 -0
- data/lib/pipeline_toolkit/message_pusher.rb +54 -0
- data/lib/pipeline_toolkit/message_sink.rb +8 -0
- data/lib/pipeline_toolkit/message_subscriber.rb +138 -0
- data/lib/pipeline_toolkit/open_hash.rb +24 -0
- data/monitor/munin.rb +91 -0
- data/pipeline_toolkit.gemspec +65 -0
- metadata +107 -0
data/README.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
= Pipeline Toolkit
|
2
|
+
by VisFleet
|
3
|
+
|
4
|
+
Command line tools for processing messages by constructing a pipeline of workers. AMQP and Unix pipes are used to construct the pipeline. Messages are simple Hashs (serialized as YAML) so they can hold any values and change throughout the processing.
|
5
|
+
|
6
|
+
Provides:
|
7
|
+
- Processing acknowledgments, ensuring the a message is only disposed of once it has been successful processed
|
8
|
+
- Performance. Messages are moved through the pipeline fast
|
9
|
+
- Command line tools for:
|
10
|
+
- Subscribing to messages from an AMQP queue
|
11
|
+
- Pushing messages back onto an AMQP exchange
|
12
|
+
- Monitoring performance (see msg_probe)
|
13
|
+
- A base module (MessageCommand) to include into your own classes to quickly make workers.
|
14
|
+
|
15
|
+
== Install
|
16
|
+
|
17
|
+
> gem sources -a http://gems.github.com
|
18
|
+
> sudo gem install visfleet-pipeline_toolkit
|
19
|
+
|
20
|
+
=== Dependancies
|
21
|
+
|
22
|
+
It is assumed that you have:
|
23
|
+
- An AMQP msg server to pop and push messages to (e.g. http://www.rabbitmq.com/)
|
24
|
+
- A *nix system, such as Linux or Mac OS X.
|
25
|
+
|
26
|
+
== Usage
|
27
|
+
|
28
|
+
1. Create your worker
|
29
|
+
|
30
|
+
class MyWorker
|
31
|
+
include MessageCommand
|
32
|
+
|
33
|
+
def process_message(msg)
|
34
|
+
# do stuff here
|
35
|
+
msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
MyWorker.new.start
|
39
|
+
|
40
|
+
2. Hook it up to the main pipe (i.e. the AMQP server)
|
41
|
+
|
42
|
+
> msg_subscribe.rb -q source | my_worker.rb | msg_push.rb -x dest
|
43
|
+
|
44
|
+
You can learn more about the command line tools and what options are available by using their help command.
|
45
|
+
|
46
|
+
> msg_subscriber --help
|
47
|
+
> msg_push --help
|
48
|
+
> msg_sink --help
|
49
|
+
> msg_probe --help
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "pipeline_toolkit"
|
8
|
+
gem.summary = %Q{Toolkit for building processing pipelines using Unix Pipes and AMQP messages}
|
9
|
+
gem.email = "labs@visfleet.com"
|
10
|
+
gem.homepage = "http://github.com/visfleet/pipeline_toolkit"
|
11
|
+
gem.authors = ["Aisha Fenton"]
|
12
|
+
gem.executables = ["msg_probe.rb", "msg_subscribe.rb", "msg_push.rb", "msg_sink.rb", "msg_generator.rb"]
|
13
|
+
gem.add_runtime_dependency('amqp', ">=0.6.4")
|
14
|
+
gem.add_runtime_dependency('trollop', ">=1.14")
|
15
|
+
gem.add_runtime_dependency('eventmachine', ">=0.12.8")
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :build
|
24
|
+
|
25
|
+
require 'rake/rdoctask'
|
26
|
+
Rake::RDocTask.new do |rdoc|
|
27
|
+
if File.exist?('VERSION.yml')
|
28
|
+
config = YAML.load(File.read('VERSION.yml'))
|
29
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
30
|
+
else
|
31
|
+
version = ""
|
32
|
+
end
|
33
|
+
|
34
|
+
rdoc.rdoc_dir = 'rdoc'
|
35
|
+
rdoc.title = "pipeline_toolkit #{version}"
|
36
|
+
rdoc.rdoc_files.include('README*')
|
37
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.1
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
require 'pipeline_toolkit'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
banner <<-EOS
|
8
|
+
Generates messages for testing the pipeline
|
9
|
+
Usage:
|
10
|
+
msg_generator.rb -m "My message"
|
11
|
+
EOS
|
12
|
+
opt :msg, "The message to send", :short => "m", :default => "Test message"
|
13
|
+
opt :delay, "Sleep time between sends in seconds", :short => "d", :type => :float
|
14
|
+
end
|
15
|
+
|
16
|
+
class MsgGenerator
|
17
|
+
|
18
|
+
def initialize(opts)
|
19
|
+
@delay = opts.delay
|
20
|
+
@msg = opts.msg
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
loop do
|
25
|
+
self.send_msg(@msg)
|
26
|
+
sleep(@delay) unless @delay.nil?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_msg(msg)
|
31
|
+
puts MessageCoder.encode(@msg)
|
32
|
+
$stdout.flush
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
MsgGenerator.new(opts).start
|
data/bin/msg_probe.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'pipeline_toolkit'
|
6
|
+
|
7
|
+
opts = Trollop::options do
|
8
|
+
opt :interval, "Time in seconds between updates", :short => "i", :default => 2
|
9
|
+
opt :http_port, "The HTTP server port for accessing the stats", :default => 9070
|
10
|
+
opt :name, "The name of the probe. Used in monitoring", :type => :string, :short => 'n'
|
11
|
+
end
|
12
|
+
|
13
|
+
MessageProbe.new(opts).start
|
data/bin/msg_push.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'pipeline_toolkit'
|
6
|
+
|
7
|
+
opts = Trollop::options do
|
8
|
+
opt :exchanges, "The destination exchange(s)", :short => "x", :type => :strings
|
9
|
+
opt :key_eval, "A string of ruby code that is evaluated to produce a routing key for a given message.
|
10
|
+
By default each message has no routing key", :short => "e", :type => :string
|
11
|
+
opt :key_file, "A ruby file that gets included which contains a custom route_key method", :short => "f", :type => :string
|
12
|
+
|
13
|
+
# Msg server
|
14
|
+
opt :host, "The AMQP message server host", :default => "localhost"
|
15
|
+
opt :port, "The AMQP message server port", :default => "5672"
|
16
|
+
opt :user, "The AMQP message server username", :default => "guest"
|
17
|
+
opt :pass, "The AMQP message server username", :default => "guest"
|
18
|
+
opt :vhost, "The AMQP message server vhost", :default => "/"
|
19
|
+
end
|
20
|
+
|
21
|
+
mp = MessagePusher.new(opts)
|
22
|
+
# FIXME. Should be in MessageCommand class
|
23
|
+
Signal.trap('INT') { mp.stop }
|
24
|
+
Signal.trap('TERM'){ mp.stop }
|
25
|
+
mp.start
|
data/bin/msg_sink.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
require 'pipeline_toolkit'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :exchange, "The exchange to subscribe to", :short => "x", :type => :string
|
8
|
+
opt :queue, "The source queue", :short => "q", :type => :string
|
9
|
+
opt :ack, "Switch that requires that messages are successfully processed before continuing with the next message", :short => "a"
|
10
|
+
opt :topic, "The queue topic to subscribe to", :short => "t", :type => :string
|
11
|
+
opt :max_unackd, "The maximum number of unaknowledged messages to buffer before waiting for them to be acknowledged. Defaults to 100", :default => 100
|
12
|
+
|
13
|
+
# Msg server
|
14
|
+
opt :host, "The AMQP message server host", :default => "localhost"
|
15
|
+
opt :port, "The AMQP message server port", :default => "5672"
|
16
|
+
opt :user, "The AMQP message server username", :default => "guest"
|
17
|
+
opt :pass, "The AMQP message server username", :default => "guest", :short => "w"
|
18
|
+
opt :vhost, "The AMQP message server vhost", :default => "/"
|
19
|
+
end
|
20
|
+
|
21
|
+
ms = MessageSubscriber.new(opts).start
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "pipeline_toolkit/default_logger"
|
2
|
+
require "pipeline_toolkit/message_coder"
|
3
|
+
require "pipeline_toolkit/message_command"
|
4
|
+
require "pipeline_toolkit/message_probe"
|
5
|
+
require "pipeline_toolkit/message_pusher"
|
6
|
+
require "pipeline_toolkit/message_subscriber"
|
7
|
+
require "pipeline_toolkit/message_sink"
|
8
|
+
require "pipeline_toolkit/open_hash"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "logger"
|
2
|
+
require 'syslog_logger'
|
3
|
+
|
4
|
+
# OPTIMIZE. Is there a better way to handle default/common/shared logging?
|
5
|
+
module DefaultLogger
|
6
|
+
|
7
|
+
# Return the logger for this class
|
8
|
+
def log
|
9
|
+
@logger ||= SyslogLogger.new(self.syslog_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def syslog_name
|
13
|
+
"pipeline-" + Process.ppid.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
# Encodes messages
|
3
|
+
#
|
4
|
+
class MessageCoder
|
5
|
+
|
6
|
+
def self.encode(msg)
|
7
|
+
# NB: Using Marshal here because it's 9-10x faster than to_yaml
|
8
|
+
# See http://gist.github.com/190849
|
9
|
+
str = Marshal.dump(msg)
|
10
|
+
str.gsub!("\n", '--\\n')
|
11
|
+
str
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.decode(str)
|
15
|
+
str.gsub!('--\\n', "\n")
|
16
|
+
Marshal.load(str)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "eventmachine"
|
3
|
+
|
4
|
+
module MessageCommand
|
5
|
+
include DefaultLogger
|
6
|
+
|
7
|
+
attr_reader :sys_pipe
|
8
|
+
|
9
|
+
def start
|
10
|
+
log.info("starting")
|
11
|
+
|
12
|
+
Signal.trap('INT') { EM.stop }
|
13
|
+
Signal.trap('TERM'){ EM.stop }
|
14
|
+
|
15
|
+
@ack_buffer ||= ""
|
16
|
+
|
17
|
+
begin
|
18
|
+
EM.run do
|
19
|
+
self.init_loop
|
20
|
+
conn = EM.watch($stdin, ProcessLine, self)
|
21
|
+
conn.notify_readable = true
|
22
|
+
end
|
23
|
+
rescue StandardError => e
|
24
|
+
log.info e
|
25
|
+
raise e
|
26
|
+
ensure
|
27
|
+
self.shutdown
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def shutdown
|
32
|
+
log.info("shutting down")
|
33
|
+
@sys_pipe && @sys_pipe.close
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_line(line)
|
37
|
+
msg = MessageCoder.decode(line)
|
38
|
+
|
39
|
+
case msg[:msg_type]
|
40
|
+
when :system
|
41
|
+
result = process_system(msg)
|
42
|
+
else
|
43
|
+
result = process_message(msg)
|
44
|
+
end
|
45
|
+
|
46
|
+
case result
|
47
|
+
when :ack
|
48
|
+
self.ack_msg(msg)
|
49
|
+
else
|
50
|
+
self.pass_on_msg(result)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def ack_msg(msg)
|
55
|
+
return unless @use_ack
|
56
|
+
@sys_pipe.syswrite(msg.ack_id + "\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def pass_on_msg(msg)
|
60
|
+
$stdout.syswrite(MessageCoder.encode(msg) << "\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Override in included class. Provides a chance to initialize any
|
64
|
+
# code that needs to take place once the EM loop has started.
|
65
|
+
def init_loop
|
66
|
+
# Implemented in class that includes me
|
67
|
+
end
|
68
|
+
|
69
|
+
# Override in included class. Processes a message. This method
|
70
|
+
# must return either a msg object -- which may or may not have been modified -- or the symbol :ack.
|
71
|
+
# Returning :ack mean that the message has been dealt with and can be acknowledged back to the queue
|
72
|
+
# server. All messages must be acknowledged by at least one message_command.
|
73
|
+
def process_message(msg)
|
74
|
+
# Implemented in class that includes me
|
75
|
+
msg
|
76
|
+
end
|
77
|
+
|
78
|
+
def process_system(msg)
|
79
|
+
@sys_pipe = File.open(msg.sys_pipe, "w")
|
80
|
+
@use_ack = msg.use_ack
|
81
|
+
@max_unackd = msg.max_unackd
|
82
|
+
msg
|
83
|
+
end
|
84
|
+
|
85
|
+
module ProcessLine
|
86
|
+
include DefaultLogger
|
87
|
+
|
88
|
+
def initialize(msg_command)
|
89
|
+
@msg_command = msg_command
|
90
|
+
end
|
91
|
+
|
92
|
+
def notify_readable
|
93
|
+
@msg_command.process_line(@io.gets)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'evma_httpserver'
|
2
|
+
|
3
|
+
class MessageProbe
|
4
|
+
include MessageCommand
|
5
|
+
attr_reader :mps, :uptime, :name
|
6
|
+
|
7
|
+
def initialize(opts)
|
8
|
+
@interval = opts.interval
|
9
|
+
@http_port= opts.http_port
|
10
|
+
@start_time = Time.now
|
11
|
+
@name = opts.name
|
12
|
+
self.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_loop
|
16
|
+
EM.start_server('0.0.0.0', @http_port, ProbeHttpRequest, self)
|
17
|
+
EM.add_periodic_timer(@interval) { self.tick }
|
18
|
+
end
|
19
|
+
|
20
|
+
def tick
|
21
|
+
@time_delta = Time.now - @prev_time
|
22
|
+
@mps = @count / @time_delta
|
23
|
+
|
24
|
+
self.reset
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
@count = 0
|
29
|
+
@prev_time = Time.now
|
30
|
+
end
|
31
|
+
|
32
|
+
def uptime
|
33
|
+
Time.now - @start_time
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_message(msg)
|
37
|
+
@count += 1
|
38
|
+
msg
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class ProbeHttpRequest < EM::Connection
|
44
|
+
include EM::HttpServer
|
45
|
+
|
46
|
+
def initialize(probe)
|
47
|
+
@probe = probe
|
48
|
+
end
|
49
|
+
|
50
|
+
def post_init
|
51
|
+
super
|
52
|
+
no_environment_strings
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_http_request
|
56
|
+
response = EM::DelegatedHttpResponse.new(self)
|
57
|
+
response.status = 200
|
58
|
+
response.content_type 'text/html'
|
59
|
+
response.content = <<-EOL
|
60
|
+
<html>
|
61
|
+
<head>
|
62
|
+
<title>Message Probe</title>
|
63
|
+
<style type="text/css">
|
64
|
+
body {
|
65
|
+
background: black;
|
66
|
+
color: #80c0c0;
|
67
|
+
}
|
68
|
+
h1 {
|
69
|
+
font: 12pt Monospace;
|
70
|
+
text-align:center;
|
71
|
+
}
|
72
|
+
table {
|
73
|
+
font: 10pt Monospace;
|
74
|
+
margin-left:auto;
|
75
|
+
margin-right:auto;
|
76
|
+
text-align:right;
|
77
|
+
}
|
78
|
+
.page {
|
79
|
+
position:relative;
|
80
|
+
top: 20%;
|
81
|
+
# border-style:solid;
|
82
|
+
# border-width:5px;
|
83
|
+
width: 30%;
|
84
|
+
margin-left:auto;
|
85
|
+
margin-right:auto;
|
86
|
+
}
|
87
|
+
</style>
|
88
|
+
</head>
|
89
|
+
<body>
|
90
|
+
<div class=page>
|
91
|
+
<h1><span class="name">#{@probe.name}</span></h1>
|
92
|
+
<table>
|
93
|
+
<tr>
|
94
|
+
<td>messages per second:</td><td><span class="mps">#{@probe.mps}</span></td><td></td>
|
95
|
+
</tr>
|
96
|
+
<tr>
|
97
|
+
<td>uptime:</td><td><span class="uptime">#{@probe.uptime.to_i / 60}</span></td><td>mins</td
|
98
|
+
</tr>
|
99
|
+
</table>
|
100
|
+
</div>
|
101
|
+
</body>
|
102
|
+
</html>
|
103
|
+
EOL
|
104
|
+
|
105
|
+
response.send_response
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "mq"
|
2
|
+
|
3
|
+
class MessagePusher
|
4
|
+
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 load_route(key_file)
|
31
|
+
require key_file
|
32
|
+
self.extend eval(classify(key_file.gsub(".rb", "")))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Turn path Class or Module name (i.e. strip directories and turn into camel-case)
|
36
|
+
def classify(str)
|
37
|
+
str.gsub(/^.*\//, '').gsub(".rb","").gsub(/(?:^|_)(.)/) { $1.upcase }
|
38
|
+
end
|
39
|
+
|
40
|
+
# is overriden by included fork_file if specified
|
41
|
+
def route_key(msg)
|
42
|
+
@key_eval ? eval(@key_eval) : nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_message(msg)
|
46
|
+
@exchanges.each do |exchange|
|
47
|
+
key = route_key(msg)
|
48
|
+
exchange.publish(msg.to_yaml, :routing_key => key)
|
49
|
+
# OPTIMIZE. Using MessageCoder.encode(msg) instead of to_yaml is 2x faster. But won't be easy to debug. Worth it?
|
50
|
+
end
|
51
|
+
:ack
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "mq"
|
3
|
+
require "time"
|
4
|
+
require "socket"
|
5
|
+
|
6
|
+
class MessageSubscriber
|
7
|
+
include DefaultLogger
|
8
|
+
|
9
|
+
PIPE_PATH = "/tmp"
|
10
|
+
|
11
|
+
def initialize(opts)
|
12
|
+
@exchange_name, @exchange_type = opts[:exchange].split(":")
|
13
|
+
@queue_name = opts[:queue]
|
14
|
+
@use_ack = opts[:ack]
|
15
|
+
@topic = opts[:topic]
|
16
|
+
@msg_server_opts = opts.select_keys(:host, :port, :user, :pass, :vhost)
|
17
|
+
@unackd_msgs = {}
|
18
|
+
@max_unackd = opts[:max_unackd]
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
Signal.trap('INT') { AMQP.stop{ EM.stop } }
|
23
|
+
Signal.trap('TERM'){ AMQP.stop{ EM.stop } }
|
24
|
+
|
25
|
+
begin
|
26
|
+
self.create_sys_pipe
|
27
|
+
AMQP.start(@msg_server_opts) do
|
28
|
+
# For ack to work appropriatly you must shutdown AMQP gracefully,
|
29
|
+
# otherwise all items in your queue will be returned
|
30
|
+
# FIXME. Doesn't shut down cleanly with these commands included. Why?
|
31
|
+
|
32
|
+
self.setup_queue
|
33
|
+
# NB. prefetch limits the amount of unknowledged messages that come down the pipe.
|
34
|
+
MQ.prefetch(@max_unackd)
|
35
|
+
@queue.subscribe(:ack => @use_ack) do |header, body|
|
36
|
+
self.process_msg(header, body)
|
37
|
+
end
|
38
|
+
|
39
|
+
EM.attach(@sys_pipe, HandleAcks, @sys_pipe, @unackd_msgs)
|
40
|
+
end
|
41
|
+
rescue StandardError => e
|
42
|
+
log.info e
|
43
|
+
raise e
|
44
|
+
ensure
|
45
|
+
self.shutdown
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def shutdown
|
50
|
+
log.info "Shutting down"
|
51
|
+
self.destroy_sys_pipe
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_queue
|
55
|
+
# If a queue_name is given then we treat the queue as fixed, otherwise as temporary
|
56
|
+
@queue = @queue_name ? MQ.queue(@queue_name, :durable => true) :
|
57
|
+
self.generate_temporary_queue
|
58
|
+
if @exchange_name
|
59
|
+
create_exchange(@exchange_name, (@exchange_type || :fanout))
|
60
|
+
log.info("Binding to exchange:#{@exchange_str} #{@topic ? "using topic:" + @topic : ""}")
|
61
|
+
@queue.bind(@exchange_str, :key => @topic)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_exchange(name, type)
|
66
|
+
MQ::Exchange.new(MQ.default, type.to_sym, @exchange_name, :durable => true, :passive => false)
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_sys_pipe
|
70
|
+
log.debug("creating sys-pipe")
|
71
|
+
name = File.join(PIPE_PATH, "sys_pipe_#{self.generate_guid}")
|
72
|
+
`mkfifo #{name}`
|
73
|
+
@sys_pipe = File.new(name, "r+")
|
74
|
+
$stdout.puts(MessageCoder.encode({:msg_type => :system,
|
75
|
+
:sys_pipe => name,
|
76
|
+
:use_ack => @use_ack,
|
77
|
+
:max_unackd => @max_unackd}))
|
78
|
+
$stdout.flush
|
79
|
+
end
|
80
|
+
|
81
|
+
def destroy_sys_pipe
|
82
|
+
`rm #{@sys_pipe.path}`
|
83
|
+
end
|
84
|
+
|
85
|
+
def generate_temporary_queue
|
86
|
+
qname = "#{Socket.gethostname}_#{self.generate_guid}"
|
87
|
+
log.debug("Binding temporary queue #{qname}")
|
88
|
+
MQ.queue(qname, :auto_delete => true, :durable => false)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Generates a guid. Stolen from EM.
|
92
|
+
def generate_guid
|
93
|
+
# Cache uuidgen seed for better performance
|
94
|
+
if @ix and @ix >= 10_000
|
95
|
+
@ix = nil
|
96
|
+
@seed = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# NB. This will only work on *nix platforms
|
100
|
+
@seed ||= `uuidgen`.chomp.gsub(/-/,"")
|
101
|
+
@ix ||= 0
|
102
|
+
|
103
|
+
"#{@seed}#{@ix += 1}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def process_msg(header, body)
|
107
|
+
msg = YAML.load(body)
|
108
|
+
store_ack(msg, header) if @use_ack
|
109
|
+
write_msg(msg)
|
110
|
+
end
|
111
|
+
|
112
|
+
def write_msg(msg)
|
113
|
+
$stdout.syswrite(MessageCoder.encode(msg) << "\n")
|
114
|
+
end
|
115
|
+
|
116
|
+
def store_ack(msg, header)
|
117
|
+
msg.ack_id = header.delivery_tag.to_s
|
118
|
+
@unackd_msgs[msg.ack_id] = header
|
119
|
+
end
|
120
|
+
|
121
|
+
# Handles msg acks
|
122
|
+
module HandleAcks
|
123
|
+
include DefaultLogger
|
124
|
+
|
125
|
+
def initialize(sys_pipe, unackd_msgs)
|
126
|
+
@sys_pipe = sys_pipe
|
127
|
+
@unackd_msgs = unackd_msgs
|
128
|
+
end
|
129
|
+
|
130
|
+
def notify_readable
|
131
|
+
ack_id = @sys_pipe.gets.chomp!
|
132
|
+
header = @unackd_msgs.delete(ack_id)
|
133
|
+
header.ack
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Stolen from
|
2
|
+
# http://github.com/karottenreibe/ohash/
|
3
|
+
module OpenHash
|
4
|
+
def method_missing(meth, *args)
|
5
|
+
method = meth.to_s
|
6
|
+
|
7
|
+
if method =~ %r{.+=$}
|
8
|
+
super unless args.length == 1
|
9
|
+
self[method[0...-1].to_sym] = args.first
|
10
|
+
else
|
11
|
+
self[meth]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def select_keys(*keys)
|
16
|
+
h = {}
|
17
|
+
self.each do |key, value|
|
18
|
+
h[key] = value if keys.include?(key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
Hash.send(:include, OpenHash)
|
data/monitor/munin.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'trollop'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
class MuninPlugin
|
8
|
+
|
9
|
+
def initialize(is_config, opts)
|
10
|
+
@probe_urls = opts.probe_urls
|
11
|
+
|
12
|
+
self.get_probes
|
13
|
+
|
14
|
+
if is_config
|
15
|
+
self.config
|
16
|
+
else
|
17
|
+
self.data
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_probes
|
22
|
+
@probes = []
|
23
|
+
@probe_urls.each_with_index do |url, idx|
|
24
|
+
@probes << get_probe(url)
|
25
|
+
end
|
26
|
+
@probes
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_probe(url)
|
30
|
+
probe = Probe.new
|
31
|
+
doc = Nokogiri::HTML(open(url))
|
32
|
+
probe.name = extract_mf_value(doc, "name")
|
33
|
+
probe.mps = extract_mf_value(doc, "mps")
|
34
|
+
probe.uptime = extract_mf_value(doc, "uptime")
|
35
|
+
probe
|
36
|
+
end
|
37
|
+
|
38
|
+
# Extracts the microformat value from the HTML doc
|
39
|
+
def extract_mf_value(doc, key)
|
40
|
+
doc.css(".#{key}").first.content
|
41
|
+
end
|
42
|
+
|
43
|
+
def config
|
44
|
+
config=<<-EOL
|
45
|
+
graph_title Message Probe Throughput
|
46
|
+
graph_vlabel messages per second
|
47
|
+
graph_category Message Probe
|
48
|
+
EOL
|
49
|
+
|
50
|
+
@probes.each do |probe|
|
51
|
+
config << "probe_#{probe.name}.label #{probe.name}\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
puts config
|
55
|
+
end
|
56
|
+
|
57
|
+
def data
|
58
|
+
data = ""
|
59
|
+
@probes.each do |probe|
|
60
|
+
data << "#{probe.name}.value #{probe.mps}\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
puts data
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class Probe
|
69
|
+
attr_accessor :name, :mps, :uptime
|
70
|
+
end
|
71
|
+
|
72
|
+
SUB_COMMANDS = %w{run config}
|
73
|
+
|
74
|
+
global_opts = Trollop::options do
|
75
|
+
banner <<-EOS
|
76
|
+
Munin plugin
|
77
|
+
Usage:
|
78
|
+
munin.rb [run|config] [options]
|
79
|
+
|
80
|
+
For more help run
|
81
|
+
munin.rb [command] --help
|
82
|
+
EOS
|
83
|
+
stop_on SUB_COMMANDS
|
84
|
+
end
|
85
|
+
|
86
|
+
is_config = (ARGV.shift == "config")
|
87
|
+
opts = Trollop::options do
|
88
|
+
opt :probe_urls, "The probe's URL", :short => "p", :type => :strings, :default => ["http://127.0.0.1:9070"]
|
89
|
+
end
|
90
|
+
|
91
|
+
MuninPlugin.new(is_config, opts)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{pipeline_toolkit}
|
8
|
+
s.version = "1.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aisha Fenton"]
|
12
|
+
s.date = %q{2009-12-06}
|
13
|
+
s.email = %q{labs@visfleet.com}
|
14
|
+
s.executables = ["msg_probe.rb", "msg_subscribe.rb", "msg_push.rb", "msg_sink.rb", "msg_generator.rb"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README.rdoc",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"bin/msg_generator.rb",
|
24
|
+
"bin/msg_probe.rb",
|
25
|
+
"bin/msg_push.rb",
|
26
|
+
"bin/msg_sink.rb",
|
27
|
+
"bin/msg_subscribe.rb",
|
28
|
+
"lib/pipeline_toolkit.rb",
|
29
|
+
"lib/pipeline_toolkit/default_logger.rb",
|
30
|
+
"lib/pipeline_toolkit/message_coder.rb",
|
31
|
+
"lib/pipeline_toolkit/message_command.rb",
|
32
|
+
"lib/pipeline_toolkit/message_probe.rb",
|
33
|
+
"lib/pipeline_toolkit/message_pusher.rb",
|
34
|
+
"lib/pipeline_toolkit/message_sink.rb",
|
35
|
+
"lib/pipeline_toolkit/message_subscriber.rb",
|
36
|
+
"lib/pipeline_toolkit/open_hash.rb",
|
37
|
+
"monitor/munin.rb",
|
38
|
+
"pipeline_toolkit.gemspec"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/visfleet/pipeline_toolkit}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{Toolkit for building processing pipelines using Unix Pipes and AMQP messages}
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
|
+
s.specification_version = 3
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
51
|
+
s.add_runtime_dependency(%q<amqp>, [">= 0.6.4"])
|
52
|
+
s.add_runtime_dependency(%q<trollop>, [">= 1.14"])
|
53
|
+
s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.8"])
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<amqp>, [">= 0.6.4"])
|
56
|
+
s.add_dependency(%q<trollop>, [">= 1.14"])
|
57
|
+
s.add_dependency(%q<eventmachine>, [">= 0.12.8"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<amqp>, [">= 0.6.4"])
|
61
|
+
s.add_dependency(%q<trollop>, [">= 1.14"])
|
62
|
+
s.add_dependency(%q<eventmachine>, [">= 0.12.8"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pipeline_toolkit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aisha Fenton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-06 00:00:00 +13:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: amqp
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.6.4
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: trollop
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "1.14"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: eventmachine
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.12.8
|
44
|
+
version:
|
45
|
+
description:
|
46
|
+
email: labs@visfleet.com
|
47
|
+
executables:
|
48
|
+
- msg_probe.rb
|
49
|
+
- msg_subscribe.rb
|
50
|
+
- msg_push.rb
|
51
|
+
- msg_sink.rb
|
52
|
+
- msg_generator.rb
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files:
|
56
|
+
- README.rdoc
|
57
|
+
files:
|
58
|
+
- .gitignore
|
59
|
+
- README.rdoc
|
60
|
+
- Rakefile
|
61
|
+
- VERSION
|
62
|
+
- bin/msg_generator.rb
|
63
|
+
- bin/msg_probe.rb
|
64
|
+
- bin/msg_push.rb
|
65
|
+
- bin/msg_sink.rb
|
66
|
+
- bin/msg_subscribe.rb
|
67
|
+
- lib/pipeline_toolkit.rb
|
68
|
+
- lib/pipeline_toolkit/default_logger.rb
|
69
|
+
- lib/pipeline_toolkit/message_coder.rb
|
70
|
+
- lib/pipeline_toolkit/message_command.rb
|
71
|
+
- lib/pipeline_toolkit/message_probe.rb
|
72
|
+
- lib/pipeline_toolkit/message_pusher.rb
|
73
|
+
- lib/pipeline_toolkit/message_sink.rb
|
74
|
+
- lib/pipeline_toolkit/message_subscriber.rb
|
75
|
+
- lib/pipeline_toolkit/open_hash.rb
|
76
|
+
- monitor/munin.rb
|
77
|
+
- pipeline_toolkit.gemspec
|
78
|
+
has_rdoc: true
|
79
|
+
homepage: http://github.com/visfleet/pipeline_toolkit
|
80
|
+
licenses: []
|
81
|
+
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: "0"
|
92
|
+
version:
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: "0"
|
98
|
+
version:
|
99
|
+
requirements: []
|
100
|
+
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 1.3.5
|
103
|
+
signing_key:
|
104
|
+
specification_version: 3
|
105
|
+
summary: Toolkit for building processing pipelines using Unix Pipes and AMQP messages
|
106
|
+
test_files: []
|
107
|
+
|