1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.Gun.Conn
11 alias Pleroma.Pool.Connections
14 {:ok, _} = Registry.start_link(keys: :unique, name: API.Mock)
18 clear_config([:connections_pool, :retry]) do
19 Pleroma.Config.put([:connections_pool, :retry], 5)
23 name = :test_connections
24 adapter = Application.get_env(:tesla, :adapter)
25 Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
28 Connections.start_link({name, [max_connections: 2, receive_connection_timeout: 1_500]})
31 Application.put_env(:tesla, :adapter, adapter)
33 if Process.alive?(pid) do
41 describe "alive?/2" do
42 test "is alive", %{name: name} do
43 assert Connections.alive?(name)
46 test "returns false if not started" do
47 refute Connections.alive?(:some_random_name)
51 test "opens connection and reuse it on next request", %{name: name} do
52 url = "http://some-domain.com"
53 key = "http:some-domain.com:80"
54 refute Connections.checkin(url, name)
55 :ok = Conn.open(url, name)
57 conn = Connections.checkin(url, name)
59 assert Process.alive?(conn)
68 used_by: [{^self, _}],
72 } = Connections.get_state(name)
74 reused_conn = Connections.checkin(url, name)
76 assert conn == reused_conn
83 used_by: [{^self, _}, {^self, _}],
87 } = Connections.get_state(name)
89 :ok = Connections.checkout(conn, self, name)
96 used_by: [{^self, _}],
100 } = Connections.get_state(name)
102 :ok = Connections.checkout(conn, self, name)
113 } = Connections.get_state(name)
116 test "reuse connection for idna domains", %{name: name} do
117 url = "http://ですsome-domain.com"
118 refute Connections.checkin(url, name)
120 :ok = Conn.open(url, name)
122 conn = Connections.checkin(url, name)
124 assert Process.alive?(conn)
130 "http:ですsome-domain.com:80" => %Conn{
133 used_by: [{^self, _}],
137 } = Connections.get_state(name)
139 reused_conn = Connections.checkin(url, name)
141 assert conn == reused_conn
144 test "reuse for ipv4", %{name: name} do
145 url = "http://127.0.0.1"
147 refute Connections.checkin(url, name)
149 :ok = Conn.open(url, name)
151 conn = Connections.checkin(url, name)
153 assert Process.alive?(conn)
159 "http:127.0.0.1:80" => %Conn{
162 used_by: [{^self, _}],
166 } = Connections.get_state(name)
168 reused_conn = Connections.checkin(url, name)
170 assert conn == reused_conn
172 :ok = Connections.checkout(conn, self, name)
173 :ok = Connections.checkout(reused_conn, self, name)
177 "http:127.0.0.1:80" => %Conn{
184 } = Connections.get_state(name)
187 test "reuse for ipv6", %{name: name} do
188 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
190 refute Connections.checkin(url, name)
192 :ok = Conn.open(url, name)
194 conn = Connections.checkin(url, name)
196 assert Process.alive?(conn)
202 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
205 used_by: [{^self, _}],
209 } = Connections.get_state(name)
211 reused_conn = Connections.checkin(url, name)
213 assert conn == reused_conn
216 test "up and down ipv4", %{name: name} do
218 url = "http://127.0.0.1"
219 :ok = Conn.open(url, name)
220 conn = Connections.checkin(url, name)
221 send(name, {:gun_down, conn, nil, nil, nil})
222 send(name, {:gun_up, conn, nil})
226 "http:127.0.0.1:80" => %Conn{
229 used_by: [{^self, _}],
233 } = Connections.get_state(name)
236 test "up and down ipv6", %{name: name} do
238 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
239 :ok = Conn.open(url, name)
240 conn = Connections.checkin(url, name)
241 send(name, {:gun_down, conn, nil, nil, nil})
242 send(name, {:gun_up, conn, nil})
246 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
249 used_by: [{^self, _}],
253 } = Connections.get_state(name)
256 test "reuses connection based on protocol", %{name: name} do
257 http_url = "http://some-domain.com"
258 http_key = "http:some-domain.com:80"
259 https_url = "https://some-domain.com"
260 https_key = "https:some-domain.com:443"
262 refute Connections.checkin(http_url, name)
263 :ok = Conn.open(http_url, name)
264 conn = Connections.checkin(http_url, name)
266 assert Process.alive?(conn)
268 refute Connections.checkin(https_url, name)
269 :ok = Conn.open(https_url, name)
270 https_conn = Connections.checkin(https_url, name)
272 refute conn == https_conn
274 reused_https = Connections.checkin(https_url, name)
276 refute conn == reused_https
278 assert reused_https == https_conn
291 } = Connections.get_state(name)
294 test "connection can't get up", %{name: name} do
295 url = "http://gun-not-up.com"
297 assert capture_log(fn ->
298 refute Conn.open(url, name)
299 refute Connections.checkin(url, name)
301 "Received error on opening connection http://gun-not-up.com {:error, :timeout}"
304 test "process gun_down message and then gun_up", %{name: name} do
306 url = "http://gun-down-and-up.com"
307 key = "http:gun-down-and-up.com:80"
308 :ok = Conn.open(url, name)
309 conn = Connections.checkin(url, name)
312 assert Process.alive?(conn)
319 used_by: [{^self, _}]
322 } = Connections.get_state(name)
324 send(name, {:gun_down, conn, :http, nil, nil})
331 used_by: [{^self, _}]
334 } = Connections.get_state(name)
336 send(name, {:gun_up, conn, :http})
338 conn2 = Connections.checkin(url, name)
342 assert Process.alive?(conn2)
349 used_by: [{^self, _}, {^self, _}]
352 } = Connections.get_state(name)
355 test "async processes get same conn for same domain", %{name: name} do
356 url = "http://some-domain.com"
357 :ok = Conn.open(url, name)
362 Connections.checkin(url, name)
366 tasks_with_results = Task.yield_many(tasks)
369 Enum.map(tasks_with_results, fn {task, res} ->
370 res || Task.shutdown(task, :brutal_kill)
373 conns = for {:ok, value} <- results, do: value
377 "http:some-domain.com:80" => %Conn{
382 } = Connections.get_state(name)
384 assert Enum.all?(conns, fn res -> res == conn end)
387 test "remove frequently used and idle", %{name: name} do
389 http_url = "http://some-domain.com"
390 https_url = "https://some-domain.com"
391 :ok = Conn.open(https_url, name)
392 :ok = Conn.open(http_url, name)
394 conn1 = Connections.checkin(https_url, name)
398 Connections.checkin(http_url, name)
401 http_key = "http:some-domain.com:80"
409 used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
411 "https:some-domain.com:443" => %Conn{
415 used_by: [{^self, _}]
418 } = Connections.get_state(name)
420 :ok = Connections.checkout(conn1, self, name)
422 another_url = "http://another-domain.com"
423 :ok = Conn.open(another_url, name)
424 conn = Connections.checkin(another_url, name)
428 "http:another-domain.com:80" => %Conn{
437 } = Connections.get_state(name)
440 describe "integration test" do
441 @describetag :integration
444 Pleroma.Config.put(API, Pleroma.Gun)
447 test "opens connection and change owner", %{name: name} do
448 url = "https://httpbin.org"
449 :ok = Conn.open(url, name)
450 conn = Connections.checkin(url, name)
452 pid = Process.whereis(name)
454 assert :gun.info(conn).owner == pid
457 test "opens connection and reuse it on next request", %{name: name} do
458 url = "http://httpbin.org"
459 :ok = Conn.open(url, name)
461 conn = Connections.checkin(url, name)
464 assert Process.alive?(conn)
466 reused_conn = Connections.checkin(url, name)
468 assert conn == reused_conn
472 "http:httpbin.org:80" => %Conn{
477 } = Connections.get_state(name)
480 test "opens ssl connection and reuse it on next request", %{name: name} do
481 url = "https://httpbin.org"
482 :ok = Conn.open(url, name)
484 conn = Connections.checkin(url, name)
487 assert Process.alive?(conn)
489 reused_conn = Connections.checkin(url, name)
491 assert conn == reused_conn
495 "https:httpbin.org:443" => %Conn{
500 } = Connections.get_state(name)
503 test "remove frequently used and idle", %{name: name} do
505 https1 = "https://www.google.com"
506 https2 = "https://httpbin.org"
508 :ok = Conn.open(https1, name)
509 :ok = Conn.open(https2, name)
511 conn = Connections.checkin(https1, name)
514 Connections.checkin(https2, name)
519 "https:httpbin.org:443" => %Conn{
523 "https:www.google.com:443" => %Conn{
528 } = Connections.get_state(name)
530 :ok = Connections.checkout(conn, self, name)
531 http = "http://httpbin.org"
533 :ok = Conn.open(http, name)
534 conn = Connections.checkin(http, name)
538 "http:httpbin.org:80" => %Conn{
542 "https:httpbin.org:443" => %Conn{
547 } = Connections.get_state(name)
550 test "remove earlier used and idle", %{name: name} do
553 https1 = "https://www.google.com"
554 https2 = "https://httpbin.org"
555 :ok = Conn.open(https1, name)
556 :ok = Conn.open(https2, name)
559 Connections.checkin(https1, name)
560 conn = Connections.checkin(https1, name)
563 Connections.checkin(https2, name)
564 Connections.checkin(https2, name)
568 "https:httpbin.org:443" => %Conn{
572 "https:www.google.com:443" => %Conn{
577 } = Connections.get_state(name)
579 :ok = Connections.checkout(conn, self, name)
580 :ok = Connections.checkout(conn, self, name)
582 http = "http://httpbin.org"
583 :ok = Conn.open(http, name)
586 conn = Connections.checkin(http, name)
590 "http:httpbin.org:80" => %Conn{
594 "https:httpbin.org:443" => %Conn{
599 } = Connections.get_state(name)
602 test "doesn't open new conn on pool overflow", %{name: name} do
605 https1 = "https://www.google.com"
606 https2 = "https://httpbin.org"
607 :ok = Conn.open(https1, name)
608 :ok = Conn.open(https2, name)
610 Connections.checkin(https1, name)
611 conn1 = Connections.checkin(https1, name)
612 conn2 = Connections.checkin(https2, name)
616 "https:httpbin.org:443" => %Conn{
620 used_by: [{^self, _}]
622 "https:www.google.com:443" => %Conn{
626 used_by: [{^self, _}, {^self, _}]
629 } = Connections.get_state(name)
631 refute Connections.checkin("http://httpbin.org", name)
635 "https:httpbin.org:443" => %Conn{
639 used_by: [{^self, _}]
641 "https:www.google.com:443" => %Conn{
645 used_by: [{^self, _}, {^self, _}]
648 } = Connections.get_state(name)
651 test "get idle connection with the smallest crf", %{
656 https1 = "https://www.google.com"
657 https2 = "https://httpbin.org"
659 :ok = Conn.open(https1, name)
660 :ok = Conn.open(https2, name)
662 Connections.checkin(https1, name)
663 Connections.checkin(https2, name)
664 Connections.checkin(https1, name)
665 conn1 = Connections.checkin(https1, name)
666 conn2 = Connections.checkin(https2, name)
670 "https:httpbin.org:443" => %Conn{
674 used_by: [{^self, _}, {^self, _}],
677 "https:www.google.com:443" => %Conn{
681 used_by: [{^self, _}, {^self, _}, {^self, _}],
685 } = Connections.get_state(name)
689 :ok = Connections.checkout(conn1, self, name)
690 :ok = Connections.checkout(conn1, self, name)
691 :ok = Connections.checkout(conn1, self, name)
693 :ok = Connections.checkout(conn2, self, name)
694 :ok = Connections.checkout(conn2, self, name)
698 "https:httpbin.org:443" => %Conn{
704 "https:www.google.com:443" => %Conn{
711 } = Connections.get_state(name)
713 http = "http://httpbin.org"
714 :ok = Conn.open(http, name)
716 conn = Connections.checkin(http, name)
720 "https:www.google.com:443" => %Conn{
727 "http:httpbin.org:80" => %Conn{
731 used_by: [{^self, _}],
735 } = Connections.get_state(name)
741 describe "with proxy" do
742 test "as ip", %{name: name} do
743 url = "http://proxy-string.com"
744 key = "http:proxy-string.com:80"
745 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
747 conn = Connections.checkin(url, name)
756 } = Connections.get_state(name)
758 reused_conn = Connections.checkin(url, name)
760 assert reused_conn == conn
763 test "as host", %{name: name} do
764 url = "http://proxy-tuple-atom.com"
765 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
766 conn = Connections.checkin(url, name)
770 "http:proxy-tuple-atom.com:80" => %Conn{
775 } = Connections.get_state(name)
777 reused_conn = Connections.checkin(url, name)
779 assert reused_conn == conn
782 test "as ip and ssl", %{name: name} do
783 url = "https://proxy-string.com"
785 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
786 conn = Connections.checkin(url, name)
790 "https:proxy-string.com:443" => %Conn{
795 } = Connections.get_state(name)
797 reused_conn = Connections.checkin(url, name)
799 assert reused_conn == conn
802 test "as host and ssl", %{name: name} do
803 url = "https://proxy-tuple-atom.com"
804 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
805 conn = Connections.checkin(url, name)
809 "https:proxy-tuple-atom.com:443" => %Conn{
814 } = Connections.get_state(name)
816 reused_conn = Connections.checkin(url, name)
818 assert reused_conn == conn
821 test "with socks type", %{name: name} do
822 url = "http://proxy-socks.com"
824 :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
826 conn = Connections.checkin(url, name)
830 "http:proxy-socks.com:80" => %Conn{
835 } = Connections.get_state(name)
837 reused_conn = Connections.checkin(url, name)
839 assert reused_conn == conn
842 test "with socks4 type and ssl", %{name: name} do
843 url = "https://proxy-socks.com"
845 :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
847 conn = Connections.checkin(url, name)
851 "https:proxy-socks.com:443" => %Conn{
856 } = Connections.get_state(name)
858 reused_conn = Connections.checkin(url, name)
860 assert reused_conn == conn
866 crf = Connections.crf(1, 10, 1)
870 test "more used will have crf higher", %{crf: crf} do
872 crf1 = Connections.crf(1, 10, crf)
873 crf1 = Connections.crf(1, 10, crf1)
876 crf2 = Connections.crf(1, 10, crf)
881 test "recently used will have crf higher on equal references", %{crf: crf} do
883 crf1 = Connections.crf(3, 10, crf)
886 crf2 = Connections.crf(4, 10, crf)
891 test "equal crf on equal reference and time", %{crf: crf} do
893 crf1 = Connections.crf(1, 10, crf)
896 crf2 = Connections.crf(1, 10, crf)
901 test "recently used will have higher crf", %{crf: crf} do
902 crf1 = Connections.crf(2, 10, crf)
903 crf1 = Connections.crf(1, 10, crf1)
905 crf2 = Connections.crf(3, 10, crf)
906 crf2 = Connections.crf(4, 10, crf2)
911 describe "get_unused_conns/1" do
912 test "crf is equalent, sorting by reference", %{name: name} do
913 Connections.add_conn(name, "1", %Conn{
915 last_reference: now() - 1
918 Connections.add_conn(name, "2", %Conn{
920 last_reference: now()
923 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
926 test "reference is equalent, sorting by crf", %{name: name} do
927 Connections.add_conn(name, "1", %Conn{
932 Connections.add_conn(name, "2", %Conn{
937 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
940 test "higher crf and lower reference", %{name: name} do
941 Connections.add_conn(name, "1", %Conn{
944 last_reference: now() - 1
947 Connections.add_conn(name, "2", %Conn{
950 last_reference: now()
953 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
956 test "lower crf and lower reference", %{name: name} do
957 Connections.add_conn(name, "1", %Conn{
960 last_reference: now() - 1
963 Connections.add_conn(name, "2", %Conn{
966 last_reference: now()
969 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
973 test "count/1", %{name: name} do
974 assert Connections.count(name) == 0
975 Connections.add_conn(name, "1", %Conn{conn: self()})
976 assert Connections.count(name) == 1
977 Connections.remove_conn(name, "1")
978 assert Connections.count(name) == 0
982 :os.system_time(:second)