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 function to report metrics for a HTTP.Connection.Pool #1116

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
158 changes: 158 additions & 0 deletions Manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# This file is machine-generated - editing it directly is not advised
nickrobinson251 marked this conversation as resolved.
Show resolved Hide resolved

julia_version = "1.9.2"
manifest_format = "2.0"
project_hash = "c46963b102670aecccdbc101e6269347b2e132f5"

[[deps.Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.BitFlags]]
git-tree-sha1 = "43b1a4a8f797c1cddadf60499a8a077d4af2cd2d"
uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35"
version = "0.1.7"

[[deps.CodecZlib]]
deps = ["TranscodingStreams", "Zlib_jll"]
git-tree-sha1 = "9c209fb7536406834aa938fb149964b985de6c83"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.1"

[[deps.ConcurrentUtilities]]
deps = ["Serialization", "Sockets"]
git-tree-sha1 = "584a4f8d88293ad5eaa5de6dff3e5066b9d47b14"
repo-rev = "npr-pool-size"
repo-url = "https://github.com/JuliaServices/ConcurrentUtilities.jl.git"
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
version = "2.3.0"

[[deps.Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[deps.ExceptionUnwrapping]]
deps = ["Test"]
git-tree-sha1 = "e90caa41f5a86296e014e148ee061bd6c3edec96"
uuid = "460bff9d-24e4-43bc-9d9f-a8973cb893f4"
version = "0.1.9"

[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[deps.JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.4.1"

[[deps.Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[deps.LoggingExtras]]
deps = ["Dates", "Logging"]
git-tree-sha1 = "cedb76b37bc5a6c702ade66be44f831fa23c681e"
uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36"
version = "1.0.0"

[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[deps.MbedTLS]]
deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"]
git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b"
uuid = "739be429-bea8-5141-9913-cc70e7f3736d"
version = "1.1.7"

[[deps.MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
version = "2.28.2+0"

[[deps.MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
version = "2022.10.11"

[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
version = "1.2.0"

[[deps.OpenSSL]]
deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"]
git-tree-sha1 = "51901a49222b09e3743c65b8847687ae5fc78eb2"
uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c"
version = "1.4.1"

[[deps.OpenSSL_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "cae3153c7f6cf3f069a853883fd1919a6e5bab5b"
uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
version = "3.0.9+0"

[[deps.Preferences]]
deps = ["TOML"]
git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.4.0"

[[deps.Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"

[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[deps.SimpleBufferStream]]
git-tree-sha1 = "874e8867b33a00e784c8a7e4b60afe9e037b74e1"
uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7"
version = "1.1.0"

[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
version = "1.0.3"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[deps.TranscodingStreams]]
deps = ["Random", "Test"]
git-tree-sha1 = "9a6ae7ed916312b41236fcef7e0af564ef934769"
uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.9.13"

[[deps.URIs]]
git-tree-sha1 = "074f993b0ca030848b897beff716d93aca60f06a"
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
version = "1.4.2"

[[deps.UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[deps.Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[deps.Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
version = "1.2.13+0"
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
CodecZlib = "0.7"
ConcurrentUtilities = "2.2"
ConcurrentUtilities = "2.3"
ExceptionUnwrapping = "0.1"
LoggingExtras = "0.4.9,1"
MbedTLS = "0.6.8, 0.7, 1"
Expand Down
87 changes: 72 additions & 15 deletions src/Connections.jl
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,64 @@ const MBEDTLS_POOL = Ref{CPool{MbedTLS.SSLContext}}()
const OPENSSL_POOL = Ref{CPool{OpenSSL.SSLStream}}()
const OTHER_POOL = Lockable(IdDict{Type, CPool}())

"""
HTTP.Connections.metrics([nothing]) -> IdDict{Type,Metrics}

Return a dictionary of connection metrics, keyed by the connection type, for the default global pool.
"""
function metrics(pool::Nothing=nothing)
return IdDict{Type,Metrics}(
Sockets.TCPSocket => Metrics(TCP_POOL[]),
MbedTLS.SSLContext => Metrics(MBEDTLS_POOL[]),
OpenSSL.SSLStream => Metrics(OPENSSL_POOL[]),
(Base.@lock OTHER_POOL.lock (k => Metrics(v) for (k, v) in OTHER_POOL[]))...,
)
end

"""
HTTP.Connections.metrics(pool::Pool) -> IdDict{Type,Metrics}

Return a dictionary of connection metrics, keyed by the connection type, for the given `pool`.
"""
function metrics(pool::Pool)
return IdDict{Type,Metrics}(
Sockets.TCPSocket => Metrics(pool.tcp),
MbedTLS.SSLContext => Metrics(pool.mbedtls),
OpenSSL.SSLStream => Metrics(pool.openssl),
(Base.@lock pool.lock (k => Metrics(v) for (k, v) in pool.other))...,
)
end

Base.@kwdef struct Metrics
limit::Int
in_use::Int
in_pool::Int
end

"""
Metrics(cpool::$CPool)

Metrics for the given connection pool:
- `limit`: the maximum number of connections allowed to be in-use at the same time.
- `in_use`: the number of connections currently in use.
- `in_pool`: the number of connections available for re-use.
"""
function Metrics(cpool::CPool)
return Metrics(
limit=ConcurrentUtilities.Pools.max(cpool),
in_use=ConcurrentUtilities.Pools.permits(cpool),
in_pool=ConcurrentUtilities.Pools.depth(cpool),
)
end

function Base.show(io::IO, m::Metrics)
print(io, "Metrics(")
print(io, "limit=", m.limit)
print(io, ", in_use=", m.in_use)
print(io, ", in_pool=", m.in_pool)
print(io, ")")
end

getpool(::Nothing, ::Type{Sockets.TCPSocket}) = TCP_POOL[]
getpool(::Nothing, ::Type{MbedTLS.SSLContext}) = MBEDTLS_POOL[]
getpool(::Nothing, ::Type{OpenSSL.SSLStream}) = OPENSSL_POOL[]
Expand Down Expand Up @@ -451,22 +509,21 @@ function newconnection(::Type{T},
keepalive::Bool=true,
kw...) where {T <: IO}
connection_limit_warning(connection_limit)
return acquire(
getpool(pool, T),
(host, port, require_ssl_verification, keepalive, true);
forcenew=forcenew,
isvalid=c->connection_isvalid(c, Int(idle_timeout))) do
Connection(host, port,
idle_timeout, require_ssl_verification, keepalive,
connect_timeout > 0 ?
try_with_timeout(_ ->
getconnection(T, host, port;
require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...),
connect_timeout) :
getconnection(T, host, port;
require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...)
)
function connect(timeout)
if timeout > 0
try_with_timeout(timeout) do _
getconnection(T, host, port; require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...)
end
else
getconnection(T, host, port; require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...)
end
end
newconn() = Connection(host, port, idle_timeout, require_ssl_verification, keepalive, connect(connect_timeout))
key = (host, port, require_ssl_verification, keepalive, true)
return acquire(
newconn, getpool(pool, T), key;
forcenew=forcenew, isvalid=c->connection_isvalid(c, Int(idle_timeout)),
)
end

function releaseconnection(c::Connection{T}, reuse; pool::Union{Nothing, Pool}=nothing, kw...) where {T}
Expand Down
59 changes: 59 additions & 0 deletions test/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,65 @@ end
end
end

@testset "Connections.metrics" begin
# Test that `metrics` function returns the expected values as we use / release connections.
# Initialise a pool, and check that it is empty and has the expected limit
pool = HTTP.Pool(3)
for (T, v) in HTTP.Connections.metrics(pool)
@test v.limit == 3
@test v.in_pool == 0
@test v.in_use == 0
end

TCP = Sockets.TCPSocket
# After a request, check the connection is put in the pool for reuse
HTTP.get("https://$httpbin/get"; pool=pool, socket_type_tls=TCP)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 1
@test metrics[TCP].in_use == 0

# A second request should use this same connection and put it back again
HTTP.get("https://$httpbin/get"; pool=pool, socket_type_tls=TCP)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 1
@test metrics[TCP].in_use == 0

# Force a new connection -- the one in the pool should remain there.
c1 = HTTP.Connections.newconnection(TCP, httpbin, ""; forcenew=true, connect_timeout=3, pool=pool)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 1
@test metrics[TCP].in_use == 1

# Get another "new connection", since we didn't force new, this should use the one from the pool.
c2 = HTTP.Connections.newconnection(TCP, httpbin, ""; forcenew=false, connect_timeout=3, pool=pool)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 0
@test metrics[TCP].in_use == 2

# Release the first connection back to the pool
HTTP.Connections.releaseconnection(c1, true; pool=pool)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 1
@test metrics[TCP].in_use == 1

# Release the second connection but do not put back in the pool
HTTP.Connections.releaseconnection(c1, false; pool=pool)
metrics = HTTP.Connections.metrics(pool)
@test metrics[TCP].in_pool == 1
@test metrics[TCP].in_use == 0

# Test another connection type
SSL = MbedTLS.SSLContext
@test metrics[SSL].limit == 3
@test metrics[SSL].in_pool == 0
@test metrics[SSL].in_use == 0
HTTP.get("https://$httpbin/get"; pool=pool, socket_type_tls=SSL)
metrics = HTTP.Connections.metrics(pool)
@test metrics[SSL].limit == 3
@test metrics[SSL].in_pool == 1
@test metrics[SSL].in_use == 0
end

@testset "Retry all resolved IP addresses" begin
# See issue https://github.com/JuliaWeb/HTTP.jl/issues/672
# Bit tricky to test, but can at least be tested if localhost
Expand Down
Loading