pipeline_toolkit 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.markdown +1 -1
- data/README.markdown +38 -19
- data/bin/msg_generator +3 -4
- data/bin/msg_push +3 -4
- data/bin/msg_sink +3 -7
- data/bin/msg_subscribe +3 -4
- data/lib/pipeline_toolkit.rb +5 -5
- data/lib/pipeline_toolkit/amqp/abstract.rb +81 -78
- data/lib/pipeline_toolkit/amqp/reader.rb +55 -52
- data/lib/pipeline_toolkit/amqp/writer.rb +42 -39
- data/lib/pipeline_toolkit/{commands/msg_generator/cli.rb → cli/msg_generator_cli.rb} +6 -5
- data/lib/pipeline_toolkit/{commands/msg_push/cli.rb → cli/msg_push_cli.rb} +16 -8
- data/lib/pipeline_toolkit/cli/msg_sink_cli.rb +28 -0
- data/lib/pipeline_toolkit/{commands/msg_subscribe/cli.rb → cli/msg_subscribe_cli.rb} +23 -14
- data/lib/pipeline_toolkit/cucumber.rb +5 -3
- data/lib/pipeline_toolkit/cucumber/amqp.rb +1 -1
- data/lib/pipeline_toolkit/cucumber/in_out_process.rb +50 -0
- data/lib/pipeline_toolkit/cucumber/machine.rb +44 -26
- data/lib/pipeline_toolkit/cucumber/machine_steps.rb +15 -9
- data/lib/pipeline_toolkit/cucumber/{msg_sub_machine.rb → msg_sub_process.rb} +5 -5
- data/lib/pipeline_toolkit/handlers/message_handler.rb +43 -39
- data/lib/pipeline_toolkit/message_coder.rb +38 -25
- data/lib/pipeline_toolkit/message_command.rb +167 -157
- data/lib/pipeline_toolkit/message_generator.rb +22 -18
- data/lib/pipeline_toolkit/message_pusher.rb +46 -47
- data/lib/pipeline_toolkit/message_sink.rb +9 -5
- data/lib/pipeline_toolkit/message_subscriber.rb +190 -183
- data/lib/pipeline_toolkit/monitoring/monitor_server.rb +64 -109
- data/lib/pipeline_toolkit/util/hash_ext.rb +8 -0
- data/lib/pipeline_toolkit/util/indifferent_hash.rb +10 -0
- data/lib/pipeline_toolkit/{socket_util.rb → util/socket_util.rb} +1 -1
- metadata +25 -115
- data/lib/pipeline_toolkit/commands/msg_sink/cli.rb +0 -41
- data/lib/pipeline_toolkit/open_hash.rb +0 -24
data/LICENSE.markdown
CHANGED
data/README.markdown
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Pipeline Toolkit #
|
2
2
|
by VisFleet
|
3
3
|
|
4
|
-
Command line tools for processing messages by constructing a pipeline of
|
4
|
+
Command line tools for processing messages by constructing a pipeline of machines. [AMQP](http://amqp.rubyforge.org/ "AMQP") and Unix pipes are used to construct the pipeline. Messages are simple Hashes (serialized as JSON) so they can hold any values and change throughout the processing.
|
5
5
|
|
6
6
|
Provides:
|
7
7
|
|
@@ -11,21 +11,27 @@ Provides:
|
|
11
11
|
* Subscribing to messages from an [AMQP queue](http://amqp.rubyforge.org/classes/MQ/Queue.html "MQ::Queue")
|
12
12
|
* Pushing messages back onto an [AMQP exchange](http://amqp.rubyforge.org/classes/MQ/Exchange.html "MQ::Exchange")
|
13
13
|
* Monitoring performance (see msg_probe)
|
14
|
-
* A base module ({MessageCommand}) to include into your own classes to quickly make
|
14
|
+
* A base module ({MessageCommand}) to include into your own classes to quickly make machines.
|
15
15
|
|
16
16
|
## Install ##
|
17
17
|
|
18
|
-
|
18
|
+
> (sudo) gem install pipeline_toolkit
|
19
19
|
|
20
20
|
### Dependancies ###
|
21
21
|
|
22
22
|
It is assumed that you have:
|
23
|
-
|
24
|
-
-
|
23
|
+
|
24
|
+
- An [AMQP](http://amqp.rubyforge.org/ "AMQP") message server to pop and push messages to (e.g. http://www.rabbitmq.com/)
|
25
|
+
|
26
|
+
- A *nix system, such as Linux or Mac OS X.
|
27
|
+
|
28
|
+
- The 'uuidgen' command-line tool. This is installed by default on OS X, and is available through most package managers (e.g it is the "uuid-runtime" package in aptitude).
|
29
|
+
|
30
|
+
All gem dependancies are installed automatically, but if you're curious checkout *pipeline_toolkit.gemspec*
|
25
31
|
|
26
32
|
## Usage ##
|
27
33
|
|
28
|
-
1. Create your
|
34
|
+
1. Create your machine
|
29
35
|
|
30
36
|
class MyMachine
|
31
37
|
include MessageCommand
|
@@ -34,11 +40,6 @@ It is assumed that you have:
|
|
34
40
|
# Optional: setup any dependencies required by your machine
|
35
41
|
end
|
36
42
|
|
37
|
-
def report_back
|
38
|
-
# Optional: write information about the pipeline back
|
39
|
-
# through the acknowledgement pipe
|
40
|
-
end
|
41
|
-
|
42
43
|
def process_standard(message)
|
43
44
|
# Required: handle the message within your machine
|
44
45
|
unless failed_to_process?
|
@@ -55,7 +56,7 @@ It is assumed that you have:
|
|
55
56
|
|
56
57
|
2. Hook it up to a [AMQP](http://amqp.rubyforge.org/ "AMQP") message source
|
57
58
|
|
58
|
-
> msg_subscribe -x raw -t topic -q octane |
|
59
|
+
> msg_subscribe -x raw -t topic -q octane | my_machine.rb | msg_push -x standard -q standard
|
59
60
|
|
60
61
|
You can learn more about the command line tools and what options are available by using their help command.
|
61
62
|
|
@@ -89,13 +90,31 @@ This example does the same as above, but this time with acknowledgements switche
|
|
89
90
|
This guarantees that a message isn't removed from the message server until handled.
|
90
91
|
msg_sink acknowledges all messages.
|
91
92
|
|
92
|
-
##
|
93
|
+
## Pre-canned cucumber steps ##
|
93
94
|
|
94
|
-
|
95
|
+
Pipeline toolkit also includes pre-canned cucumber steps for you to test your machines with. The following steps are included:
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
97
|
+
Given I run this machine:
|
98
|
+
When I input these messages:
|
99
|
+
Then these messages are passed on:
|
100
|
+
Then these messages are acknowledged:
|
101
|
+
|
102
|
+
Given an amqp server running at "localhost" on port 1234
|
103
|
+
Given amqp queue "test" is empty
|
104
|
+
When I input these messages to exchange "test"
|
105
|
+
Then these messages are on queue "test"
|
106
|
+
|
107
|
+
The steps are defined in the following files, so have a look at them to get more details
|
108
|
+
|
109
|
+
lib/pipeline_toolkit/cucumber/machine_steps.rb
|
110
|
+
lib/pipeline_toolkit/cucumber/amqp_steps.rb
|
111
|
+
|
112
|
+
To make the steps available to your application's cucumber features simply put the follow at the top of your env.rb file (or any file under features/support).
|
113
|
+
|
114
|
+
require 'pipeline_toolkit/cucumber'
|
100
115
|
|
101
|
-
|
116
|
+
## Todo ##
|
117
|
+
|
118
|
+
* Tidy up logging. Feels horrible that we're insisting that SysLogger is used. Want to abstract this out and let user decide which logging framework to use. Also we're hard coding that logs are output to ~/pipeline_toolkit. yuk.
|
119
|
+
|
120
|
+
* Add tmp path as a argument (hardcoded to /tmp at mo)
|
data/bin/msg_generator
CHANGED
@@ -3,8 +3,7 @@
|
|
3
3
|
# Created on 2010-3-17.
|
4
4
|
# Copyright Visfleet Ltd. (c) 2010. All rights reserved.
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
require "pipeline_toolkit/commands/msg_generator/cli"
|
6
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
7
|
+
require "pipeline_toolkit/cli/msg_generator_cli"
|
9
8
|
|
10
|
-
|
9
|
+
PipelineToolkit::CLI::MsgGeneratorCLI.execute(STDOUT, ARGV)
|
data/bin/msg_push
CHANGED
@@ -3,11 +3,10 @@
|
|
3
3
|
# Created on 2010-3-17.
|
4
4
|
# Copyright Visfleet Ltd. (c) 2010. All rights reserved.
|
5
5
|
|
6
|
-
|
7
|
-
require
|
8
|
-
require "pipeline_toolkit/commands/msg_push/cli"
|
6
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
7
|
+
require "pipeline_toolkit/cli/msg_push_cli"
|
9
8
|
|
10
9
|
# print an options summary if no args specified
|
11
10
|
ARGV << "--help" if ARGV.empty?
|
12
11
|
|
13
|
-
|
12
|
+
PipelineToolkit::CLI::MsgPushCLI.execute(STDOUT, ARGV)
|
data/bin/msg_sink
CHANGED
@@ -3,12 +3,8 @@
|
|
3
3
|
# Created on 2010-3-17.
|
4
4
|
# Copyright Visfleet Ltd. (c) 2010. All rights reserved.
|
5
5
|
|
6
|
-
|
7
|
-
require
|
8
|
-
require "pipeline_toolkit/commands/msg_sink/cli"
|
6
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
7
|
+
require "pipeline_toolkit/cli/msg_sink_cli"
|
9
8
|
|
10
|
-
|
11
|
-
ARGV << "--help" if ARGV.empty?
|
12
|
-
|
13
|
-
Commands::MsgSink::CLI.execute(STDOUT, ARGV)
|
9
|
+
PipelineToolkit::CLI::MsgSinkCLI.execute(STDOUT, ARGV)
|
14
10
|
|
data/bin/msg_subscribe
CHANGED
@@ -3,11 +3,10 @@
|
|
3
3
|
# Created on 2010-3-17.
|
4
4
|
# Copyright Visfleet Ltd. (c) 2010. All rights reserved.
|
5
5
|
|
6
|
-
|
7
|
-
require
|
8
|
-
require "pipeline_toolkit/commands/msg_subscribe/cli"
|
6
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
7
|
+
require "pipeline_toolkit/cli/msg_subscribe_cli"
|
9
8
|
|
10
9
|
# print an options summary if no args specified
|
11
10
|
ARGV << "--help" if ARGV.empty?
|
12
11
|
|
13
|
-
|
12
|
+
PipelineToolkit::CLI::MsgSubscribeCLI.execute(STDOUT, ARGV)
|
data/lib/pipeline_toolkit.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
2
|
-
|
3
1
|
require 'pipeline_toolkit/amqp/abstract'
|
4
2
|
require 'pipeline_toolkit/amqp/reader'
|
5
3
|
require 'pipeline_toolkit/amqp/writer'
|
@@ -14,6 +12,8 @@ require "pipeline_toolkit/message_command"
|
|
14
12
|
require "pipeline_toolkit/message_pusher"
|
15
13
|
require "pipeline_toolkit/message_subscriber"
|
16
14
|
require "pipeline_toolkit/message_sink"
|
17
|
-
require
|
18
|
-
|
19
|
-
require
|
15
|
+
require 'pipeline_toolkit/message_generator'
|
16
|
+
|
17
|
+
require "pipeline_toolkit/util/hash_ext"
|
18
|
+
require "pipeline_toolkit/util/indifferent_hash"
|
19
|
+
require "pipeline_toolkit/util/socket_util"
|
@@ -1,95 +1,98 @@
|
|
1
1
|
require 'mq'
|
2
2
|
|
3
|
-
module
|
3
|
+
module PipelineToolkit
|
4
|
+
module Amqp # :nodoc:
|
4
5
|
|
5
|
-
##
|
6
|
-
# Provides abstract base functionality used in both {Amqp::Reader} and {Amqp::Writer}
|
7
|
-
#
|
8
|
-
module Abstract
|
9
|
-
|
10
6
|
##
|
11
|
-
#
|
7
|
+
# Provides abstract base functionality used in both {Amqp::Reader} and {Amqp::Writer}
|
12
8
|
#
|
13
|
-
|
9
|
+
module Abstract
|
10
|
+
|
11
|
+
##
|
12
|
+
# Getter for the options hash
|
13
|
+
#
|
14
|
+
attr_reader :options
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
16
|
+
##
|
17
|
+
# Initialize a new instance of either the {Amqp::Reader} or {Amqp::Writer}
|
18
|
+
#
|
19
|
+
# @param options<Hash> The options hash
|
20
|
+
# @option options [Symbol] :host ('localhost') The AMQP server address for the AMQP server.
|
21
|
+
# @option options [Symbol] :port (5672) The AMQP server port for the AMQP server.
|
22
|
+
# @option options [Symbol] :user ('guest') The username as defined by the AMQP server.
|
23
|
+
# @option options [Symbol] :pass ('guest') The password for the associated :user as defined by the AMQP server.
|
24
|
+
# @option options [Symbol] :vhost ('/') The virtual host as defined by the AMQP server.
|
25
|
+
# @option options [Symbol] :type ('fanout') The exchange type (direct, fanout or topic).
|
26
|
+
# @option options [Symbol] :exchange ('') The exchange name
|
27
|
+
# @option options [Symbol] :durable (false) If set to true, the exchange will be marked as durable.
|
28
|
+
# @option options [Symbol] :passive (false) If set to true, the server will not create the exchange if it does not already exist.
|
29
|
+
# @option options [Symbol] :ack (true) If this field is set to false the server does not expect acknowledgments for messages.
|
30
|
+
# @option options [Symbol] :env ('development') The environment to run (development, production).
|
31
|
+
#
|
32
|
+
def initialize(options = {})
|
33
|
+
super(options)
|
33
34
|
|
34
|
-
|
35
|
+
DefaultLogger.init_logger(options)
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
@options = options
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
##
|
41
|
+
# Create a new connection to the AMQP server.
|
42
|
+
#
|
43
|
+
def initialize_connection
|
44
|
+
DefaultLogger.debug("Amqp::Abstract#initialize_connection") if options[:env] == "development"
|
45
|
+
@connection = AMQP.connect(options.select_keys(:host, :port, :user, :pass, :vhost))
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
##
|
49
|
+
# Returns a new channel. A channel is a bidirectional virtual connection between the client
|
50
|
+
# and the AMQP server.
|
51
|
+
#
|
52
|
+
def initialize_channel
|
53
|
+
DefaultLogger.debug("Amqp::Abstract#initialize_channel") if options[:env] == "development"
|
54
|
+
@channel = MQ.new(@connection)
|
55
|
+
end
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
57
|
+
##
|
58
|
+
# Defines, intializes and returns an Exchange
|
59
|
+
# to act as an ingress point for all published messages.
|
60
|
+
#
|
61
|
+
def initialize_exchange
|
62
|
+
DefaultLogger.debug("Amqp::Abstract#initialize_exchange") if options[:env] == "development"
|
63
|
+
# declare a exchange on the channel
|
64
|
+
@exchange = MQ::Exchange.new(@channel, options[:type], options[:exchange], :durable => options[:durable], :passive => options[:passive])
|
65
|
+
rescue MQ::Error => e # rescued here because main thread does not seem to see it
|
66
|
+
DefaultLogger.error "#{e.class.name}: #{e.message}\n" << e.backtrace.join("\n")
|
67
|
+
raise e
|
68
|
+
end
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
70
|
+
##
|
71
|
+
# Defines, intializes and returns an Queue that store and forward messages.
|
72
|
+
#
|
73
|
+
# @param name<String> The name to use for the queue
|
74
|
+
#
|
75
|
+
def initialize_queue(name = '')
|
76
|
+
DefaultLogger.debug("Amqp::Abstract#initialize_queue(name = '')") if options[:env] == "development"
|
77
|
+
name = options[:queue] if name.nil? || name.empty?
|
78
|
+
@queue = MQ::Queue.new(@channel, name, :durable => options[:durable], :passive => options[:passive])
|
79
|
+
end
|
79
80
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
81
|
+
##
|
82
|
+
# Binds the queue to an exchange.
|
83
|
+
#
|
84
|
+
# @param routing_key<Sting> Specifies the routing key for the binding. The routing key is used for
|
85
|
+
# routing messages depending on the exchange configuration.
|
86
|
+
#
|
87
|
+
def bind_queue(routing_key = '')
|
88
|
+
DefaultLogger.debug("Amqp::Abstract#bind_queue(routing_key = '')") if options[:env] == "development"
|
89
|
+
unless routing_key.nil? || routing_key.empty?
|
90
|
+
@queue.bind(@exchange, :key => routing_key)
|
91
|
+
else
|
92
|
+
@queue.bind(@exchange)
|
93
|
+
end
|
92
94
|
end
|
93
95
|
end
|
96
|
+
|
94
97
|
end
|
95
98
|
end
|
@@ -1,65 +1,68 @@
|
|
1
1
|
require 'pipeline_toolkit/amqp/abstract'
|
2
2
|
|
3
|
-
module
|
3
|
+
module PipelineToolkit
|
4
|
+
module Amqp # :nodoc:
|
4
5
|
|
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
6
|
##
|
13
|
-
#
|
14
|
-
#
|
7
|
+
# The Reader provides functionality specific to asynchronous message
|
8
|
+
# delivery by subscribing to the AMQP server.
|
15
9
|
#
|
16
|
-
|
17
|
-
|
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
|
-
bind_queue
|
25
|
-
end
|
10
|
+
module Reader
|
11
|
+
include Amqp::Abstract
|
26
12
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
|
35
|
-
|
13
|
+
##
|
14
|
+
# Initialize the AMQP server specific to handling
|
15
|
+
# of messages from the server.
|
16
|
+
#
|
17
|
+
def initialize_reader
|
18
|
+
DefaultLogger.debug("Amqp::Reader#initialize_reader") if options[:env] == "development"
|
19
|
+
@ack_headers ||= {}
|
20
|
+
initialize_connection
|
21
|
+
initialize_channel
|
22
|
+
@channel.prefetch(1) # We only want to handle one message at a time
|
23
|
+
initialize_exchange
|
24
|
+
initialize_queue
|
25
|
+
bind_queue
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Subscribe the queue to receive messages
|
30
|
+
#
|
31
|
+
def queue_subscribe
|
32
|
+
DefaultLogger.debug("Amqp::Reader#queue_subscribe") if options[:env] == "development"
|
33
|
+
unless AMQP.closing?
|
34
|
+
@queue.subscribe(:ack => options[:ack]) do |header, body|
|
35
|
+
DefaultLogger.debug("Rabbit Message: #{body}") if options[:env] == "development"
|
36
|
+
process_queue_message(header, body)
|
37
|
+
end
|
36
38
|
end
|
37
39
|
end
|
38
|
-
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
##
|
42
|
+
# Store the acknowledgement header
|
43
|
+
#
|
44
|
+
# @param message<Hash> The message
|
45
|
+
# @param header<MQ::Header> The message queue header for the message
|
46
|
+
#
|
47
|
+
def store_acknowledgement(message, header)
|
48
|
+
DefaultLogger.debug("Amqp::Reader#store_acknowledgement(message, header)") if options[:env] == "development"
|
49
|
+
message[:ack_id] = header.delivery_tag.to_s
|
50
|
+
@ack_headers[message[:ack_id]] = header
|
51
|
+
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
53
|
+
##
|
54
|
+
# Perform the acknowledgement on the stored header to
|
55
|
+
# tell AMQP server we are done with the message.
|
56
|
+
#
|
57
|
+
# @param ack_id<String> The acknowledgement identifier
|
58
|
+
#
|
59
|
+
def perform_acknowledgement(ack_id)
|
60
|
+
DefaultLogger.debug("Amqp::Reader#perform_acknowledgement(ack_id)") if options[:env] == "development"
|
61
|
+
header = @ack_headers.delete(ack_id)
|
62
|
+
header.ack
|
63
|
+
end
|
63
64
|
|
65
|
+
end
|
66
|
+
|
64
67
|
end
|
65
68
|
end
|