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
7 use Pleroma.Tests.Helpers
8 import ExUnit.CaptureLog
10 alias Pleroma.Pool.Connections
13 {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock)
17 clear_config([:connections_pool, :retry]) do
18 Pleroma.Config.put([:connections_pool, :retry], 5)
22 name = :test_connections
23 adapter = Application.get_env(:tesla, :adapter)
24 Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
26 {:ok, pid} = Connections.start_link({name, [max_connections: 2, checkin_timeout: 1_500]})
29 Application.put_env(:tesla, :adapter, adapter)
31 if Process.alive?(pid) do
39 describe "alive?/2" do
40 test "is alive", %{name: name} do
41 assert Connections.alive?(name)
44 test "returns false if not started" do
45 refute Connections.alive?(:some_random_name)
49 test "opens connection and reuse it on next request", %{name: name} do
50 url = "http://some-domain.com"
51 key = "http:some-domain.com:80"
52 refute Connections.checkin(url, name)
53 :ok = Conn.open(url, name)
55 conn = Connections.checkin(url, name)
57 assert Process.alive?(conn)
66 used_by: [{^self, _}],
70 } = Connections.get_state(name)
72 reused_conn = Connections.checkin(url, name)
74 assert conn == reused_conn
81 used_by: [{^self, _}, {^self, _}],
85 } = Connections.get_state(name)
87 :ok = Connections.checkout(conn, self, name)
94 used_by: [{^self, _}],
98 } = Connections.get_state(name)
100 :ok = Connections.checkout(conn, self, name)
111 } = Connections.get_state(name)
114 test "reuse connection for idna domains", %{name: name} do
115 url = "http://ですsome-domain.com"
116 refute Connections.checkin(url, name)
118 :ok = Conn.open(url, name)
120 conn = Connections.checkin(url, name)
122 assert Process.alive?(conn)
128 "http:ですsome-domain.com:80" => %Conn{
131 used_by: [{^self, _}],
135 } = Connections.get_state(name)
137 reused_conn = Connections.checkin(url, name)
139 assert conn == reused_conn
142 test "reuse for ipv4", %{name: name} do
143 url = "http://127.0.0.1"
145 refute Connections.checkin(url, name)
147 :ok = Conn.open(url, name)
149 conn = Connections.checkin(url, name)
151 assert Process.alive?(conn)
157 "http:127.0.0.1:80" => %Conn{
160 used_by: [{^self, _}],
164 } = Connections.get_state(name)
166 reused_conn = Connections.checkin(url, name)
168 assert conn == reused_conn
170 :ok = Connections.checkout(conn, self, name)
171 :ok = Connections.checkout(reused_conn, self, name)
175 "http:127.0.0.1:80" => %Conn{
182 } = Connections.get_state(name)
185 test "reuse for ipv6", %{name: name} do
186 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
188 refute Connections.checkin(url, name)
190 :ok = Conn.open(url, name)
192 conn = Connections.checkin(url, name)
194 assert Process.alive?(conn)
200 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
203 used_by: [{^self, _}],
207 } = Connections.get_state(name)
209 reused_conn = Connections.checkin(url, name)
211 assert conn == reused_conn
214 test "up and down ipv4", %{name: name} do
216 url = "http://127.0.0.1"
217 :ok = Conn.open(url, name)
218 conn = Connections.checkin(url, name)
219 send(name, {:gun_down, conn, nil, nil, nil})
220 send(name, {:gun_up, conn, nil})
224 "http:127.0.0.1:80" => %Conn{
227 used_by: [{^self, _}],
231 } = Connections.get_state(name)
234 test "up and down ipv6", %{name: name} do
236 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
237 :ok = Conn.open(url, name)
238 conn = Connections.checkin(url, name)
239 send(name, {:gun_down, conn, nil, nil, nil})
240 send(name, {:gun_up, conn, nil})
244 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
247 used_by: [{^self, _}],
251 } = Connections.get_state(name)
254 test "reuses connection based on protocol", %{name: name} do
255 http_url = "http://some-domain.com"
256 http_key = "http:some-domain.com:80"
257 https_url = "https://some-domain.com"
258 https_key = "https:some-domain.com:443"
260 refute Connections.checkin(http_url, name)
261 :ok = Conn.open(http_url, name)
262 conn = Connections.checkin(http_url, name)
264 assert Process.alive?(conn)
266 refute Connections.checkin(https_url, name)
267 :ok = Conn.open(https_url, name)
268 https_conn = Connections.checkin(https_url, name)
270 refute conn == https_conn
272 reused_https = Connections.checkin(https_url, name)
274 refute conn == reused_https
276 assert reused_https == https_conn
289 } = Connections.get_state(name)
292 test "connection can't get up", %{name: name} do
293 url = "http://gun-not-up.com"
295 assert capture_log(fn ->
296 refute Conn.open(url, name)
297 refute Connections.checkin(url, name)
299 "Received error on opening connection http://gun-not-up.com {:error, :timeout}"
302 test "process gun_down message and then gun_up", %{name: name} do
304 url = "http://gun-down-and-up.com"
305 key = "http:gun-down-and-up.com:80"
306 :ok = Conn.open(url, name)
307 conn = Connections.checkin(url, name)
310 assert Process.alive?(conn)
317 used_by: [{^self, _}]
320 } = Connections.get_state(name)
322 send(name, {:gun_down, conn, :http, nil, nil})
329 used_by: [{^self, _}]
332 } = Connections.get_state(name)
334 send(name, {:gun_up, conn, :http})
336 conn2 = Connections.checkin(url, name)
340 assert Process.alive?(conn2)
347 used_by: [{^self, _}, {^self, _}]
350 } = Connections.get_state(name)
353 test "async processes get same conn for same domain", %{name: name} do
354 url = "http://some-domain.com"
355 :ok = Conn.open(url, name)
360 Connections.checkin(url, name)
364 tasks_with_results = Task.yield_many(tasks)
367 Enum.map(tasks_with_results, fn {task, res} ->
368 res || Task.shutdown(task, :brutal_kill)
371 conns = for {:ok, value} <- results, do: value
375 "http:some-domain.com:80" => %Conn{
380 } = Connections.get_state(name)
382 assert Enum.all?(conns, fn res -> res == conn end)
385 test "remove frequently used and idle", %{name: name} do
387 http_url = "http://some-domain.com"
388 https_url = "https://some-domain.com"
389 :ok = Conn.open(https_url, name)
390 :ok = Conn.open(http_url, name)
392 conn1 = Connections.checkin(https_url, name)
396 Connections.checkin(http_url, name)
399 http_key = "http:some-domain.com:80"
407 used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
409 "https:some-domain.com:443" => %Conn{
413 used_by: [{^self, _}]
416 } = Connections.get_state(name)
418 :ok = Connections.checkout(conn1, self, name)
420 another_url = "http://another-domain.com"
421 :ok = Conn.open(another_url, name)
422 conn = Connections.checkin(another_url, name)
426 "http:another-domain.com:80" => %Conn{
435 } = Connections.get_state(name)
438 describe "integration test" do
439 @describetag :integration
441 clear_config(Pleroma.Gun) do
442 Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API)
445 test "opens connection and change owner", %{name: name} do
446 url = "https://httpbin.org"
447 :ok = Conn.open(url, name)
448 conn = Connections.checkin(url, name)
450 pid = Process.whereis(name)
452 assert :gun.info(conn).owner == pid
455 test "opens connection and reuse it on next request", %{name: name} do
456 url = "http://httpbin.org"
457 :ok = Conn.open(url, name)
459 conn = Connections.checkin(url, name)
462 assert Process.alive?(conn)
464 reused_conn = Connections.checkin(url, name)
466 assert conn == reused_conn
470 "http:httpbin.org:80" => %Conn{
475 } = Connections.get_state(name)
478 test "opens ssl connection and reuse it on next request", %{name: name} do
479 url = "https://httpbin.org"
480 :ok = Conn.open(url, name)
482 conn = Connections.checkin(url, name)
485 assert Process.alive?(conn)
487 reused_conn = Connections.checkin(url, name)
489 assert conn == reused_conn
493 "https:httpbin.org:443" => %Conn{
498 } = Connections.get_state(name)
501 test "remove frequently used and idle", %{name: name} do
503 https1 = "https://www.google.com"
504 https2 = "https://httpbin.org"
506 :ok = Conn.open(https1, name)
507 :ok = Conn.open(https2, name)
509 conn = Connections.checkin(https1, name)
512 Connections.checkin(https2, name)
517 "https:httpbin.org:443" => %Conn{
521 "https:www.google.com:443" => %Conn{
526 } = Connections.get_state(name)
528 :ok = Connections.checkout(conn, self, name)
529 http = "http://httpbin.org"
531 :ok = Conn.open(http, name)
532 conn = Connections.checkin(http, name)
536 "http:httpbin.org:80" => %Conn{
540 "https:httpbin.org:443" => %Conn{
545 } = Connections.get_state(name)
548 test "remove earlier used and idle", %{name: name} do
551 https1 = "https://www.google.com"
552 https2 = "https://httpbin.org"
553 :ok = Conn.open(https1, name)
554 :ok = Conn.open(https2, name)
557 Connections.checkin(https1, name)
558 conn = Connections.checkin(https1, name)
561 Connections.checkin(https2, name)
562 Connections.checkin(https2, name)
566 "https:httpbin.org:443" => %Conn{
570 "https:www.google.com:443" => %Conn{
575 } = Connections.get_state(name)
577 :ok = Connections.checkout(conn, self, name)
578 :ok = Connections.checkout(conn, self, name)
580 http = "http://httpbin.org"
581 :ok = Conn.open(http, name)
584 conn = Connections.checkin(http, name)
588 "http:httpbin.org:80" => %Conn{
592 "https:httpbin.org:443" => %Conn{
597 } = Connections.get_state(name)
600 test "doesn't open new conn on pool overflow", %{name: name} do
603 https1 = "https://www.google.com"
604 https2 = "https://httpbin.org"
605 :ok = Conn.open(https1, name)
606 :ok = Conn.open(https2, name)
608 Connections.checkin(https1, name)
609 conn1 = Connections.checkin(https1, name)
610 conn2 = Connections.checkin(https2, name)
614 "https:httpbin.org:443" => %Conn{
618 used_by: [{^self, _}]
620 "https:www.google.com:443" => %Conn{
624 used_by: [{^self, _}, {^self, _}]
627 } = Connections.get_state(name)
629 refute Connections.checkin("http://httpbin.org", name)
633 "https:httpbin.org:443" => %Conn{
637 used_by: [{^self, _}]
639 "https:www.google.com:443" => %Conn{
643 used_by: [{^self, _}, {^self, _}]
646 } = Connections.get_state(name)
649 test "get idle connection with the smallest crf", %{
654 https1 = "https://www.google.com"
655 https2 = "https://httpbin.org"
657 :ok = Conn.open(https1, name)
658 :ok = Conn.open(https2, name)
660 Connections.checkin(https1, name)
661 Connections.checkin(https2, name)
662 Connections.checkin(https1, name)
663 conn1 = Connections.checkin(https1, name)
664 conn2 = Connections.checkin(https2, name)
668 "https:httpbin.org:443" => %Conn{
672 used_by: [{^self, _}, {^self, _}],
675 "https:www.google.com:443" => %Conn{
679 used_by: [{^self, _}, {^self, _}, {^self, _}],
683 } = Connections.get_state(name)
687 :ok = Connections.checkout(conn1, self, name)
688 :ok = Connections.checkout(conn1, self, name)
689 :ok = Connections.checkout(conn1, self, name)
691 :ok = Connections.checkout(conn2, self, name)
692 :ok = Connections.checkout(conn2, self, name)
696 "https:httpbin.org:443" => %Conn{
702 "https:www.google.com:443" => %Conn{
709 } = Connections.get_state(name)
711 http = "http://httpbin.org"
712 :ok = Conn.open(http, name)
714 conn = Connections.checkin(http, name)
718 "https:www.google.com:443" => %Conn{
725 "http:httpbin.org:80" => %Conn{
729 used_by: [{^self, _}],
733 } = Connections.get_state(name)
739 describe "with proxy" do
740 test "as ip", %{name: name} do
741 url = "http://proxy-string.com"
742 key = "http:proxy-string.com:80"
743 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
745 conn = Connections.checkin(url, name)
754 } = Connections.get_state(name)
756 reused_conn = Connections.checkin(url, name)
758 assert reused_conn == conn
761 test "as host", %{name: name} do
762 url = "http://proxy-tuple-atom.com"
763 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
764 conn = Connections.checkin(url, name)
768 "http:proxy-tuple-atom.com:80" => %Conn{
773 } = Connections.get_state(name)
775 reused_conn = Connections.checkin(url, name)
777 assert reused_conn == conn
780 test "as ip and ssl", %{name: name} do
781 url = "https://proxy-string.com"
783 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
784 conn = Connections.checkin(url, name)
788 "https:proxy-string.com:443" => %Conn{
793 } = Connections.get_state(name)
795 reused_conn = Connections.checkin(url, name)
797 assert reused_conn == conn
800 test "as host and ssl", %{name: name} do
801 url = "https://proxy-tuple-atom.com"
802 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
803 conn = Connections.checkin(url, name)
807 "https:proxy-tuple-atom.com:443" => %Conn{
812 } = Connections.get_state(name)
814 reused_conn = Connections.checkin(url, name)
816 assert reused_conn == conn
819 test "with socks type", %{name: name} do
820 url = "http://proxy-socks.com"
822 :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
824 conn = Connections.checkin(url, name)
828 "http:proxy-socks.com:80" => %Conn{
833 } = Connections.get_state(name)
835 reused_conn = Connections.checkin(url, name)
837 assert reused_conn == conn
840 test "with socks4 type and ssl", %{name: name} do
841 url = "https://proxy-socks.com"
843 :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
845 conn = Connections.checkin(url, name)
849 "https:proxy-socks.com:443" => %Conn{
854 } = Connections.get_state(name)
856 reused_conn = Connections.checkin(url, name)
858 assert reused_conn == conn
864 crf = Connections.crf(1, 10, 1)
868 test "more used will have crf higher", %{crf: crf} do
870 crf1 = Connections.crf(1, 10, crf)
871 crf1 = Connections.crf(1, 10, crf1)
874 crf2 = Connections.crf(1, 10, crf)
879 test "recently used will have crf higher on equal references", %{crf: crf} do
881 crf1 = Connections.crf(3, 10, crf)
884 crf2 = Connections.crf(4, 10, crf)
889 test "equal crf on equal reference and time", %{crf: crf} do
891 crf1 = Connections.crf(1, 10, crf)
894 crf2 = Connections.crf(1, 10, crf)
899 test "recently used will have higher crf", %{crf: crf} do
900 crf1 = Connections.crf(2, 10, crf)
901 crf1 = Connections.crf(1, 10, crf1)
903 crf2 = Connections.crf(3, 10, crf)
904 crf2 = Connections.crf(4, 10, crf2)
909 describe "get_unused_conns/1" do
910 test "crf is equalent, sorting by reference", %{name: name} do
911 Connections.add_conn(name, "1", %Conn{
913 last_reference: now() - 1
916 Connections.add_conn(name, "2", %Conn{
918 last_reference: now()
921 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
924 test "reference is equalent, sorting by crf", %{name: name} do
925 Connections.add_conn(name, "1", %Conn{
930 Connections.add_conn(name, "2", %Conn{
935 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
938 test "higher crf and lower reference", %{name: name} do
939 Connections.add_conn(name, "1", %Conn{
942 last_reference: now() - 1
945 Connections.add_conn(name, "2", %Conn{
948 last_reference: now()
951 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
954 test "lower crf and lower reference", %{name: name} do
955 Connections.add_conn(name, "1", %Conn{
958 last_reference: now() - 1
961 Connections.add_conn(name, "2", %Conn{
964 last_reference: now()
967 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
971 test "count/1", %{name: name} do
972 assert Connections.count(name) == 0
973 Connections.add_conn(name, "1", %Conn{conn: self()})
974 assert Connections.count(name) == 1
975 Connections.remove_conn(name, "1")
976 assert Connections.count(name) == 0
980 :os.system_time(:second)