multibinder 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/bin/multibinder +129 -0
- data/lib/multibinder.rb +33 -0
- data/lib/multibinder/version.rb +3 -0
- data/multibinder.gemspec +22 -0
- data/script/cibuild +13 -0
- data/script/test +15 -0
- data/test/haproxy_shim.rb +27 -0
- data/test/httpbinder.rb +21 -0
- data/test/lib-project.sh +22 -0
- data/test/lib.sh +282 -0
- data/test/test-haproxy.sh +29 -0
- data/test/test-simple.sh +99 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 04308acfb4e6a38f2ec0d7bd53b3bf921328ece0
|
4
|
+
data.tar.gz: cd6936ec293af4d11edf3d13e6e26a402c6dcb9e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 071505702ac618577faf5de290e3b87c6b0f52d4579fa08d259b501509d1b47a2b5e040eedabc32438e3e2b5bbe77cbbcc577269dba62b8d7fc69fb2eeac1e46
|
7
|
+
data.tar.gz: 7fbf21103b70a01ad7e87768774f6fd37eef0980f4375db09e3bb6996685969068b6b57ce1711b6962040e54177437077bec8226a8bba2d9cb5dcccfd0ec949b
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Theo Julienne
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
### multibinder
|
2
|
+
|
3
|
+
multibinder is a tiny ruby server that makes writing zero-downtime-reload services simpler. It accepts connections on a UNIX domain socket and binds an arbitrary number of LISTEN sockets given their ip+port combinations. When a bind is requested, the LISTEN socket is sent over the UNIX domain socket using ancillary data. Subsequent identical binds receive the same LISTEN socket.
|
4
|
+
|
5
|
+
multibinder runs on its own, separate from the daemons that use the sockets. multibinder can be re-exec itself to take upgrades by sending it a `SIGUSR1` - existing binds will be retained across re-execs.
|
6
|
+
|
7
|
+
#### Server usage
|
8
|
+
|
9
|
+
After installing multibinder, you can run the multibinder daemon:
|
10
|
+
|
11
|
+
```
|
12
|
+
bundle exec multibinder /path/to/control.sock
|
13
|
+
```
|
14
|
+
|
15
|
+
#### Client usage
|
16
|
+
|
17
|
+
The multibinder library retrieves a socket from a local multibinder server, communicating over the socket you specify in the `MULTIBINDER_SOCK` environment variable (which has to be the same as specified when running multibinder, and the user must have permission to access the file).
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require 'multibinder'
|
21
|
+
|
22
|
+
server = MultiBinder.bind '127.0.0.1', 8000
|
23
|
+
|
24
|
+
# use the server socket
|
25
|
+
# ... server.accept ...
|
26
|
+
```
|
27
|
+
|
28
|
+
The socket has close-on-exec disabled and is ready to be used in Ruby or passed on to a real service like haproxy via spawn/exec. For an example of using multibinder with haproxy (there are a couple of tricks), see [the haproxy test shim](https://github.com/theojulienne/multibinder/blob/master/test/haproxy_shim.rb).
|
data/Rakefile
ADDED
data/bin/multibinder
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class MultiBinderServer
|
7
|
+
def initialize(control_file)
|
8
|
+
@control_file = control_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_client(s)
|
12
|
+
loop do
|
13
|
+
msg, _, _, _ = s.recvmsg
|
14
|
+
break if msg.empty?
|
15
|
+
request = JSON.parse(msg)
|
16
|
+
puts "Request: #{request}"
|
17
|
+
|
18
|
+
case request['method']
|
19
|
+
when 'bind'
|
20
|
+
do_bind s, request
|
21
|
+
else
|
22
|
+
response = { :error => { :code => -32601, :message => 'Method not found' } }
|
23
|
+
s.sendmsg JSON.dump(response), 0, nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def bind_to_env(bind)
|
29
|
+
"MULTIBINDER_BIND__tcp__#{bind['address'].sub('.','_')}__#{bind['port']}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def do_bind(s, request)
|
33
|
+
bind = request['params'][0]
|
34
|
+
|
35
|
+
begin
|
36
|
+
name = bind_to_env(bind)
|
37
|
+
if ENV[name]
|
38
|
+
socket = IO.for_fd ENV[name].to_i
|
39
|
+
else
|
40
|
+
socket = Socket.new(:INET, :STREAM, 0)
|
41
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
42
|
+
socket.fcntl(Fcntl::F_SETFD, socket.fcntl(Fcntl::F_GETFD) & (-Fcntl::FD_CLOEXEC-1))
|
43
|
+
socket.bind(Addrinfo.tcp(bind['address'], bind['port']))
|
44
|
+
socket.listen(bind['backlog'] || 1000)
|
45
|
+
ENV[name] = socket.fileno.to_s
|
46
|
+
end
|
47
|
+
rescue Exception => e
|
48
|
+
response = {
|
49
|
+
:jsonrpc => '2.0',
|
50
|
+
:id => request['id'],
|
51
|
+
:error => {
|
52
|
+
:code => 10000,
|
53
|
+
:message => "Could not bind: #{e.message}",
|
54
|
+
:backtrace => e.backtrace,
|
55
|
+
},
|
56
|
+
}
|
57
|
+
s.sendmsg JSON.dump(response), 0, nil
|
58
|
+
return
|
59
|
+
else
|
60
|
+
response = {
|
61
|
+
:jsonrpc => '2.0',
|
62
|
+
:id => request['id'],
|
63
|
+
:result => true,
|
64
|
+
}
|
65
|
+
s.sendmsg JSON.dump(response), 0, nil, Socket::AncillaryData.unix_rights(socket)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def bind_accept_loop
|
70
|
+
UNIXServer.open(@control_file) do |serv|
|
71
|
+
puts "Listening for binds on control socket: #{@control_file}"
|
72
|
+
STDOUT.flush
|
73
|
+
|
74
|
+
Signal.trap("USR1") do
|
75
|
+
serv.close
|
76
|
+
begin
|
77
|
+
File.unlink @control_file
|
78
|
+
rescue Errno::ENOENT
|
79
|
+
# cool!
|
80
|
+
end
|
81
|
+
|
82
|
+
# respawn ourselved in an identical way, keeping state through environment.
|
83
|
+
puts 'Respawning...'
|
84
|
+
STDOUT.flush
|
85
|
+
args = [RbConfig.ruby, $0, *ARGV]
|
86
|
+
args << { :close_others => false } if RUBY_VERSION > '1.9' # RUBBY.
|
87
|
+
Kernel.exec *args
|
88
|
+
end
|
89
|
+
|
90
|
+
loop do
|
91
|
+
s = serv.accept
|
92
|
+
begin
|
93
|
+
handle_client s
|
94
|
+
rescue Exception => e
|
95
|
+
puts e
|
96
|
+
puts e.backtrace
|
97
|
+
ensure
|
98
|
+
s.close
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def serve
|
105
|
+
begin
|
106
|
+
File.unlink @control_file
|
107
|
+
puts "Removed existing control socket: #{@control_file}"
|
108
|
+
rescue Errno::ENOENT
|
109
|
+
# :+1:
|
110
|
+
end
|
111
|
+
|
112
|
+
begin
|
113
|
+
bind_accept_loop
|
114
|
+
ensure
|
115
|
+
begin
|
116
|
+
File.unlink @control_file
|
117
|
+
rescue Errno::ENOENT
|
118
|
+
# cool!
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
if ARGV.length == 0
|
125
|
+
abort "Usage: #{$0} <path/to/control.sock>"
|
126
|
+
else
|
127
|
+
server = MultiBinderServer.new ARGV[0]
|
128
|
+
server.serve
|
129
|
+
end
|
data/lib/multibinder.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'multibinder/version'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module MultiBinder
|
5
|
+
def self.bind(address, port)
|
6
|
+
abort 'MULTIBINDER_SOCK environment variable must be set' if !ENV['MULTIBINDER_SOCK']
|
7
|
+
|
8
|
+
binder = UNIXSocket.open(ENV['MULTIBINDER_SOCK'])
|
9
|
+
|
10
|
+
# make the request
|
11
|
+
binder.sendmsg JSON.dump({
|
12
|
+
:jsonrpc => '2.0',
|
13
|
+
:method => 'bind',
|
14
|
+
:params => [{
|
15
|
+
:address => address,
|
16
|
+
:port => port,
|
17
|
+
}]
|
18
|
+
}, 0, nil)
|
19
|
+
|
20
|
+
# get the response
|
21
|
+
msg, _, _, ctl = binder.recvmsg(:scm_rights=>true)
|
22
|
+
response = JSON.parse(msg)
|
23
|
+
if response['error']
|
24
|
+
raise response['error']['message']
|
25
|
+
end
|
26
|
+
|
27
|
+
binder.close
|
28
|
+
|
29
|
+
socket = ctl.unix_rights[0]
|
30
|
+
socket.fcntl(Fcntl::F_SETFD, socket.fcntl(Fcntl::F_GETFD) & (-Fcntl::FD_CLOEXEC-1))
|
31
|
+
socket
|
32
|
+
end
|
33
|
+
end
|
data/multibinder.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'multibinder/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "multibinder"
|
8
|
+
spec.version = MultiBinder::VERSION
|
9
|
+
spec.authors = ["Theo Julienne"]
|
10
|
+
spec.email = ["theojulienne@github.com"]
|
11
|
+
spec.summary = %q{multibinder is a tiny ruby server that makes writing zero-downtime-reload services simpler.}
|
12
|
+
spec.homepage = "https://github.com/theojulienne/multibinder"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
end
|
data/script/cibuild
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
export RUBY_VERSION=2.1.6-github
|
4
|
+
export RBENV_VERSION=2.1.6-github
|
5
|
+
export PATH=/usr/share/rbenv/shims:/usr/sbin:$PATH
|
6
|
+
export BASE_DIR=/data/binder
|
7
|
+
|
8
|
+
#bundle install --local --quiet --path vendor/gems
|
9
|
+
|
10
|
+
REALPATH=$(cd $(dirname "$0") && pwd)
|
11
|
+
|
12
|
+
cd $REALPATH/../
|
13
|
+
script/test
|
data/script/test
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'multibinder'
|
2
|
+
|
3
|
+
server = MultiBinder.bind '127.0.0.1', ARGV[0].to_i
|
4
|
+
|
5
|
+
cfg_fn = "#{ENV['TEMPDIR']}/haproxy.cfg"
|
6
|
+
|
7
|
+
File.write(cfg_fn, <<eos)
|
8
|
+
global
|
9
|
+
maxconn 256
|
10
|
+
|
11
|
+
defaults
|
12
|
+
mode http
|
13
|
+
timeout connect 5000ms
|
14
|
+
timeout client 50000ms
|
15
|
+
timeout server 50000ms
|
16
|
+
|
17
|
+
frontend http-in
|
18
|
+
bind fd@#{server.fileno}
|
19
|
+
http-request deny
|
20
|
+
eos
|
21
|
+
|
22
|
+
pid = Process.spawn "haproxy", "-f", cfg_fn, :close_others => false
|
23
|
+
|
24
|
+
Signal.trap("INT") { Process.kill "USR1", pid }
|
25
|
+
Signal.trap("TERM") { Process.kill "USR1", pid }
|
26
|
+
|
27
|
+
Process.waitpid
|
data/test/httpbinder.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'json'
|
3
|
+
require 'multibinder'
|
4
|
+
|
5
|
+
server = MultiBinder.bind '127.0.0.1', ARGV[0].to_i
|
6
|
+
|
7
|
+
loop do
|
8
|
+
socket, _ = server.accept
|
9
|
+
request = socket.gets
|
10
|
+
puts request
|
11
|
+
|
12
|
+
socket.print "HTTP/1.0 200 OK\r\n"
|
13
|
+
socket.print "Content-Type: text/plain\r\n"
|
14
|
+
socket.print "Connection: close\r\n"
|
15
|
+
|
16
|
+
socket.print "\r\n"
|
17
|
+
|
18
|
+
socket.print "Hello World #{ARGV[1] || ''}!\n"
|
19
|
+
|
20
|
+
socket.close
|
21
|
+
end
|
data/test/lib-project.sh
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Extends lib.sh for this project
|
3
|
+
|
4
|
+
project_setup () {
|
5
|
+
local control_sock=${TEMPDIR}/multibinder.sock
|
6
|
+
launch_service "multibinder" bundle exec multibinder ${control_sock}
|
7
|
+
|
8
|
+
tries=0
|
9
|
+
while [ ! -S $control_sock ]; do
|
10
|
+
sleep .1
|
11
|
+
echo 'Waiting for control socket...'
|
12
|
+
tries=$((tries + 1))
|
13
|
+
if [ $tries -gt 10 ]; then
|
14
|
+
echo 'Giving up.'
|
15
|
+
exit 1
|
16
|
+
fi
|
17
|
+
done
|
18
|
+
}
|
19
|
+
|
20
|
+
project_cleanup () {
|
21
|
+
kill_service "multibinder"
|
22
|
+
}
|
data/test/lib.sh
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Usage: . lib.sh
|
3
|
+
# Simple shell command language test library.
|
4
|
+
#
|
5
|
+
# Tests must follow the basic form:
|
6
|
+
#
|
7
|
+
# begin_test "the thing"
|
8
|
+
# (
|
9
|
+
# set -e
|
10
|
+
# echo "hello"
|
11
|
+
# false
|
12
|
+
# )
|
13
|
+
# end_test
|
14
|
+
#
|
15
|
+
# When a test fails its stdout and stderr are shown.
|
16
|
+
#
|
17
|
+
# Note that tests must `set -e' within the subshell block or failed assertions
|
18
|
+
# will not cause the test to fail and the result may be misreported.
|
19
|
+
#
|
20
|
+
# Copyright (c) 2011-13 by Ryan Tomayko <http://tomayko.com>
|
21
|
+
# License: MIT
|
22
|
+
|
23
|
+
set -e
|
24
|
+
|
25
|
+
TEST_DIR=$(dirname "$0")
|
26
|
+
ROLE=$(basename $(dirname "$0"))
|
27
|
+
BASE_DIR=$(cd $(dirname "$0")/../ && pwd)
|
28
|
+
|
29
|
+
TEMPDIR=$(mktemp -d /tmp/test-XXXXXX)
|
30
|
+
HOME=$TEMPDIR; export HOME
|
31
|
+
TRASHDIR="${TEMPDIR}"
|
32
|
+
LOGDIR="$TEMPDIR/log"
|
33
|
+
|
34
|
+
BUILD_DIR=${TEMPDIR}/build
|
35
|
+
ROLE_DIR=$BUILD_DIR
|
36
|
+
|
37
|
+
# keep track of num tests and failures
|
38
|
+
tests=0
|
39
|
+
failures=0
|
40
|
+
|
41
|
+
#mkdir -p $TRASHDIR
|
42
|
+
mkdir -p $LOGDIR
|
43
|
+
|
44
|
+
# offset port numbers if running in '--batch' mode
|
45
|
+
TEST_PORT_OFFSET=0
|
46
|
+
if [ "$1" = "--batch" ]; then
|
47
|
+
TEST_PORT_OFFSET=$(ls -1 $(dirname "$0")/test-*.sh | grep -n $(basename "$0") | grep -o "^[0-9]*")
|
48
|
+
TEST_PORT_OFFSET=$(( $TEST_PORT_OFFSET - 1 ))
|
49
|
+
fi
|
50
|
+
|
51
|
+
# Sanity check up front that nothing is currently using the ports we're
|
52
|
+
# trying use; port collisions will cause non-obvious test failures.
|
53
|
+
tests_use_port () {
|
54
|
+
local p=$(offset_port $1)
|
55
|
+
|
56
|
+
set +e
|
57
|
+
lsof -n -iTCP:$p | grep -q LISTEN
|
58
|
+
if [ $? -eq 0 ]; then
|
59
|
+
echo "**** $(basename "$0") FAIL: Found something using port $p, bailing."
|
60
|
+
lsof -n -iTCP:$p | grep -e ":$p" | sed -e "s/^/lsof failure $(basename "$0"): /"
|
61
|
+
exit 1
|
62
|
+
fi
|
63
|
+
set -e
|
64
|
+
}
|
65
|
+
|
66
|
+
# Given a port, increment it by our test number so multiple tests can run
|
67
|
+
# in parallel without conflicting.
|
68
|
+
offset_port () {
|
69
|
+
local base_port="$1"
|
70
|
+
|
71
|
+
echo $(( $base_port + $TEST_PORT_OFFSET ))
|
72
|
+
}
|
73
|
+
|
74
|
+
# Mark the beginning of a test. A subshell should immediately follow this
|
75
|
+
# statement.
|
76
|
+
begin_test () {
|
77
|
+
test_status=$?
|
78
|
+
[ -n "$test_description" ] && end_test $test_status
|
79
|
+
unset test_status
|
80
|
+
|
81
|
+
tests=$(( tests + 1 ))
|
82
|
+
test_description="$1"
|
83
|
+
|
84
|
+
exec 3>&1 4>&2
|
85
|
+
out="$TRASHDIR/out"
|
86
|
+
err="$TRASHDIR/err"
|
87
|
+
exec 1>"$out" 2>"$err"
|
88
|
+
|
89
|
+
echo "begin_test: $test_description"
|
90
|
+
|
91
|
+
# allow the subshell to exit non-zero without exiting this process
|
92
|
+
set -x +e
|
93
|
+
before_time=$(date '+%s')
|
94
|
+
}
|
95
|
+
|
96
|
+
report_failure () {
|
97
|
+
msg=$1
|
98
|
+
desc=$2
|
99
|
+
failures=$(( failures + 1 ))
|
100
|
+
printf "test: %-60s $msg\n" "$desc ..."
|
101
|
+
(
|
102
|
+
echo "-- stdout --"
|
103
|
+
sed 's/^/ /' <"$TRASHDIR/out"
|
104
|
+
echo "-- stderr --"
|
105
|
+
grep -a -v -e '^\+ end_test' -e '^+ set +x' <"$TRASHDIR/err" |
|
106
|
+
sed 's/^/ /'
|
107
|
+
|
108
|
+
for service_log in $(ls $LOGDIR/*.log); do
|
109
|
+
echo "-- $(basename "$service_log") --"
|
110
|
+
sed 's/^/ /' <"$service_log"
|
111
|
+
done
|
112
|
+
|
113
|
+
echo "-- end --"
|
114
|
+
) 1>&2
|
115
|
+
}
|
116
|
+
|
117
|
+
# Mark the end of a test.
|
118
|
+
end_test () {
|
119
|
+
test_status="${1:-$?}"
|
120
|
+
ex_fail="${2:-0}"
|
121
|
+
after_time=$(date '+%s')
|
122
|
+
set +x -e
|
123
|
+
exec 1>&3 2>&4
|
124
|
+
elapsed_time=$((after_time - before_time))
|
125
|
+
|
126
|
+
if [ "$test_status" -eq 0 ]; then
|
127
|
+
if [ "$ex_fail" -eq 0 ]; then
|
128
|
+
printf "test: %-60s OK (${elapsed_time}s)\n" "$test_description ..."
|
129
|
+
else
|
130
|
+
report_failure "OK (unexpected)" "$test_description ..."
|
131
|
+
fi
|
132
|
+
else
|
133
|
+
if [ "$ex_fail" -eq 0 ]; then
|
134
|
+
report_failure "FAILED (${elapsed_time}s)" "$test_description ..."
|
135
|
+
else
|
136
|
+
printf "test: %-60s FAILED (expected)\n" "$test_description ..."
|
137
|
+
fi
|
138
|
+
fi
|
139
|
+
unset test_description
|
140
|
+
}
|
141
|
+
|
142
|
+
# Mark the end of a test that is expected to fail.
|
143
|
+
end_test_exfail () {
|
144
|
+
end_test $? 1
|
145
|
+
}
|
146
|
+
|
147
|
+
atexit () {
|
148
|
+
[ -z "$KEEPTRASH" ] && rm -rf "$TEMPDIR"
|
149
|
+
if [ $failures -gt 0 ]; then
|
150
|
+
exit 1
|
151
|
+
else
|
152
|
+
exit 0
|
153
|
+
fi
|
154
|
+
}
|
155
|
+
trap "atexit" EXIT
|
156
|
+
|
157
|
+
cleanup() {
|
158
|
+
set +e
|
159
|
+
|
160
|
+
project_cleanup "$@"
|
161
|
+
|
162
|
+
for pid_file in $(ls ${TEMPDIR}/*.pid); do
|
163
|
+
echo "Cleaning up process in $pid_file ..."
|
164
|
+
kill $(cat ${pid_file}) || true
|
165
|
+
done
|
166
|
+
|
167
|
+
echo "Cleaning up any remaining pid files."
|
168
|
+
rm -rf ${TEMPDIR}/*.pid
|
169
|
+
|
170
|
+
if [ -f "$TEMPDIR/core" ]; then
|
171
|
+
echo "found a coredump, failing"
|
172
|
+
exit 1
|
173
|
+
fi
|
174
|
+
}
|
175
|
+
|
176
|
+
setup() {
|
177
|
+
trap cleanup EXIT
|
178
|
+
trap cleanup INT
|
179
|
+
trap cleanup TERM
|
180
|
+
|
181
|
+
project_setup "$@"
|
182
|
+
|
183
|
+
set -e
|
184
|
+
}
|
185
|
+
|
186
|
+
wait_for_file () {
|
187
|
+
(
|
188
|
+
SERVICE="$1"
|
189
|
+
PID_FILE="$2"
|
190
|
+
|
191
|
+
set +e
|
192
|
+
|
193
|
+
tries=0
|
194
|
+
|
195
|
+
echo "Waiting for $SERVICE to drop $PID_FILE"
|
196
|
+
while [ ! -e "$PID_FILE" ]; do
|
197
|
+
tries=$(( $tries + 1 ))
|
198
|
+
if [ $tries -gt 50 ]; then
|
199
|
+
echo "FAILED: $SERVICE did not drop $PID_FILE after $tries attempts"
|
200
|
+
exit 1
|
201
|
+
fi
|
202
|
+
echo "Waiting for $SERVICE to drop $PID_FILE"
|
203
|
+
sleep 0.1
|
204
|
+
done
|
205
|
+
echo "OK -- $SERVICE dropped $PID_FILE"
|
206
|
+
exit 0
|
207
|
+
)
|
208
|
+
}
|
209
|
+
|
210
|
+
# wait for a process to start accepting connections
|
211
|
+
wait_for_port () {
|
212
|
+
(
|
213
|
+
SERVICE="$1"
|
214
|
+
SERVICE_PORT="$2"
|
215
|
+
|
216
|
+
set +e
|
217
|
+
|
218
|
+
tries=0
|
219
|
+
|
220
|
+
echo "Waiting for $SERVICE to start accepting connections"
|
221
|
+
if [ $(uname) = "Linux" ]; then
|
222
|
+
echo "PROXY TCP4 127.0.0.1 127.0.0.1 123 123\r" | nc -q 0 localhost $SERVICE_PORT 2>&1 >/dev/null
|
223
|
+
else
|
224
|
+
echo "PROXY TCP4 127.0.0.1 127.0.0.1 123 123\r" | nc localhost $SERVICE_PORT 2>&1 >/dev/null
|
225
|
+
fi
|
226
|
+
while [ $? -ne 0 ]; do
|
227
|
+
tries=$(( $tries + 1 ))
|
228
|
+
if [ $tries -gt 50 ]; then
|
229
|
+
echo "FAILED: $SERVICE not accepting connections after $tries attempts"
|
230
|
+
exit 1
|
231
|
+
fi
|
232
|
+
echo "Waiting for $SERVICE to start accepting connections"
|
233
|
+
sleep 0.1
|
234
|
+
if [ $(uname) = "Linux" ]; then
|
235
|
+
echo "PROXY TCP4 127.0.0.1 127.0.0.1 123 123\r" | nc -q 0 localhost $SERVICE_PORT 2>&1 >/dev/null
|
236
|
+
else
|
237
|
+
echo "PROXY TCP4 127.0.0.1 127.0.0.1 123 123\r" | nc localhost $SERVICE_PORT 2>&1 >/dev/null
|
238
|
+
fi
|
239
|
+
done
|
240
|
+
echo "OK -- $SERVICE seems to be accepting connections"
|
241
|
+
exit 0
|
242
|
+
)
|
243
|
+
}
|
244
|
+
|
245
|
+
# Allow simple launching of a background service, keeping track of the pid
|
246
|
+
launch_service () {
|
247
|
+
local service_name=$1
|
248
|
+
shift
|
249
|
+
|
250
|
+
"$@" >${LOGDIR}/${service_name}.log 2>&1 &
|
251
|
+
echo "$!" > ${TEMPDIR}/${service_name}.pid
|
252
|
+
}
|
253
|
+
|
254
|
+
# Clean up after a service launched with launch_service
|
255
|
+
kill_service () {
|
256
|
+
local service_name=$1
|
257
|
+
kill $(cat ${TEMPDIR}/${service_name}.pid) || true
|
258
|
+
rm -rf ${TEMPDIR}/${service_name}.pid
|
259
|
+
}
|
260
|
+
|
261
|
+
service_pid () {
|
262
|
+
local service_name=$1
|
263
|
+
cat ${TEMPDIR}/${service_name}.pid
|
264
|
+
}
|
265
|
+
|
266
|
+
service_log () {
|
267
|
+
local service_name=$1
|
268
|
+
echo ${LOGDIR}/${service_name}.log
|
269
|
+
}
|
270
|
+
|
271
|
+
# Stub out functions and let the project extend them
|
272
|
+
project_setup () {
|
273
|
+
true
|
274
|
+
}
|
275
|
+
|
276
|
+
project_cleanup () {
|
277
|
+
true
|
278
|
+
}
|
279
|
+
|
280
|
+
if [ -e "$BASE_DIR/test/lib-project.sh" ]; then
|
281
|
+
. $BASE_DIR/test/lib-project.sh
|
282
|
+
fi
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# test-haproxy.sh: check that we can work with haproxy (if it's installed)
|
4
|
+
|
5
|
+
REALPATH=$(cd $(dirname "$0") && pwd)
|
6
|
+
. "${REALPATH}/lib.sh"
|
7
|
+
|
8
|
+
TEST_PORT=8000
|
9
|
+
|
10
|
+
tests_use_port $TEST_PORT
|
11
|
+
|
12
|
+
if ! which -s haproxy; then
|
13
|
+
echo "haproxy not available, skipping tests."
|
14
|
+
exit 0
|
15
|
+
fi
|
16
|
+
|
17
|
+
begin_test "haproxy runs with multibinder"
|
18
|
+
(
|
19
|
+
setup
|
20
|
+
|
21
|
+
export TEMPDIR
|
22
|
+
|
23
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "haproxy" bundle exec ruby test/haproxy_shim.rb $(offset_port $TEST_PORT)
|
24
|
+
|
25
|
+
wait_for_port "haproxy" $(offset_port $TEST_PORT)
|
26
|
+
|
27
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/ | grep -q 'Request forbidden'
|
28
|
+
)
|
29
|
+
end_test
|
data/test/test-simple.sh
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# test-simple.sh: simple sanity checks
|
4
|
+
|
5
|
+
REALPATH=$(cd $(dirname "$0") && pwd)
|
6
|
+
. "${REALPATH}/lib.sh"
|
7
|
+
|
8
|
+
TEST_PORT=8000
|
9
|
+
|
10
|
+
tests_use_port $TEST_PORT
|
11
|
+
|
12
|
+
begin_test "server binds and accepts through multibinder"
|
13
|
+
(
|
14
|
+
setup
|
15
|
+
|
16
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT)
|
17
|
+
|
18
|
+
wait_for_port "binder" $(offset_port $TEST_PORT)
|
19
|
+
|
20
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/ | grep -q 'Hello World'
|
21
|
+
)
|
22
|
+
end_test
|
23
|
+
|
24
|
+
begin_test "server can restart without requests failing while down"
|
25
|
+
(
|
26
|
+
setup
|
27
|
+
|
28
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT)
|
29
|
+
|
30
|
+
wait_for_port "binder" $(offset_port $TEST_PORT)
|
31
|
+
|
32
|
+
kill_service "http"
|
33
|
+
|
34
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/ | grep -q 'Hello World' &
|
35
|
+
curl_pid=$!
|
36
|
+
|
37
|
+
sleep 0.5
|
38
|
+
|
39
|
+
# now restart the service
|
40
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT)
|
41
|
+
|
42
|
+
# curl should finish, and succeed
|
43
|
+
wait $curl_pid
|
44
|
+
)
|
45
|
+
end_test
|
46
|
+
|
47
|
+
|
48
|
+
begin_test "server can load a second copy then terminate the first"
|
49
|
+
(
|
50
|
+
setup
|
51
|
+
|
52
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT) "first"
|
53
|
+
wait_for_port "binder" $(offset_port $TEST_PORT)
|
54
|
+
|
55
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/r1 | grep -q 'Hello World first'
|
56
|
+
|
57
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http2" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT) "second"
|
58
|
+
|
59
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/r2 | egrep -q 'Hello World (first|second)'
|
60
|
+
|
61
|
+
kill_service "http"
|
62
|
+
|
63
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/r3 | grep -q 'Hello World second'
|
64
|
+
|
65
|
+
kill_service "http2"
|
66
|
+
)
|
67
|
+
end_test
|
68
|
+
|
69
|
+
begin_test "multibinder restarts safely on sigusr1"
|
70
|
+
(
|
71
|
+
setup
|
72
|
+
|
73
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT)
|
74
|
+
wait_for_port "binder" $(offset_port $TEST_PORT)
|
75
|
+
|
76
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/r1 | grep -q 'Hello World'
|
77
|
+
|
78
|
+
kill_service "http"
|
79
|
+
|
80
|
+
lsof -p $(service_pid "multibinder")
|
81
|
+
|
82
|
+
kill -USR1 $(service_pid "multibinder")
|
83
|
+
|
84
|
+
# should still be running, should still be listening
|
85
|
+
lsof -p $(service_pid "multibinder")
|
86
|
+
lsof -i :$(offset_port $TEST_PORT) -a -p $(service_pid "multibinder")
|
87
|
+
|
88
|
+
# should be able to request the bind again
|
89
|
+
MULTIBINDER_SOCK=${TEMPDIR}/multibinder.sock launch_service "http" bundle exec ruby test/httpbinder.rb $(offset_port $TEST_PORT)
|
90
|
+
wait_for_port "multibinder" $(offset_port $TEST_PORT)
|
91
|
+
|
92
|
+
# requests should work
|
93
|
+
curl --max-time 5 http://localhost:$(offset_port $TEST_PORT)/r2 | grep -q 'Hello World'
|
94
|
+
|
95
|
+
# and multibinder should have started listening on the control socket twice
|
96
|
+
grep 'Respawning' $(service_log "multibinder")
|
97
|
+
grep 'Listening for binds' $(service_log "multibinder") | grep -n 'Listen' | grep "2:"
|
98
|
+
)
|
99
|
+
end_test
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multibinder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Theo Julienne
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- theojulienne@github.com
|
44
|
+
executables:
|
45
|
+
- multibinder
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- bin/multibinder
|
55
|
+
- lib/multibinder.rb
|
56
|
+
- lib/multibinder/version.rb
|
57
|
+
- multibinder.gemspec
|
58
|
+
- script/cibuild
|
59
|
+
- script/test
|
60
|
+
- test/haproxy_shim.rb
|
61
|
+
- test/httpbinder.rb
|
62
|
+
- test/lib-project.sh
|
63
|
+
- test/lib.sh
|
64
|
+
- test/test-haproxy.sh
|
65
|
+
- test/test-simple.sh
|
66
|
+
homepage: https://github.com/theojulienne/multibinder
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.2.3
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: multibinder is a tiny ruby server that makes writing zero-downtime-reload
|
90
|
+
services simpler.
|
91
|
+
test_files:
|
92
|
+
- test/haproxy_shim.rb
|
93
|
+
- test/httpbinder.rb
|
94
|
+
- test/lib-project.sh
|
95
|
+
- test/lib.sh
|
96
|
+
- test/test-haproxy.sh
|
97
|
+
- test/test-simple.sh
|
98
|
+
has_rdoc:
|