1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Pool.ConnectionsTest do
6 use ExUnit.Case, async: true
7 use Pleroma.Tests.Helpers
9 import ExUnit.CaptureLog
12 alias Pleroma.Gun.Conn
14 alias Pleroma.Pool.Connections
16 setup :verify_on_exit!
19 name = :test_connections
20 {:ok, pid} = Connections.start_link({name, [checkin_timeout: 150]})
21 {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock)
24 if Process.alive?(pid), do: GenServer.stop(name)
30 defp open_mock(num \\ 1) do
32 |> expect(:open, num, &start_and_register(&1, &2, &3))
33 |> expect(:await_up, num, fn _, _ -> {:ok, :http} end)
34 |> expect(:set_owner, num, fn _, _ -> :ok end)
37 defp connect_mock(mock) do
39 |> expect(:connect, &connect(&1, &2))
40 |> expect(:await, &await(&1, &2))
43 defp info_mock(mock), do: expect(mock, :info, &info(&1))
45 defp start_and_register('gun-not-up.com', _, _), do: {:error, :timeout}
47 defp start_and_register(host, port, _) do
48 {:ok, pid} = Task.start_link(fn -> Process.sleep(1000) end)
56 Registry.register(GunMock, pid, %{
57 origin_scheme: scheme,
66 [{_, info}] = Registry.lookup(GunMock, pid)
70 defp connect(pid, _) do
72 Registry.register(GunMock, ref, pid)
76 defp await(pid, ref) do
77 [{_, ^pid}] = Registry.lookup(GunMock, ref)
78 {:response, :fin, 200, []}
81 defp now, do: :os.system_time(:second)
83 describe "alive?/2" do
84 test "is alive", %{name: name} do
85 assert Connections.alive?(name)
88 test "returns false if not started" do
89 refute Connections.alive?(:some_random_name)
93 test "opens connection and reuse it on next request", %{name: name} do
95 url = "http://some-domain.com"
96 key = "http:some-domain.com:80"
97 refute Connections.checkin(url, name)
98 :ok = Conn.open(url, name)
100 conn = Connections.checkin(url, name)
102 assert Process.alive?(conn)
111 used_by: [{^self, _}],
115 } = Connections.get_state(name)
117 reused_conn = Connections.checkin(url, name)
119 assert conn == reused_conn
126 used_by: [{^self, _}, {^self, _}],
130 } = Connections.get_state(name)
132 :ok = Connections.checkout(conn, self, name)
139 used_by: [{^self, _}],
143 } = Connections.get_state(name)
145 :ok = Connections.checkout(conn, self, name)
156 } = Connections.get_state(name)
159 test "reuse connection for idna domains", %{name: name} do
161 url = "http://ですsome-domain.com"
162 refute Connections.checkin(url, name)
164 :ok = Conn.open(url, name)
166 conn = Connections.checkin(url, name)
168 assert Process.alive?(conn)
174 "http:ですsome-domain.com:80" => %Conn{
177 used_by: [{^self, _}],
181 } = Connections.get_state(name)
183 reused_conn = Connections.checkin(url, name)
185 assert conn == reused_conn
188 test "reuse for ipv4", %{name: name} do
190 url = "http://127.0.0.1"
192 refute Connections.checkin(url, name)
194 :ok = Conn.open(url, name)
196 conn = Connections.checkin(url, name)
198 assert Process.alive?(conn)
204 "http:127.0.0.1:80" => %Conn{
207 used_by: [{^self, _}],
211 } = Connections.get_state(name)
213 reused_conn = Connections.checkin(url, name)
215 assert conn == reused_conn
217 :ok = Connections.checkout(conn, self, name)
218 :ok = Connections.checkout(reused_conn, self, name)
222 "http:127.0.0.1:80" => %Conn{
229 } = Connections.get_state(name)
232 test "reuse for ipv6", %{name: name} do
234 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
236 refute Connections.checkin(url, name)
238 :ok = Conn.open(url, name)
240 conn = Connections.checkin(url, name)
242 assert Process.alive?(conn)
248 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
251 used_by: [{^self, _}],
255 } = Connections.get_state(name)
257 reused_conn = Connections.checkin(url, name)
259 assert conn == reused_conn
262 test "up and down ipv4", %{name: name} do
265 |> allow(self(), name)
268 url = "http://127.0.0.1"
269 :ok = Conn.open(url, name)
270 conn = Connections.checkin(url, name)
271 send(name, {:gun_down, conn, nil, nil, nil})
272 send(name, {:gun_up, conn, nil})
276 "http:127.0.0.1:80" => %Conn{
279 used_by: [{^self, _}],
283 } = Connections.get_state(name)
286 test "up and down ipv6", %{name: name} do
293 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
294 :ok = Conn.open(url, name)
295 conn = Connections.checkin(url, name)
296 send(name, {:gun_down, conn, nil, nil, nil})
297 send(name, {:gun_up, conn, nil})
301 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
304 used_by: [{^self, _}],
308 } = Connections.get_state(name)
311 test "reuses connection based on protocol", %{name: name} do
313 http_url = "http://some-domain.com"
314 http_key = "http:some-domain.com:80"
315 https_url = "https://some-domain.com"
316 https_key = "https:some-domain.com:443"
318 refute Connections.checkin(http_url, name)
319 :ok = Conn.open(http_url, name)
320 conn = Connections.checkin(http_url, name)
322 assert Process.alive?(conn)
324 refute Connections.checkin(https_url, name)
325 :ok = Conn.open(https_url, name)
326 https_conn = Connections.checkin(https_url, name)
328 refute conn == https_conn
330 reused_https = Connections.checkin(https_url, name)
332 refute conn == reused_https
334 assert reused_https == https_conn
347 } = Connections.get_state(name)
350 test "connection can't get up", %{name: name} do
351 expect(GunMock, :open, &start_and_register(&1, &2, &3))
352 url = "http://gun-not-up.com"
354 assert capture_log(fn ->
355 refute Conn.open(url, name)
356 refute Connections.checkin(url, name)
358 "Opening connection to http://gun-not-up.com failed with error {:error, :timeout}"
361 test "process gun_down message and then gun_up", %{name: name} do
368 url = "http://gun-down-and-up.com"
369 key = "http:gun-down-and-up.com:80"
370 :ok = Conn.open(url, name)
371 conn = Connections.checkin(url, name)
374 assert Process.alive?(conn)
381 used_by: [{^self, _}]
384 } = Connections.get_state(name)
386 send(name, {:gun_down, conn, :http, nil, nil})
393 used_by: [{^self, _}]
396 } = Connections.get_state(name)
398 send(name, {:gun_up, conn, :http})
400 conn2 = Connections.checkin(url, name)
404 assert Process.alive?(conn2)
411 used_by: [{^self, _}, {^self, _}]
414 } = Connections.get_state(name)
417 test "async processes get same conn for same domain", %{name: name} do
419 url = "http://some-domain.com"
420 :ok = Conn.open(url, name)
425 Connections.checkin(url, name)
429 tasks_with_results = Task.yield_many(tasks)
432 Enum.map(tasks_with_results, fn {task, res} ->
433 res || Task.shutdown(task, :brutal_kill)
436 conns = for {:ok, value} <- results, do: value
440 "http:some-domain.com:80" => %Conn{
445 } = Connections.get_state(name)
447 assert Enum.all?(conns, fn res -> res == conn end)
450 test "remove frequently used and idle", %{name: name} do
453 http_url = "http://some-domain.com"
454 https_url = "https://some-domain.com"
455 :ok = Conn.open(https_url, name)
456 :ok = Conn.open(http_url, name)
458 conn1 = Connections.checkin(https_url, name)
462 Connections.checkin(http_url, name)
465 http_key = "http:some-domain.com:80"
473 used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
475 "https:some-domain.com:443" => %Conn{
479 used_by: [{^self, _}]
482 } = Connections.get_state(name)
484 :ok = Connections.checkout(conn1, self, name)
486 another_url = "http://another-domain.com"
487 :ok = Conn.open(another_url, name)
488 conn = Connections.checkin(another_url, name)
492 "http:another-domain.com:80" => %Conn{
501 } = Connections.get_state(name)
504 describe "with proxy" do
505 test "as ip", %{name: name} do
509 url = "http://proxy-string.com"
510 key = "http:proxy-string.com:80"
511 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
513 conn = Connections.checkin(url, name)
522 } = Connections.get_state(name)
524 reused_conn = Connections.checkin(url, name)
526 assert reused_conn == conn
529 test "as host", %{name: name} do
533 url = "http://proxy-tuple-atom.com"
534 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
535 conn = Connections.checkin(url, name)
539 "http:proxy-tuple-atom.com:80" => %Conn{
544 } = Connections.get_state(name)
546 reused_conn = Connections.checkin(url, name)
548 assert reused_conn == conn
551 test "as ip and ssl", %{name: name} do
555 url = "https://proxy-string.com"
557 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
558 conn = Connections.checkin(url, name)
562 "https:proxy-string.com:443" => %Conn{
567 } = Connections.get_state(name)
569 reused_conn = Connections.checkin(url, name)
571 assert reused_conn == conn
574 test "as host and ssl", %{name: name} do
578 url = "https://proxy-tuple-atom.com"
579 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
580 conn = Connections.checkin(url, name)
584 "https:proxy-tuple-atom.com:443" => %Conn{
589 } = Connections.get_state(name)
591 reused_conn = Connections.checkin(url, name)
593 assert reused_conn == conn
596 test "with socks type", %{name: name} do
599 url = "http://proxy-socks.com"
601 :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
603 conn = Connections.checkin(url, name)
607 "http:proxy-socks.com:80" => %Conn{
612 } = Connections.get_state(name)
614 reused_conn = Connections.checkin(url, name)
616 assert reused_conn == conn
619 test "with socks4 type and ssl", %{name: name} do
621 url = "https://proxy-socks.com"
623 :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
625 conn = Connections.checkin(url, name)
629 "https:proxy-socks.com:443" => %Conn{
634 } = Connections.get_state(name)
636 reused_conn = Connections.checkin(url, name)
638 assert reused_conn == conn
644 crf = Connections.crf(1, 10, 1)
648 test "more used will have crf higher", %{crf: crf} do
650 crf1 = Connections.crf(1, 10, crf)
651 crf1 = Connections.crf(1, 10, crf1)
654 crf2 = Connections.crf(1, 10, crf)
659 test "recently used will have crf higher on equal references", %{crf: crf} do
661 crf1 = Connections.crf(3, 10, crf)
664 crf2 = Connections.crf(4, 10, crf)
669 test "equal crf on equal reference and time", %{crf: crf} do
671 crf1 = Connections.crf(1, 10, crf)
674 crf2 = Connections.crf(1, 10, crf)
679 test "recently used will have higher crf", %{crf: crf} do
680 crf1 = Connections.crf(2, 10, crf)
681 crf1 = Connections.crf(1, 10, crf1)
683 crf2 = Connections.crf(3, 10, crf)
684 crf2 = Connections.crf(4, 10, crf2)
689 describe "get_unused_conns/1" do
690 test "crf is equalent, sorting by reference", %{name: name} do
691 Connections.add_conn(name, "1", %Conn{
693 last_reference: now() - 1
696 Connections.add_conn(name, "2", %Conn{
698 last_reference: now()
701 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
704 test "reference is equalent, sorting by crf", %{name: name} do
705 Connections.add_conn(name, "1", %Conn{
710 Connections.add_conn(name, "2", %Conn{
715 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
718 test "higher crf and lower reference", %{name: name} do
719 Connections.add_conn(name, "1", %Conn{
722 last_reference: now() - 1
725 Connections.add_conn(name, "2", %Conn{
728 last_reference: now()
731 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
734 test "lower crf and lower reference", %{name: name} do
735 Connections.add_conn(name, "1", %Conn{
738 last_reference: now() - 1
741 Connections.add_conn(name, "2", %Conn{
744 last_reference: now()
747 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
753 {:ok, _} = Connections.start_link({name, [checkin_timeout: 150]})
754 assert Connections.count(name) == 0
755 Connections.add_conn(name, "1", %Conn{conn: self()})
756 assert Connections.count(name) == 1
757 Connections.remove_conn(name, "1")
758 assert Connections.count(name) == 0