Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ConnectionPool connect_timeout and idle_timeout #215

Merged
merged 8 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 60 additions & 7 deletions src/ConnectionPool.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Fields:
- `host::String`
- `port::String`, exactly as specified in the URI (i.e. may be empty).
- `pipeline_limit`, number of requests to send before waiting for responses.
- `idle_timeout`, No. of sconds to maintain connection after last transaction.
- `peerport`, remote TCP port number (used for debug messages).
- `localport`, local TCP port number (used for debug messages).
- `io::T`, the `TCPSocket` or `SSLContext.
Expand All @@ -68,6 +69,7 @@ mutable struct Connection{T <: IO}
host::String
port::String
pipeline_limit::Int
idle_timeout::Int
peerport::UInt16
localport::UInt16
io::T
Expand Down Expand Up @@ -95,16 +97,17 @@ struct Transaction{T <: IO} <: IO
end

Connection(host::AbstractString, port::AbstractString,
pipeline_limit::Int, io::T) where T <: IO =
Connection{T}(host, port, pipeline_limit,
pipeline_limit::Int, idle_timeout::Int, io::T) where T <: IO =
Connection{T}(host, port,
pipeline_limit, idle_timeout,
peerport(io), localport(io),
io, nobytes,
-1,
0, false, Condition(),
0, false, Condition(),
0)

Connection(io) = Connection("", "", default_pipeline_limit, io)
Connection(io) = Connection("", "", default_pipeline_limit, 0, io)

Transaction(c::Connection{T}) where T <: IO =
Transaction{T}(c, (c.sequence += 1))
Expand Down Expand Up @@ -139,7 +142,7 @@ function Base.eof(t::Transaction)
@require isreadable(t) || !isopen(t)
if bytesavailable(t) > 0
return false
end ;@debug 4 "eof(::Transaction) -> eof($(typeof(c.io))): $t"
end ;@debug 4 "eof(::Transaction) -> eof($(typeof(t.c.io))): $t"
return eof(t.c.io)
end

Expand Down Expand Up @@ -392,6 +395,17 @@ end
Remove closed connections from `pool`.
"""
function purge()

for c in pool
if c.idle_timeout > 0 &&
!c.readbusy &&
!c.writebusy &&
time() - c.timestamp > c.idle_timeout

close(c.io) ;@debug 1 "⌛️ Timeout: $c"
end
end

isdeletable(c) = !isopen(c.io) && (@debug 1 "🗑 Deleted: $c"; true)
deleteat!(pool, map(isdeletable, pool))
end
Expand All @@ -407,6 +421,7 @@ function getconnection(::Type{Transaction{T}},
port::AbstractString;
connection_limit::Int=default_connection_limit,
pipeline_limit::Int=default_pipeline_limit,
idle_timeout::Int=0,
reuse_limit::Int=nolimit,
kw...)::Transaction{T} where T <: IO

Expand Down Expand Up @@ -438,7 +453,9 @@ function getconnection(::Type{Transaction{T}},
busy = findall(T, host, port, pipeline_limit)
if length(busy) < connection_limit
io = getconnection(T, host, port; kw...)
c = Connection(host, port, pipeline_limit, io)
c = Connection(host, port,
pipeline_limit, idle_timeout,
io)
push!(pool, c) ;@debug 1 "🔗 New: $c"
return client_transaction(c)
end
Expand Down Expand Up @@ -466,14 +483,50 @@ function keepalive!(tcp)
return
end

struct ConnectTimeout <: Exception
host
port
end

function getconnection(::Type{TCPSocket},
host::AbstractString,
port::AbstractString;
keepalive::Bool=false,
kw...)::TCPSocket
connect_timeout::Int=0,
kw...)::Sockets.TCP

p::UInt = isempty(port) ? UInt(80) : parse(UInt, port)

@debug 2 "TCP connect: $host:$p..."
tcp = Sockets.connect(Sockets.getaddrinfo(host), p)

if connect_timeout == 0
tcp = Sockets.connect(Sockets.getaddrinfo(host), p)
keepalive && keepalive!(tcp)
return tcp
end

tcp = Sockets.TCPSocket()
Base.connect!(tcp, Sockets.getaddrinfo(host), p)

timeout = Ref{Bool}(false)
@schedule begin
sleep(connect_timeout)
if tcp.status == Base.StatusConnecting
timeout[] = true
tcp.status = Base.StatusClosing
ccall(:jl_forceclose_uv, Void, (Ptr{Void},), tcp.handle)
#close(tcp)
end
end
try
Base.wait_connected(tcp)
catch e
if timeout[]
throw(ConnectTimeout(host, port))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to make sure it throws this exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I don't think UVError should ever be thrown from a user-level API unless it means there is a bug. See JuliaLang/julia#15879 etc.

I think application level code will want to handle "I set a short timeout and it wasn't met" differently from "something bad happened deep in the network stack.

Elsewhere in HTTP.jl we wrap things like UVError and ArgumentError "stream is closed or unusable" in an IOError type:
https://github.com/JuliaWeb/HTTP.jl/blob/master/src/ConnectionRequest.jl#L36

In higher level code it is important to handle some specific errors by retrying but make sure everything else finds its way up to create a crash and a stack trace and an issue report and a fix. e.g. https://github.com/JuliaCloud/AWSCore.jl/blob/master/src/http.jl#L47-L51

Copy link
Contributor Author

@samoconnor samoconnor Mar 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArgumentError "stream is closed or unusable" seems pretty wrong because it looks like a precondition error, but there is now way to check the precondition before calling the API because the stream can be isopen() before the call but be found to be "closed or unusable" while the call is running.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @vtjnash, I just noticed that poll_fd has a timeout_s option. Would that work here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how that's relevant here, since you don't have a fd. But yes, that does show another pattern that can be used to implement timeouts (with some more difficulty ... in fact, you've just inspired me to rewrite that file better ... you may see a new PR to base tomorrow sometime 🙂)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was assuming that I could get the fd out of the socket with uv_fileno or something like that.
Is there a gotcha along the lines of windows socket handles won't work with poll_fd?

new PR

👍🙃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(a) the socket isn't open, so you can't poll it (b) it's invalid to pass libuv-derived fd handles to libuv

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for taking the time to explain :)

end
rethrow(e)
end

keepalive && keepalive!(tcp)
return tcp
end
Expand Down
2 changes: 1 addition & 1 deletion src/Servers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ function listen(f::Function,
continue
end
io = ssl ? getsslcontext(io, sslconfig) : io
let io = Connection(host, string(port), pipeline_limit, io)
let io = Connection(host, string(port), pipeline_limit, 0, io)
@info "Accept: $io"
@async try
handle_connection(f, io; kw...)
Expand Down