pleroma/test/pool/connections_test.exs

832 lines
20 KiB
Elixir
Raw Normal View History

2020-02-11 02:12:57 -05:00
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
2020-02-11 02:12:57 -05:00
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Pool.ConnectionsTest do
2020-04-23 08:52:13 -04:00
use ExUnit.Case
2020-02-11 02:12:57 -05:00
use Pleroma.Tests.Helpers
2020-03-06 12:23:58 -05:00
2020-02-11 02:12:57 -05:00
import ExUnit.CaptureLog
2020-03-06 12:23:58 -05:00
import Mox
2020-02-11 02:12:57 -05:00
alias Pleroma.Gun.Conn
2020-03-06 12:23:58 -05:00
alias Pleroma.GunMock
2020-02-11 02:12:57 -05:00
alias Pleroma.Pool.Connections
2020-03-06 12:23:58 -05:00
setup :verify_on_exit!
2020-04-23 08:52:13 -04:00
setup :set_mox_global
2020-03-06 12:23:58 -05:00
2020-02-11 02:12:57 -05:00
setup_all do
2020-04-23 08:52:13 -04:00
{:ok, pid} = Agent.start_link(fn -> %{} end, name: :gun_state)
on_exit(fn ->
if Process.alive?(pid), do: Agent.stop(pid)
end)
end
setup do
2020-03-06 12:23:58 -05:00
name = :test_connections
2020-04-23 08:52:13 -04:00
{:ok, pid} = Connections.start_link({name, []})
2020-03-06 12:23:58 -05:00
on_exit(fn ->
if Process.alive?(pid), do: GenServer.stop(name)
end)
{:ok, name: name}
2020-02-11 02:12:57 -05:00
end
2020-03-06 12:23:58 -05:00
defp open_mock(num \\ 1) do
GunMock
|> expect(:open, num, &start_and_register(&1, &2, &3))
|> expect(:await_up, num, fn _, _ -> {:ok, :http} end)
|> expect(:set_owner, num, fn _, _ -> :ok end)
end
2020-03-06 12:23:58 -05:00
defp connect_mock(mock) do
mock
|> expect(:connect, &connect(&1, &2))
|> expect(:await, &await(&1, &2))
end
2020-02-11 02:12:57 -05:00
2020-03-06 12:23:58 -05:00
defp info_mock(mock), do: expect(mock, :info, &info(&1))
2020-02-11 02:12:57 -05:00
2020-03-06 12:23:58 -05:00
defp start_and_register('gun-not-up.com', _, _), do: {:error, :timeout}
2020-03-02 02:01:07 -05:00
2020-03-06 12:23:58 -05:00
defp start_and_register(host, port, _) do
{:ok, pid} = Task.start_link(fn -> Process.sleep(1000) end)
scheme =
case port do
443 -> "https"
_ -> "http"
2020-03-02 02:01:07 -05:00
end
2020-04-23 08:52:13 -04:00
info = %{
2020-03-06 12:23:58 -05:00
origin_scheme: scheme,
origin_host: host,
origin_port: port
2020-04-23 08:52:13 -04:00
}
Agent.update(:gun_state, &Map.put(&1, pid, %{info: info, ref: nil}))
2020-03-06 12:23:58 -05:00
{:ok, pid}
end
2020-04-23 08:52:13 -04:00
defp info(pid), do: Agent.get(:gun_state, & &1[pid][:info])
2020-02-11 02:12:57 -05:00
2020-03-06 12:23:58 -05:00
defp connect(pid, _) do
ref = make_ref()
2020-04-23 08:52:13 -04:00
Agent.update(:gun_state, &put_in(&1[pid][:ref], ref))
2020-03-06 12:23:58 -05:00
ref
end
2020-04-23 08:52:13 -04:00
defp await(pid, _ref) do
Agent.get(:gun_state, & &1[pid][:ref])
2020-03-06 12:23:58 -05:00
{:response, :fin, 200, []}
end
defp now, do: :os.system_time(:second)
2020-02-11 02:12:57 -05:00
describe "alive?/2" do
test "is alive", %{name: name} do
assert Connections.alive?(name)
end
test "returns false if not started" do
refute Connections.alive?(:some_random_name)
end
end
test "opens connection and reuse it on next request", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://some-domain.com"
key = "http:some-domain.com:80"
2020-04-21 07:28:37 -04:00
2020-02-11 02:12:57 -05:00
conn = Connections.checkin(url, name)
assert is_pid(conn)
assert Process.alive?(conn)
self = self()
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert conn == reused_conn
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}, {^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
:ok = Connections.checkout(conn, self, name)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
:ok = Connections.checkout(conn, self, name)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [],
conn_state: :idle
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "reuse connection for idna domains", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://ですsome-domain.com"
conn = Connections.checkin(url, name)
assert is_pid(conn)
assert Process.alive?(conn)
self = self()
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:ですsome-domain.com:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert conn == reused_conn
end
test "reuse for ipv4", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://127.0.0.1"
conn = Connections.checkin(url, name)
assert is_pid(conn)
assert Process.alive?(conn)
self = self()
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:127.0.0.1:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert conn == reused_conn
:ok = Connections.checkout(conn, self, name)
:ok = Connections.checkout(reused_conn, self, name)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:127.0.0.1:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [],
conn_state: :idle
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "reuse for ipv6", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
conn = Connections.checkin(url, name)
assert is_pid(conn)
assert Process.alive?(conn)
self = self()
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert conn == reused_conn
end
test "up and down ipv4", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
|> info_mock()
|> allow(self(), name)
2020-02-11 02:12:57 -05:00
self = self()
url = "http://127.0.0.1"
conn = Connections.checkin(url, name)
send(name, {:gun_down, conn, nil, nil, nil})
send(name, {:gun_up, conn, nil})
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:127.0.0.1:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "up and down ipv6", %{name: name} do
self = self()
2020-03-06 12:23:58 -05:00
open_mock()
|> info_mock()
|> allow(self, name)
2020-02-11 02:12:57 -05:00
url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
conn = Connections.checkin(url, name)
send(name, {:gun_down, conn, nil, nil, nil})
send(name, {:gun_up, conn, nil})
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}],
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "reuses connection based on protocol", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock(2)
2020-02-11 02:12:57 -05:00
http_url = "http://some-domain.com"
http_key = "http:some-domain.com:80"
https_url = "https://some-domain.com"
https_key = "https:some-domain.com:443"
conn = Connections.checkin(http_url, name)
assert is_pid(conn)
assert Process.alive?(conn)
https_conn = Connections.checkin(https_url, name)
refute conn == https_conn
reused_https = Connections.checkin(https_url, name)
refute conn == reused_https
assert reused_https == https_conn
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^http_key => %Conn{
conn: ^conn,
gun_state: :up
},
^https_key => %Conn{
conn: ^https_conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "connection can't get up", %{name: name} do
2020-03-06 12:23:58 -05:00
expect(GunMock, :open, &start_and_register(&1, &2, &3))
2020-02-11 02:12:57 -05:00
url = "http://gun-not-up.com"
assert capture_log(fn ->
refute Connections.checkin(url, name)
end) =~
2020-03-12 11:28:54 -04:00
"Opening connection to http://gun-not-up.com failed with error {:error, :timeout}"
2020-02-11 02:12:57 -05:00
end
test "process gun_down message and then gun_up", %{name: name} do
self = self()
2020-03-06 12:23:58 -05:00
open_mock()
|> info_mock()
|> allow(self, name)
2020-02-11 02:12:57 -05:00
url = "http://gun-down-and-up.com"
key = "http:gun-down-and-up.com:80"
conn = Connections.checkin(url, name)
assert is_pid(conn)
assert Process.alive?(conn)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up,
used_by: [{^self, _}]
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
send(name, {:gun_down, conn, :http, nil, nil})
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :down,
used_by: [{^self, _}]
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
send(name, {:gun_up, conn, :http})
conn2 = Connections.checkin(url, name)
assert conn == conn2
assert is_pid(conn2)
assert Process.alive?(conn2)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: _,
gun_state: :up,
used_by: [{^self, _}, {^self, _}]
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
test "async processes get same conn for same domain", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://some-domain.com"
tasks =
for _ <- 1..5 do
Task.async(fn ->
Connections.checkin(url, name)
end)
end
tasks_with_results = Task.yield_many(tasks)
results =
Enum.map(tasks_with_results, fn {task, res} ->
res || Task.shutdown(task, :brutal_kill)
end)
conns = for {:ok, value} <- results, do: value
2020-04-23 08:52:13 -04:00
state = Connections.get_state(name)
%{conn: conn} = Map.get(state.conns, "http:some-domain.com:80")
2020-02-11 02:12:57 -05:00
assert Enum.all?(conns, fn res -> res == conn end)
end
test "remove frequently used and idle", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock(3)
2020-02-11 02:12:57 -05:00
self = self()
http_url = "http://some-domain.com"
https_url = "https://some-domain.com"
conn1 = Connections.checkin(https_url, name)
[conn2 | _conns] =
for _ <- 1..4 do
Connections.checkin(http_url, name)
end
http_key = "http:some-domain.com:80"
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
^http_key => %Conn{
conn: ^conn2,
gun_state: :up,
conn_state: :active
},
"https:some-domain.com:443" => %Conn{
conn: ^conn1,
gun_state: :up,
conn_state: :active
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
:ok = Connections.checkout(conn1, self, name)
another_url = "http://another-domain.com"
conn = Connections.checkin(another_url, name)
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:another-domain.com:80" => %Conn{
conn: ^conn,
gun_state: :up
},
^http_key => %Conn{
conn: _,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
end
describe "with proxy" do
test "as ip", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
|> connect_mock()
2020-02-11 02:12:57 -05:00
url = "http://proxy-string.com"
key = "http:proxy-string.com:80"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {{127, 0, 0, 1}, 8123})
assert match?(
%Connections{
conns: %{
^key => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
test "as host", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
|> connect_mock()
2020-02-11 02:12:57 -05:00
url = "http://proxy-tuple-atom.com"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {'localhost', 9050})
assert match?(
%Connections{
conns: %{
"http:proxy-tuple-atom.com:80" => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
test "as ip and ssl", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
|> connect_mock()
2020-02-11 02:12:57 -05:00
url = "https://proxy-string.com"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {{127, 0, 0, 1}, 8123})
2020-02-11 02:12:57 -05:00
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"https:proxy-string.com:443" => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
test "as host and ssl", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
|> connect_mock()
2020-02-11 02:12:57 -05:00
url = "https://proxy-tuple-atom.com"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {'localhost', 9050})
assert match?(
%Connections{
conns: %{
"https:proxy-tuple-atom.com:443" => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
test "with socks type", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "http://proxy-socks.com"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {:socks5, 'localhost', 1234})
2020-02-11 02:12:57 -05:00
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"http:proxy-socks.com:80" => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
test "with socks4 type and ssl", %{name: name} do
2020-03-06 12:23:58 -05:00
open_mock()
2020-02-11 02:12:57 -05:00
url = "https://proxy-socks.com"
2020-04-23 08:52:13 -04:00
conn = Connections.checkin(url, name, proxy: {:socks4, 'localhost', 1234})
2020-02-11 02:12:57 -05:00
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"https:proxy-socks.com:443" => %Conn{
conn: ^conn,
gun_state: :up
}
}
},
Connections.get_state(name)
)
2020-02-11 02:12:57 -05:00
reused_conn = Connections.checkin(url, name)
assert reused_conn == conn
end
end
describe "crf/3" do
setup do
crf = Connections.crf(1, 10, 1)
{:ok, crf: crf}
end
test "more used will have crf higher", %{crf: crf} do
# used 3 times
crf1 = Connections.crf(1, 10, crf)
crf1 = Connections.crf(1, 10, crf1)
# used 2 times
crf2 = Connections.crf(1, 10, crf)
assert crf1 > crf2
end
test "recently used will have crf higher on equal references", %{crf: crf} do
# used 3 sec ago
crf1 = Connections.crf(3, 10, crf)
# used 4 sec ago
crf2 = Connections.crf(4, 10, crf)
assert crf1 > crf2
end
test "equal crf on equal reference and time", %{crf: crf} do
# used 2 times
crf1 = Connections.crf(1, 10, crf)
# used 2 times
crf2 = Connections.crf(1, 10, crf)
assert crf1 == crf2
end
test "recently used will have higher crf", %{crf: crf} do
crf1 = Connections.crf(2, 10, crf)
crf1 = Connections.crf(1, 10, crf1)
crf2 = Connections.crf(3, 10, crf)
crf2 = Connections.crf(4, 10, crf2)
assert crf1 > crf2
end
end
describe "get_unused_conns/1" do
2020-04-23 08:52:13 -04:00
setup %{name: name} do
Connections.refresh(name)
end
2020-02-24 11:56:27 -05:00
test "crf is equalent, sorting by reference", %{name: name} do
Connections.add_conn(name, "1", %Conn{
conn_state: :idle,
last_reference: now() - 1
})
Connections.add_conn(name, "2", %Conn{
conn_state: :idle,
last_reference: now()
})
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
2020-02-11 02:12:57 -05:00
end
2020-02-24 11:56:27 -05:00
test "reference is equalent, sorting by crf", %{name: name} do
Connections.add_conn(name, "1", %Conn{
conn_state: :idle,
crf: 1.999
})
Connections.add_conn(name, "2", %Conn{
conn_state: :idle,
crf: 2
})
2020-02-11 02:12:57 -05:00
2020-02-24 11:56:27 -05:00
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
2020-02-11 02:12:57 -05:00
end
2020-02-24 11:56:27 -05:00
test "higher crf and lower reference", %{name: name} do
Connections.add_conn(name, "1", %Conn{
conn_state: :idle,
crf: 3,
last_reference: now() - 1
})
Connections.add_conn(name, "2", %Conn{
conn_state: :idle,
crf: 2,
last_reference: now()
})
2020-02-11 02:12:57 -05:00
2020-02-24 11:56:27 -05:00
assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
2020-02-11 02:12:57 -05:00
end
2020-02-24 11:56:27 -05:00
test "lower crf and lower reference", %{name: name} do
Connections.add_conn(name, "1", %Conn{
conn_state: :idle,
crf: 1.99,
last_reference: now() - 1
})
2020-02-11 02:12:57 -05:00
2020-02-24 11:56:27 -05:00
Connections.add_conn(name, "2", %Conn{
conn_state: :idle,
crf: 2,
last_reference: now()
})
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
2020-02-11 02:12:57 -05:00
end
end
2020-03-06 12:23:58 -05:00
test "count/1" do
name = :test_count
2020-04-29 09:31:37 -04:00
{:ok, _} = Connections.start_link({name, []})
2020-02-24 11:56:27 -05:00
assert Connections.count(name) == 0
Connections.add_conn(name, "1", %Conn{conn: self()})
assert Connections.count(name) == 1
Connections.remove_conn(name, "1")
assert Connections.count(name) == 0
end
2020-04-08 09:01:03 -04:00
test "close_idle_conns/2", %{name: name} do
GunMock
|> expect(:close, fn _ -> :ok end)
|> allow(self(), name)
Connections.add_conn(name, "1", %Conn{
conn_state: :idle,
last_reference: now() - 30,
conn: self()
})
Connections.add_conn(name, "2", %Conn{
conn_state: :idle,
last_reference: now() - 10,
conn: self()
})
Connections.add_conn(name, "3", %Conn{
conn_state: :active,
conn: self()
})
name
|> Process.whereis()
|> send({:close_idle_conns, 15})
2020-04-23 08:52:13 -04:00
assert match?(
%Connections{
conns: %{
"3" => %Conn{},
"2" => %Conn{}
}
},
Connections.get_state(name)
)
2020-04-08 09:01:03 -04:00
end
2020-02-11 02:12:57 -05:00
end