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
data/LICENSE.markdown ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Visfleet Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,98 @@
1
+ # Pipeline Toolkit #
2
+ by VisFleet
3
+
4
+ Command line tools for processing messages by constructing a pipeline of workers. [AMQP](http://amqp.rubyforge.org/ "AMQP") and Unix pipes are used to construct the pipeline. Messages are simple Hashes (serialized) so they can hold any values and change throughout the processing.
5
+
6
+ Provides:
7
+
8
+ * Processing acknowledgments, ensuring the a message is only disposed of once it has been successful processed
9
+ * Performance. Messages are moved through the pipeline fast
10
+ * Command line tools for:
11
+ * Subscribing to messages from an [AMQP queue](http://amqp.rubyforge.org/classes/MQ/Queue.html "MQ::Queue")
12
+ * Pushing messages back onto an [AMQP exchange](http://amqp.rubyforge.org/classes/MQ/Exchange.html "MQ::Exchange")
13
+ * Monitoring performance (see msg_probe)
14
+ * A base module ({MessageCommand}) to include into your own classes to quickly make workers.
15
+
16
+ ## Install ##
17
+
18
+ > (sudo) gem install pipeline_toolkit
19
+
20
+ ### Dependancies ###
21
+
22
+ It is assumed that you have:
23
+ - An [AMQP](http://amqp.rubyforge.org/ "AMQP") message 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 MyMachine
31
+ include MessageCommand
32
+
33
+ def initialize_machine
34
+ # Optional: setup any dependencies required by your machine
35
+ end
36
+
37
+ def report_back
38
+ # Optional: write information about the pipeline back
39
+ # through the acknowledgement pipe
40
+ end
41
+
42
+ def process_standard(message)
43
+ # Required: handle the message within your machine
44
+ unless failed_to_process?
45
+ pass_on(message) # or alternatively acknowledge if this
46
+ # is the last machine in the pipeline.
47
+ else
48
+ # handle the failed message here (however you deem fit)
49
+ acknowledge(message)
50
+ end
51
+ end
52
+ end
53
+
54
+ MyMachine.new.start
55
+
56
+ 2. Hook it up to a [AMQP](http://amqp.rubyforge.org/ "AMQP") message source
57
+
58
+ > msg_subscribe -x raw -t topic -q octane | my_worker.rb | msg_push -x standard -q standard
59
+
60
+ You can learn more about the command line tools and what options are available by using their help command.
61
+
62
+ > msg_subscriber --help
63
+ > msg_push --help
64
+ > msg_sink --help
65
+ > msg_probe --help
66
+
67
+ ## Examples ##
68
+
69
+ In the example, open two terminal windows and then execute the following commands in each:
70
+
71
+ > msg_generator | msg_push -x test-exchange
72
+ > msg_subscribe -a false --http-port 9090 -x test-exchange -q test_queue > /dev/null
73
+
74
+ The example does the following:
75
+
76
+ 1. Generate a bunch of sample messages
77
+ 2. Push them to an [AMQP exchange](http://amqp.rubyforge.org/classes/MQ/Exchange.html "MQ::Exchange")
78
+ 'test-exchange'.
79
+ 3. Hook up a pipe to consume the generated messages
80
+ 4. Push results to /dev/null.
81
+ 5. Goto http://localhost:9090 to see message throughput.
82
+
83
+ Now lets switch on acknowledgements:
84
+
85
+ > msg_generator | msg_push -x test-exchange
86
+ > msg_subscribe -x test-exchange -q test_queue | msg_sink
87
+
88
+ This example does the same as above, but this time with acknowledgements switched on.
89
+ This guarantees that a message isn't removed from the message server until handled.
90
+ msg_sink acknowledges all messages.
91
+
92
+ ## Todo ##
93
+
94
+ * DNS-SD is broken. See http://github.com/tenderlove/dnssd/issues#issue/3. We need to fix it or kill it.
95
+
96
+ * Write units tests.
97
+
98
+ * Tidy up logging. Not used consistently throughout code. What do we want to log?
data/bin/msg_generator ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2010-3-17.
4
+ # Copyright Visfleet Ltd. (c) 2010. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.dirname(__FILE__) + '/../lib/pipeline_toolkit'
8
+ require "pipeline_toolkit/commands/msg_generator/cli"
9
+
10
+ # print an options summary if no args specified
11
+ ARGV << "--help" if ARGV.empty?
12
+
13
+ Commands::MsgGenerate::CLI.execute(STDOUT, ARGV)
data/bin/msg_probe ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2010-3-17.
4
+ # Copyright Visfleet Ltd. (c) 2010. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.dirname(__FILE__) + '/../lib/pipeline_toolkit'
8
+ require 'pipeline_toolkit/commands/msg_probe/cli'
9
+
10
+ # print an options summary if no args specified
11
+ ARGV << "--help" if ARGV.empty?
12
+
13
+ Commands::MsgProbe::CLI.execute(STDOUT, ARGV)
data/bin/msg_push ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2010-3-17.
4
+ # Copyright Visfleet Ltd. (c) 2010. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.dirname(__FILE__) + '/../lib/pipeline_toolkit'
8
+ require "pipeline_toolkit/commands/msg_push/cli"
9
+
10
+ # print an options summary if no args specified
11
+ ARGV << "--help" if ARGV.empty?
12
+
13
+ Commands::MsgPush::CLI.execute(STDOUT, ARGV)
data/bin/msg_sink ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2010-3-17.
4
+ # Copyright Visfleet Ltd. (c) 2010. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.dirname(__FILE__) + '/../lib/pipeline_toolkit'
8
+ require "pipeline_toolkit/commands/msg_sink/cli"
9
+
10
+ # print an options summary if no args specified
11
+ ARGV << "--help" if ARGV.empty?
12
+
13
+ Commands::MsgSink::CLI.execute(STDOUT, ARGV)
14
+
data/bin/msg_subscribe ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2010-3-17.
4
+ # Copyright Visfleet Ltd. (c) 2010. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.dirname(__FILE__) + '/../lib/pipeline_toolkit'
8
+ require "pipeline_toolkit/commands/msg_subscribe/cli"
9
+
10
+ # print an options summary if no args specified
11
+ ARGV << "--help" if ARGV.empty?
12
+
13
+ Commands::MsgSubscribe::CLI.execute(STDOUT, ARGV)
@@ -1,3 +1,13 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'pipeline_toolkit/amqp/abstract'
4
+ require 'pipeline_toolkit/amqp/reader'
5
+ require 'pipeline_toolkit/amqp/writer'
6
+
7
+ require 'pipeline_toolkit/monitoring/monitor_server'
8
+
9
+ require 'pipeline_toolkit/handlers/message_handler'
10
+
1
11
  require "pipeline_toolkit/default_logger"
2
12
  require "pipeline_toolkit/message_coder"
3
13
  require "pipeline_toolkit/message_command"
@@ -7,3 +17,10 @@ require "pipeline_toolkit/message_subscriber"
7
17
  require "pipeline_toolkit/message_sink"
8
18
  require "pipeline_toolkit/open_hash"
9
19
  require "pipeline_toolkit/socket_util"
20
+ require 'pipeline_toolkit/message_generator'
21
+
22
+ require 'pipeline_toolkit/commands/msg_subscribe/cli'
23
+ require 'pipeline_toolkit/commands/msg_generator/cli'
24
+ require 'pipeline_toolkit/commands/msg_probe/cli'
25
+ require 'pipeline_toolkit/commands/msg_push/cli'
26
+ require 'pipeline_toolkit/commands/msg_sink/cli'
@@ -0,0 +1,95 @@
1
+ require 'mq'
2
+
3
+ module Amqp # :nodoc:
4
+
5
+ ##
6
+ # Provides abstract base functionality used in both {Amqp::Reader} and {Amqp::Writer}
7
+ #
8
+ module Abstract
9
+
10
+ ##
11
+ # Getter for the options hash
12
+ #
13
+ attr_reader :options
14
+
15
+ ##
16
+ # Initialize a new instance of either the {Amqp::Reader} or {Amqp::Writer}
17
+ #
18
+ # @param options<Hash> The options hash
19
+ # @option options [Symbol] :host ('localhost') The AMQP server address for the AMQP server.
20
+ # @option options [Symbol] :port (5672) The AMQP server port for the AMQP server.
21
+ # @option options [Symbol] :user ('guest') The username as defined by the AMQP server.
22
+ # @option options [Symbol] :pass ('guest') The password for the associated :user as defined by the AMQP server.
23
+ # @option options [Symbol] :vhost ('/') The virtual host as defined by the AMQP server.
24
+ # @option options [Symbol] :type ('fanout') The exchange type (direct, fanout or topic).
25
+ # @option options [Symbol] :exchange ('') The exchange name
26
+ # @option options [Symbol] :durable (false) If set to true, the exchange will be marked as durable.
27
+ # @option options [Symbol] :passive (false) If set to true, the server will not create the exchange if it does not already exist.
28
+ # @option options [Symbol] :ack (true) If this field is set to false the server does not expect acknowledgments for messages.
29
+ # @option options [Symbol] :env ('development') The environment to run (development, production).
30
+ #
31
+ def initialize(options = {})
32
+ super(options)
33
+
34
+ DefaultLogger.init_logger(options)
35
+
36
+ @options = options
37
+ end
38
+
39
+ ##
40
+ # Create a new connection to the AMQP server.
41
+ #
42
+ def initialize_connection
43
+ DefaultLogger.debug("Amqp::Abstract#initialize_connection") if options[:env] == "development"
44
+ @connection = AMQP.connect(options.select_keys(:host, :port, :user, :pass, :vhost))
45
+ end
46
+
47
+ ##
48
+ # Returns a new channel. A channel is a bidirectional virtual connection between the client
49
+ # and the AMQP server.
50
+ #
51
+ def initialize_channel
52
+ DefaultLogger.debug("Amqp::Abstract#initialize_channel") if options[:env] == "development"
53
+ @channel = MQ.new(@connection)
54
+ end
55
+
56
+ ##
57
+ # Defines, intializes and returns an Exchange
58
+ # to act as an ingress point for all published messages.
59
+ #
60
+ def initialize_exchange
61
+ DefaultLogger.debug("Amqp::Abstract#initialize_exchange") if options[:env] == "development"
62
+ # declare a exchange on the channel
63
+ @exchange = MQ::Exchange.new(@channel, options[:type], options[:exchange], :durable => options[:durable], :passive => options[:passive])
64
+ rescue MQ::Error => e # rescued here because main thread does not seem to see it
65
+ DefaultLogger.error "#{e.class.name}: #{e.message}\n" << e.backtrace.join("\n")
66
+ raise e
67
+ end
68
+
69
+ ##
70
+ # Defines, intializes and returns an Queue that store and forward messages.
71
+ #
72
+ # @param name<String> The name to use for the queue
73
+ #
74
+ def initialize_queue(name = '')
75
+ DefaultLogger.debug("Amqp::Abstract#initialize_queue(name = '')") if options[:env] == "development"
76
+ name = options[:queue] if name.nil? || name.empty?
77
+ @queue = MQ::Queue.new(@channel, name, :durable => options[:durable], :passive => options[:passive])
78
+ end
79
+
80
+ ##
81
+ # Binds the queue to an exchange.
82
+ #
83
+ # @param routing_key<Sting> Specifies the routing key for the binding. The routing key is used for
84
+ # routing messages depending on the exchange configuration.
85
+ #
86
+ def bind_queue(routing_key = '')
87
+ DefaultLogger.debug("Amqp::Abstract#bind_queue(routing_key = '')") if options[:env] == "development"
88
+ unless routing_key.nil? || routing_key.empty?
89
+ @queue.bind(@exchange, :key => routing_key)
90
+ else
91
+ @queue.bind(@exchange)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,64 @@
1
+ require 'pipeline_toolkit/amqp/abstract'
2
+
3
+ module Amqp # :nodoc:
4
+
5
+ ##
6
+ # The Reader provides functionality specific to asynchronous message
7
+ # delivery by subscribing to the AMQP server.
8
+ #
9
+ module Reader
10
+ include Amqp::Abstract
11
+
12
+ ##
13
+ # Initialize the AMQP server specific to handling
14
+ # of messages from the server.
15
+ #
16
+ def initialize_reader
17
+ DefaultLogger.debug("Amqp::Reader#initialize_reader") if options[:env] == "development"
18
+ @ack_headers ||= {}
19
+ initialize_connection
20
+ initialize_channel
21
+ @channel.prefetch(1) # We only want to handle one message at a time
22
+ initialize_exchange
23
+ initialize_queue
24
+ end
25
+
26
+ ##
27
+ # Subscribe the queue to receive messages
28
+ #
29
+ def queue_subscribe
30
+ DefaultLogger.debug("Amqp::Reader#queue_subscribe") if options[:env] == "development"
31
+ unless AMQP.closing?
32
+ @queue.subscribe(:ack => options[:ack]) do |header, body|
33
+ DefaultLogger.debug("Rabbit Message: #{body}") if options[:env] == "development"
34
+ process_queue_message(header, body)
35
+ end
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Store the acknowledgement header
41
+ #
42
+ # @param message<Hash> The message
43
+ # @param header<MQ::Header> The message queue header for the message
44
+ #
45
+ def store_acknowledgement(message, header)
46
+ DefaultLogger.debug("Amqp::Reader#store_acknowledgement(message, header)") if options[:env] == "development"
47
+ message[:ack_id] = header.delivery_tag.to_s
48
+ @ack_headers[message[:ack_id]] = header
49
+ end
50
+
51
+ ##
52
+ # Perform the acknowledgement on the stored header to
53
+ # tell AMQP server we are done with the message.
54
+ #
55
+ # @param ack_id<String> The acknowledgement identifier
56
+ #
57
+ def perform_acknowledgement(ack_id)
58
+ DefaultLogger.debug("Amqp::Reader#perform_acknowledgement(ack_id)") if options[:env] == "development"
59
+ header = @ack_headers.delete(ack_id)
60
+ header.ack
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ require 'pipeline_toolkit/amqp/abstract'
2
+ require 'pipeline_toolkit/message_coder'
3
+
4
+ module Amqp # :nodoc:
5
+
6
+ ##
7
+ # The Writer provides functionality specific to asynchronous message
8
+ # publishing to the AMQP server.
9
+ #
10
+ module Writer
11
+ include Amqp::Abstract
12
+
13
+ ##
14
+ # Initialize the AMQP server specific to handling
15
+ # of messages publishing to the server.
16
+ #
17
+ def initialize_writer
18
+ DefaultLogger.debug("Amqp::Writer#initialize_writer") if options[:env] == "development"
19
+ initialize_connection
20
+ initialize_channel
21
+ initialize_exchange
22
+ end
23
+
24
+ ##
25
+ # Initializes the queues to publish the messages too.
26
+ #
27
+ def initialize_queues
28
+ DefaultLogger.debug("Amqp::Writer#initialize_queues") if options[:env] == "development"
29
+ options[:queues].each do |name, routing_key|
30
+ initialize_queue(name)
31
+ bind_queue(routing_key)
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Handles the publishing of messages into the AMQP server.
37
+ #
38
+ # @param message<Hash> The message to publish
39
+ #
40
+ def publish(message)
41
+ DefaultLogger.debug("Amqp::Writer#publish(message)") if options[:env] == "development"
42
+ if message.has_key?(:routing_key)
43
+ @exchange.publish(MessageCoder.encode(message), :key => message[:routing_key])
44
+ else
45
+ @exchange.publish(MessageCoder.encode(message))
46
+ end
47
+ end
48
+
49
+ def queue_names
50
+ options[:queues].map { |name, type| name }
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ require 'pp'
2
+ require 'trollop'
3
+ require 'eventmachine'
4
+
5
+ Signal.trap('INT') do
6
+ # puts "\nStopping"
7
+ # TODO: complete signal trap interrupt
8
+ end
9
+
10
+ Signal.trap('TERM') do
11
+ # puts "\nStopping"
12
+ # TODO: complete signal trap terminate
13
+ end
14
+
15
+ module Commands # :nodoc:
16
+ module MsgGenerate
17
+ class CLI
18
+ def self.execute(stdout, arguments=[])
19
+
20
+ opts = Trollop::options do
21
+ banner <<-EOL
22
+ Message Subscriber
23
+ ------------------
24
+ Generates messages for testing the pipeline. Messages are specified
25
+ using JSON.
26
+
27
+ Usage:
28
+ msg_generator [options]
29
+
30
+ Examples:
31
+ msg_generator -m '{"id":1, "firstName":"John Smith"}'
32
+
33
+ Options:
34
+ EOL
35
+ opt :msg, "The message to send", :short => "m", :default => '{"id":1}'
36
+ opt :delay, "Sleep time between sends in seconds", :short => "d", :type => :float
37
+
38
+ opt :env, "The environment to run (development, production)", :default => "development", :short => :e
39
+ end
40
+
41
+ MessageGenerator.new(opts).start
42
+ end
43
+ end
44
+ end
45
+ end