diff options
author | Pablo Carranza <pcarranza@gmail.com> | 2015-12-26 00:27:02 +0100 |
---|---|---|
committer | Pablo Carranza <pcarranza@gmail.com> | 2015-12-27 22:19:55 +0100 |
commit | 07a0950a8d191b6dd69598e38986dca9880eec5f (patch) | |
tree | 48ae3e70998896d7779d86de601ded551a91b017 | |
parent | 06d5594c82655734455e5c9b2f8c94db9829aeb2 (diff) | |
download | bundler-07a0950a8d191b6dd69598e38986dca9880eec5f.tar.gz |
Add TCP Mirror probe object to verify that the mirror is reachable
The implementation is done using nonblocking IO and then using select
to probe that the socket is writable once it is open.
This is not covering the case of the mirror host not being resolved,
this would fail with a nasty error.
There are at least 3 possible cases of probing:
* the socket is writtable just fine, which probes as true
* the socket fails to `select`, this means that the packets are being
dropped or that there is nothing listening there
* the socket fails to get a valid tcp connection, there is something
listening, but it is rejecting packets: closed connection
-rw-r--r-- | lib/bundler/mirror.rb | 64 | ||||
-rw-r--r-- | spec/bundler/mirror_spec.rb | 36 |
2 files changed, 92 insertions, 8 deletions
diff --git a/lib/bundler/mirror.rb b/lib/bundler/mirror.rb index 0a3e34801d..a18d3528f4 100644 --- a/lib/bundler/mirror.rb +++ b/lib/bundler/mirror.rb @@ -91,12 +91,6 @@ module Bundler end end - class TCPSocketProbe - def replies?(mirror) - true - end - end - private class MirrorConfig @@ -127,5 +121,63 @@ module Bundler end end end + + class TCPSocketProbe + def replies?(mirror) + MirrorSocket.new(mirror).with_socket do |socket, address, timeout| + begin + socket.connect_nonblock(address) + rescue IO::WaitWritable + wait_for_writtable_socket(socket, address, timeout) + rescue # Connection failed somehow, again + false + end + end + end + + private + + def wait_for_writtable_socket(socket, address, timeout) + if IO.select(nil, [socket], nil, timeout) + probe_writtable_socket(socket, address) + else # TCP Handshake timed out, or there is something dropping packets + false + end + end + + def probe_writtable_socket(socket, address) + begin + socket.connect_nonblock(address) + rescue Errno::EISCONN + true + rescue => e # Connection failed + false + end + end + end + end + + class MirrorSocket + def initialize(mirror) + addr_info = Socket.getaddrinfo(mirror.uri.host, mirror.uri.port) + @timeout = mirror.fallback_timeout + @type = addr_info[0][0] + @port = addr_info[0][1] + @host = addr_info[0][3] + end + + def with_socket + socket = Socket.new(Socket.const_get(@type), Socket::SOCK_STREAM, 0) + socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + value = yield socket, socket_address, @timeout + socket.close unless socket.closed? + value + end + + private + + def socket_address + Socket.pack_sockaddr_in(@port, @host) + end end end diff --git a/spec/bundler/mirror_spec.rb b/spec/bundler/mirror_spec.rb index faa16b7aa8..6e5cede084 100644 --- a/spec/bundler/mirror_spec.rb +++ b/spec/bundler/mirror_spec.rb @@ -136,8 +136,12 @@ end describe Bundler::Settings::Mirrors do let(:localhost_uri) { URI("http://localhost:9292") } - context "with a not configured mirror" do - let(:mirrors) { Bundler::Settings::Mirrors.new } + context "with a just created mirror" do + let(:mirrors) do + probe = double() + allow(probe).to receive(:replies?).and_return(true) + Bundler::Settings::Mirrors.new(probe) + end it "returns a mirror that contains the source uri for an unknown uri" do mirror = mirrors.for("http://rubygems.org/") @@ -267,3 +271,31 @@ describe Bundler::Settings::Mirrors do end end end + +describe Bundler::Settings::TCPSocketProbe do + let(:probe) { Bundler::Settings::TCPSocketProbe.new } + + context "with a listening TCP Server" do + let(:server) { TCPServer.new("127.0.0.1", 0) } + let(:port) { server.addr[1] } + let(:mirror) { Bundler::Settings::Mirror.new(uri="http://localhost:#{port}", fallback_timeout=true) } + + it "probes the server correctly" do + expect(probe.replies?(mirror)).to be_truthy + end + + it "probes falsey when the server is down" do + my_mirror = mirror + server.close + expect(probe.replies?(my_mirror)).to be_falsey + end + end + + context "with an invalid mirror" do + let(:mirror) { Bundler::Settings::Mirror.new(uri="http://127.0.0.127:#{9292}", fallback_timeout=true) } + + it "fails with a timeout when there is nothing to tcp handshake" do + expect(probe.replies?(mirror)).to be_falsey + end + end +end |