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