nodule 0.0.34

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ require 'socket'
2
+
3
+ module Nodule
4
+ module Util
5
+ @seen = {}
6
+
7
+ def self.random_tcp_port(max_tries=500)
8
+ self._random_port do |port|
9
+ TCPServer.new port
10
+ end
11
+ end
12
+
13
+ def self.random_udp_port(max_tries=500)
14
+ self._random_port do |port|
15
+ socket = UDPSocket.new
16
+ socket.bind("0.0.0.0", port)
17
+ socket
18
+ end
19
+ end
20
+
21
+ #
22
+ # Try random ports > 10_000 looking for one that's free.
23
+ # @param [Fixnum] max number of tries to find a free port
24
+ # @return [Fixnum] port number
25
+ # @yield [Fixnum] port
26
+ #
27
+ def self._random_port(max_tries=500)
28
+ tries = 0
29
+
30
+ while tries < max_tries
31
+ port = random_port
32
+ next if @seen.has_key? port
33
+
34
+ socket = begin
35
+ yield port
36
+ rescue Errno::EADDRINUSE
37
+ @seen[port] = true
38
+ tries += 1
39
+ next
40
+ end
41
+
42
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
43
+ socket.close
44
+ return port
45
+ end
46
+ end
47
+
48
+ #
49
+ # Return a random integer between 10_000 and 65_534
50
+ # @return [Fixnum]
51
+ #
52
+ def self.random_port
53
+ rand(55534) + 10_000
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module Nodule
2
+ VERSION = "0.0.34"
3
+ end
@@ -0,0 +1,280 @@
1
+ require 'nodule/tempfile'
2
+ require 'ffi-rzmq'
3
+ require 'thread'
4
+
5
+ module Nodule
6
+ #
7
+ # A resource for setting up and testing ZeroMQ message flows. The most basic usage will provide
8
+ # auto-generated IPC URI's, which can be handy for testing. More advanced usage uses the built-in
9
+ # tap device to sniff messages while they're in-flight.
10
+ #
11
+ class ZeroMQ < Tempfile
12
+ attr_reader :ctx, :uri, :method, :type, :limit, :error_count
13
+
14
+ private
15
+
16
+ def setsockopt(socket, option, value)
17
+ if option == :hwm && ::ZMQ::LibZMQ.version3?
18
+ rc = socket.setsockopt(::ZMQ::SNDHWM, value)
19
+ rc = socket.setsockopt(::ZMQ::RCVHWM, value) if rc > -1
20
+ else
21
+ option = ::ZMQ::HWM if option == :hwm
22
+ rc = socket.setsockopt(option, value)
23
+ end
24
+
25
+ rc
26
+ end
27
+
28
+ public
29
+
30
+ #
31
+ # @param [Hash{Symbol => Object}] opts named parameters
32
+ # @option [String] :uri either :gen/:generate or a string, :gen means generate an IPC URI,
33
+ # a string must be a valid URI.
34
+ # @option [Fixnum] :limit exit the read loop after :limit messages are received
35
+ # @option [String,Symbol] :connect create a socket and connect to the URI
36
+ # @option [String,Symbol] :bind create a socket and bind to the URI
37
+ #
38
+ # :connect and :bind are allowed at the same time and must be of the same socket type.
39
+ # ZMQ::SUB sockets that are connected/bound will subscribe to "" by default.
40
+ #
41
+ # For the rest of the options, see Hastur::Test::Resource::Base.
42
+ #
43
+ def initialize(opts)
44
+ opts[:suffix] ||= '.zmq'
45
+
46
+ super(opts)
47
+
48
+ @ctx = ::ZMQ::Context.new
49
+ @zmq_thread = nil
50
+ @error_count = 0
51
+ @sockprocs = []
52
+ @limit = nil
53
+ @timeout_started = false
54
+ @stopped = false
55
+
56
+ # Sockets cannot be used across thread boundaries, so use a ZMQ::PAIR socket both to synchronize thread
57
+ # startup and pass writes form main -> thread. The .socket method will return the PAIR socket.
58
+ @pipe_uri = "inproc://pair-#{Nodule.next_seq}"
59
+ @pipe = @ctx.socket(::ZMQ::PAIR)
60
+ @child = @ctx.socket(::ZMQ::PAIR)
61
+ setsockopt(@pipe, :hwm, 1)
62
+ setsockopt(@child, :hwm, 1)
63
+ setsockopt(@pipe, ::ZMQ::LINGER, 1.0)
64
+ setsockopt(@child, ::ZMQ::LINGER, 1.0)
65
+ @pipe.bind(@pipe_uri)
66
+ @child.connect(@pipe_uri)
67
+
68
+ case opts[:uri]
69
+ # Socket files are specified so they land in PWD, in the future we might want to specify a temp
70
+ # dir, but that has a whole different bag of issues, so stick with simple until it's needed.
71
+ when :gen, :generate
72
+ @uri = "ipc://#{@file.to_s}"
73
+ when String
74
+ @uri = opts[:uri]
75
+ else
76
+ raise ArgumentError.new "Invalid URI specifier: (#{opts[:uri].class}) '#{opts[:uri]}'"
77
+ end
78
+
79
+ if opts[:connect] and opts[:bind] and opts[:connect] != opts[:bind]
80
+ raise ArgumentError.new "ZMQ socket types must be the same when enabling :bind and :connect"
81
+ end
82
+
83
+ # only set type and create a socket if :bind or :connect is specified
84
+ # otherwise, the caller probably just wants to generate a URI, or possibly
85
+ # use a pre-created socket? (not supported yet)
86
+ if @type = (opts[:connect] || opts[:bind])
87
+ @socket = @ctx.socket(@type)
88
+ setsockopt(@socket, :hwm, 1)
89
+ setsockopt(@socket, ::ZMQ::LINGER, 1.0)
90
+
91
+ if opts[:connect]
92
+ @sockprocs << proc { @socket.connect(@uri) } # deferred
93
+ end
94
+
95
+ if opts[:bind]
96
+ @sockprocs << proc { @socket.bind(@uri) } # deferred
97
+ end
98
+
99
+ # by default, subscribe to "" on ZMQ::SUB sockets, since that's what we want
100
+ # the vast majority of the time. Also allow specifying a subscription for those
101
+ # times we want something else.
102
+ if @type == ::ZMQ::SUB
103
+ if opts[:subscribe]
104
+ @sockprocs << proc { subscribe(opts[:subscribe]) }
105
+ else
106
+ @sockprocs << proc { subscribe("") }
107
+ end
108
+ end
109
+ end
110
+
111
+ if opts[:limit]
112
+ @limit = opts[:limit]
113
+ end
114
+ end
115
+
116
+ def run
117
+ super
118
+ return if @sockprocs.empty?
119
+
120
+ # wrap the block in a block so errors don't simply vanish until join time
121
+ @zmq_thread = Thread.new do
122
+ Thread.current.abort_on_exception
123
+
124
+ # sockets have to be created inside the thread that uses them
125
+ @sockprocs.each { |p| p.call }
126
+
127
+ _zmq_read()
128
+ verbose "child thread #{Thread.current} shutting down"
129
+
130
+ @child.close
131
+ @socket.close if @socket
132
+ end
133
+
134
+ Thread.pass
135
+
136
+ @stopped = @zmq_thread.alive? ? false : true
137
+ end
138
+
139
+ def socket
140
+ @pipe
141
+ end
142
+
143
+ #
144
+ # For PUB sockets only, subscribe to a prefix.
145
+ # @param [String] subscription prefix, usually ""
146
+ #
147
+ def subscribe(subscription)
148
+ @pipe.send_strings ["subscribe", subscription]
149
+ end
150
+
151
+ def done?
152
+ @stopped
153
+ end
154
+
155
+ #
156
+ # Wait for the ZMQ thread to exit on its own, mostly useful with :limit => Fixnum.
157
+ #
158
+ # This does not signal the child thread.
159
+ #
160
+ def wait(timeout=60)
161
+ countdown = timeout.to_f
162
+
163
+ while countdown > 0
164
+ if @zmq_thread and @zmq_thread.alive?
165
+ sleep 0.1
166
+ countdown = countdown - 0.1
167
+ else
168
+ break
169
+ end
170
+ end
171
+
172
+ super()
173
+ end
174
+
175
+ #
176
+ # If the thread is still alive, force an exception in the thread and
177
+ # continue to do the things stop does.
178
+ #
179
+ def stop!
180
+ if @zmq_thread.alive?
181
+ STDERR.puts "force stop! called, issuing Thread.raise"
182
+ @zmq_thread.raise "force stop! called"
183
+ end
184
+
185
+ stop
186
+ wait 1
187
+
188
+ @zmq_thread.join if @zmq_thread
189
+ @pipe.close if @pipe
190
+
191
+ @stopped = true
192
+ end
193
+
194
+ #
195
+ # send a message to the child thread telling it to exit and join the thread
196
+ #
197
+ def stop
198
+ return if @stopped
199
+
200
+ @pipe.send_strings(["exit"], 1)
201
+
202
+ Thread.pass
203
+
204
+ super
205
+
206
+ @zmq_thread.join if @zmq_thread
207
+ @pipe.close if @pipe
208
+
209
+ @stopped = true
210
+ end
211
+
212
+ #
213
+ # Return the URI generated/provided for this resource. For tapped devices, the "front" side
214
+ # of the tap is returned.
215
+ #
216
+ def to_s
217
+ @uri
218
+ end
219
+
220
+ private
221
+
222
+ #
223
+ # Run a poll loop (using the zmq poller) on a 1/5 second timer, reading data
224
+ # from the socket and calling the registered procs.
225
+ # If :limit was set, will exit after that many messages are seen/processed.
226
+ # Otherwise, exits on the next iteration if the mutex is locked (which is done in stop).
227
+ # Takes no arguments, doesn't return anything meaningful.
228
+ #
229
+ def _zmq_read
230
+ return unless @socket
231
+ @poller = ::ZMQ::Poller.new
232
+
233
+ @poller.register_readable @socket
234
+ @poller.register_readable @child
235
+
236
+ # read on the socket(s) and call the registered reader blocks for every message, always using
237
+ # multipart and converting to ruby strings to avoid ZMQ::Message cleanup issues.
238
+ count = 0
239
+ @running = true
240
+ while @running
241
+ rc = @poller.poll(1)
242
+ unless rc > 0
243
+ sleep 0.01
244
+ next
245
+ end
246
+
247
+ @poller.readables.each do |sock|
248
+ rc = sock.recv_strings messages=[]
249
+ if rc > -1
250
+ if sock == @socket
251
+ count += 1
252
+ run_readers(messages, self)
253
+ # the main thread can send messages through to be resent or "exit" to shut down this thread
254
+ elsif sock == @child
255
+ if messages[0] == "exit"
256
+ verbose "Got exit message. Exiting."
257
+ @running = false
258
+ elsif messages[0] == "subscribe"
259
+ @socket.setsockopt ::ZMQ::SUBSCRIBE, messages[1]
260
+ else
261
+ @socket.send_strings messages
262
+ end
263
+ else
264
+ raise "BUG: couldn't match socket to a known socket"
265
+ end
266
+
267
+ # stop reading after a set number of messages, regardless of whether there are any more waiting
268
+ break if @limit and count >= @limit
269
+ break unless @running
270
+ else
271
+ @error_count += 1
272
+ break
273
+ end
274
+ end # @poller.readables.each
275
+
276
+ break if @limit and count >= @limit
277
+ end # while @running
278
+ end
279
+ end
280
+ end
data/lib/nodule.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "nodule/version"
2
+
3
+ require "nodule/console"
4
+ require "nodule/monkeypatch"
5
+ require "nodule/process"
6
+ require "nodule/base"
7
+ require "nodule/topology"
8
+
9
+ module Nodule
10
+ end
data/nodule.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "nodule/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "nodule"
7
+ s.version = Nodule::VERSION
8
+ s.authors = ["Al Tobey", "Noah Gibbs", "Viet Nguyen", "Jay Bhat"]
9
+ s.email = ["al@ooyala.com", "noah@ooyala.com", "viet@ooyala.com", "bhat@ooyala.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Nodule starts, stops, tests and redirects groups of processes}
12
+ s.description = %q{Nodule lets you declare Topologies of processes, which can be started or stopped together. You can also redirect sockets, set up interprocess communication, make assertions on captured packets between processes and generally monitor or change the interaction of any of your processes. Nodule is great for integration testing or for bringing up complicated interdependent sets of processes on a single host.}
13
+
14
+ s.rubyforge_project = "nodule"
15
+ s.required_ruby_version = ">= 1.9.2"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ s.add_development_dependency "minitest"
24
+ # s.add_runtime_dependency "rest-client"
25
+ s.add_runtime_dependency "ffi-rzmq"
26
+ s.add_runtime_dependency "cassandra", "=0.12.2ooyala2"
27
+ s.add_runtime_dependency "rainbow"
28
+ end
data/test/helper.rb ADDED
@@ -0,0 +1 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'helper'
4
+ require 'minitest/autorun'
5
+ require 'nodule/cassandra'
6
+
7
+ class NoduleCassandraTest < MiniTest::Unit::TestCase
8
+ KEYSPACE = "NoduleTest"
9
+
10
+ def test_cassandra
11
+ cass = nil
12
+ assert (cass = Nodule::Cassandra.new({:keyspace => KEYSPACE, :verbose => true})),
13
+ "Can't create Nodule::Cassandra!"
14
+
15
+ cass.run
16
+ cass.create_keyspace
17
+
18
+ assert_nil cass.waitpid
19
+
20
+ assert_kind_of Cassandra, cass.client
21
+
22
+ cfdef = CassandraThrift::CfDef.new :name => "foo", :keyspace => KEYSPACE
23
+ refute_nil cass.client.add_column_family cfdef
24
+
25
+ assert File.directory?(File.join(cass.data, KEYSPACE))
26
+
27
+ cass.stop
28
+
29
+ assert File.directory?(cass.tmp) != true
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/console'
7
+
8
+ class NoduleConsoleTest < MiniTest::Unit::TestCase
9
+ def test_console
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/line_io'
7
+
8
+ class NoduleLineIOTest < MiniTest::Unit::TestCase
9
+ def setup
10
+ @r_pipe, @w_pipe = IO.pipe
11
+ end
12
+
13
+ def test_stdio
14
+ io = Nodule::LineIO.new :io => @r_pipe, :run => true, :reader => :capture
15
+
16
+ @w_pipe.puts "x"
17
+ io.require_read_count 1, 10
18
+ assert_equal "x", io.output.first.chomp, "read data from pipe"
19
+
20
+ assert_equal 1, io.read_count
21
+ @w_pipe.puts "y"
22
+ io.require_read_count 2, 10
23
+
24
+ assert_equal 2, io.read_count
25
+ io.clear!
26
+ assert_equal 0, io.read_count
27
+ @w_pipe.puts "y"
28
+
29
+ io.require_read_count 1, 10
30
+ assert_equal "y", io.output.first.chomp, "read data from pipe"
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/process'
7
+
8
+ class NoduleProcessTest < MiniTest::Unit::TestCase
9
+ def test_process
10
+ true_bin = File.exist?("/bin/true") ? "/bin/true" : "/usr/bin/true"
11
+ p = Nodule::Process.new true_bin
12
+ p.run
13
+ p.wait 2
14
+ assert p.done?, "true exits immediately, done? should be true"
15
+ p.stop
16
+
17
+ echo = Nodule::Process.new '/bin/echo', 'foobar', :run => true
18
+ echo.wait 2
19
+
20
+ assert_equal 'foobar', echo.output.first.chomp
21
+ assert echo.done?, "true exits immediately, done? should be true"
22
+
23
+ echo.stop
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'helper'
4
+ require 'minitest/autorun'
5
+ require 'nodule/tempfile'
6
+
7
+ class NoduleTempfileTest < MiniTest::Unit::TestCase
8
+ def test_basic
9
+ tfile = nil
10
+ assert (tfile = Nodule::Tempfile.new), "Can't create Nodule::Tempfile!"
11
+
12
+ assert_kind_of Nodule::Tempfile, tfile
13
+ assert_kind_of Nodule::Base, tfile
14
+
15
+ assert (tfile = Nodule::Tempfile.new(:directory => true)),
16
+ "Can't create Nodule::Tempfile on a directory!"
17
+ assert_kind_of Nodule::Tempfile, tfile
18
+ assert_kind_of Nodule::Base, tfile
19
+
20
+ tfile.stop
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/topology'
7
+
8
+ class NoduleTopologyTest < MiniTest::Unit::TestCase
9
+ def test_topology
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/unixsocket'
7
+
8
+ class NoduleUnixsocketTest < MiniTest::Unit::TestCase
9
+ def test_unixsocket
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'helper'
4
+ require 'minitest/autorun'
5
+ require 'nodule/util'
6
+
7
+ class NoduleUtilTest < MiniTest::Unit::TestCase
8
+ def test_random_tcp_port
9
+ port = nil
10
+ assert (port = Nodule::Util.random_tcp_port),
11
+ "Can't create Nodule::Util with random TCP port!"
12
+ assert_kind_of Fixnum, port
13
+ assert port > 1024
14
+ assert port < 65536
15
+ end
16
+
17
+ def test_random_udp_port
18
+ port = nil
19
+ assert (port = Nodule::Util.random_udp_port),
20
+ "Can't create Nodule::Util with random UDP port!"
21
+ assert_kind_of Fixnum, port
22
+ assert port > 1024
23
+ assert port < 65536
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'minitest/autorun'
6
+ require 'nodule/zeromq'
7
+
8
+ class NoduleZeromqTest < MiniTest::Unit::TestCase
9
+ def test_zeromq_setup
10
+ t1 = Nodule::ZeroMQ.new(:uri => :gen)
11
+ refute_nil t1
12
+ t2 = Nodule::ZeroMQ.new(:uri => :gen, :bind => ZMQ::PUSH)
13
+ refute_nil t2
14
+ t3 = Nodule::ZeroMQ.new(:uri => t2.uri, :connect => ZMQ::PULL)
15
+ refute_nil t3
16
+
17
+ t1.stop
18
+ t2.stop
19
+ t3.stop
20
+ end
21
+
22
+ def test_zeromq_pubsub
23
+ pub = Nodule::ZeroMQ.new(:uri => :gen, :bind => ZMQ::PUB)
24
+ refute_nil pub
25
+ sub = Nodule::ZeroMQ.new(:uri => pub.uri, :connect => ZMQ::SUB, :reader => :capture)
26
+ refute_nil sub
27
+ pub.run
28
+ refute pub.done?
29
+ sub.run
30
+ refute sub.done?
31
+
32
+ pub.socket.send_string "Hello Verld!"
33
+ 5.times do
34
+ if sub.output.count > 0
35
+ assert_equal "Hello Verld!", sub.output.flatten[0]
36
+ break
37
+ end
38
+ sleep 0.1
39
+ end
40
+
41
+ pub.stop
42
+ sub.stop
43
+ end
44
+ end