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)
27 {:ok, pid} = Connections.start_link({name, [max_connections: 2, checkin_timeout: 1_500]})
30 Application.put_env(:tesla, :adapter, adapter)
32 if Process.alive?(pid) do
40 describe "alive?/2" do
41 test "is alive", %{name: name} do
42 assert Connections.alive?(name)
45 test "returns false if not started" do
46 refute Connections.alive?(:some_random_name)
50 test "opens connection and reuse it on next request", %{name: name} do
51 url = "http://some-domain.com"
52 key = "http:some-domain.com:80"
53 refute Connections.checkin(url, name)
54 :ok = Conn.open(url, name)
56 conn = Connections.checkin(url, name)
58 assert Process.alive?(conn)
67 used_by: [{^self, _}],
71 } = Connections.get_state(name)
73 reused_conn = Connections.checkin(url, name)
75 assert conn == reused_conn
82 used_by: [{^self, _}, {^self, _}],
86 } = Connections.get_state(name)
88 :ok = Connections.checkout(conn, self, name)
95 used_by: [{^self, _}],
99 } = Connections.get_state(name)
101 :ok = Connections.checkout(conn, self, name)
112 } = Connections.get_state(name)
115 test "reuse connection for idna domains", %{name: name} do
116 url = "http://ですsome-domain.com"
117 refute Connections.checkin(url, name)
119 :ok = Conn.open(url, name)
121 conn = Connections.checkin(url, name)
123 assert Process.alive?(conn)
129 "http:ですsome-domain.com:80" => %Conn{
132 used_by: [{^self, _}],
136 } = Connections.get_state(name)
138 reused_conn = Connections.checkin(url, name)
140 assert conn == reused_conn
143 test "reuse for ipv4", %{name: name} do
144 url = "http://127.0.0.1"
146 refute Connections.checkin(url, name)
148 :ok = Conn.open(url, name)
150 conn = Connections.checkin(url, name)
152 assert Process.alive?(conn)
158 "http:127.0.0.1:80" => %Conn{
161 used_by: [{^self, _}],
165 } = Connections.get_state(name)
167 reused_conn = Connections.checkin(url, name)
169 assert conn == reused_conn
171 :ok = Connections.checkout(conn, self, name)
172 :ok = Connections.checkout(reused_conn, self, name)
176 "http:127.0.0.1:80" => %Conn{
183 } = Connections.get_state(name)
186 test "reuse for ipv6", %{name: name} do
187 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
189 refute Connections.checkin(url, name)
191 :ok = Conn.open(url, name)
193 conn = Connections.checkin(url, name)
195 assert Process.alive?(conn)
201 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
204 used_by: [{^self, _}],
208 } = Connections.get_state(name)
210 reused_conn = Connections.checkin(url, name)
212 assert conn == reused_conn
215 test "up and down ipv4", %{name: name} do
217 url = "http://127.0.0.1"
218 :ok = Conn.open(url, name)
219 conn = Connections.checkin(url, name)
220 send(name, {:gun_down, conn, nil, nil, nil})
221 send(name, {:gun_up, conn, nil})
225 "http:127.0.0.1:80" => %Conn{
228 used_by: [{^self, _}],
232 } = Connections.get_state(name)
235 test "up and down ipv6", %{name: name} do
237 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
238 :ok = Conn.open(url, name)
239 conn = Connections.checkin(url, name)
240 send(name, {:gun_down, conn, nil, nil, nil})
241 send(name, {:gun_up, conn, nil})
245 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
248 used_by: [{^self, _}],
252 } = Connections.get_state(name)
255 test "reuses connection based on protocol", %{name: name} do
256 http_url = "http://some-domain.com"
257 http_key = "http:some-domain.com:80"
258 https_url = "https://some-domain.com"
259 https_key = "https:some-domain.com:443"
261 refute Connections.checkin(http_url, name)
262 :ok = Conn.open(http_url, name)
263 conn = Connections.checkin(http_url, name)
265 assert Process.alive?(conn)
267 refute Connections.checkin(https_url, name)
268 :ok = Conn.open(https_url, name)
269 https_conn = Connections.checkin(https_url, name)
271 refute conn == https_conn
273 reused_https = Connections.checkin(https_url, name)
275 refute conn == reused_https
277 assert reused_https == https_conn
290 } = Connections.get_state(name)
293 test "connection can't get up", %{name: name} do
294 url = "http://gun-not-up.com"
296 assert capture_log(fn ->
297 refute Conn.open(url, name)
298 refute Connections.checkin(url, name)
300 "Received error on opening connection http://gun-not-up.com {:error, :timeout}"
303 test "process gun_down message and then gun_up", %{name: name} do
305 url = "http://gun-down-and-up.com"
306 key = "http:gun-down-and-up.com:80"
307 :ok = Conn.open(url, name)
308 conn = Connections.checkin(url, name)
311 assert Process.alive?(conn)
318 used_by: [{^self, _}]
321 } = Connections.get_state(name)
323 send(name, {:gun_down, conn, :http, nil, nil})
330 used_by: [{^self, _}]
333 } = Connections.get_state(name)
335 send(name, {:gun_up, conn, :http})
337 conn2 = Connections.checkin(url, name)
341 assert Process.alive?(conn2)
348 used_by: [{^self, _}, {^self, _}]
351 } = Connections.get_state(name)
354 test "async processes get same conn for same domain", %{name: name} do
355 url = "http://some-domain.com"
356 :ok = Conn.open(url, name)
361 Connections.checkin(url, name)
365 tasks_with_results = Task.yield_many(tasks)
368 Enum.map(tasks_with_results, fn {task, res} ->
369 res || Task.shutdown(task, :brutal_kill)
372 conns = for {:ok, value} <- results, do: value
376 "http:some-domain.com:80" => %Conn{
381 } = Connections.get_state(name)
383 assert Enum.all?(conns, fn res -> res == conn end)
386 test "remove frequently used and idle", %{name: name} do
388 http_url = "http://some-domain.com"
389 https_url = "https://some-domain.com"
390 :ok = Conn.open(https_url, name)
391 :ok = Conn.open(http_url, name)
393 conn1 = Connections.checkin(https_url, name)
397 Connections.checkin(http_url, name)
400 http_key = "http:some-domain.com:80"
408 used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
410 "https:some-domain.com:443" => %Conn{
414 used_by: [{^self, _}]
417 } = Connections.get_state(name)
419 :ok = Connections.checkout(conn1, self, name)
421 another_url = "http://another-domain.com"
422 :ok = Conn.open(another_url, name)
423 conn = Connections.checkin(another_url, name)
427 "http:another-domain.com:80" => %Conn{
436 } = Connections.get_state(name)
439 describe "integration test" do
440 @describetag :integration
443 Pleroma.Config.put(API, Pleroma.Gun)
446 test "opens connection and change owner", %{name: name} do
447 url = "https://httpbin.org"
448 :ok = Conn.open(url, name)
449 conn = Connections.checkin(url, name)
451 pid = Process.whereis(name)
453 assert :gun.info(conn).owner == pid
456 test "opens connection and reuse it on next request", %{name: name} do
457 url = "http://httpbin.org"
458 :ok = Conn.open(url, name)
460 conn = Connections.checkin(url, name)
463 assert Process.alive?(conn)
465 reused_conn = Connections.checkin(url, name)
467 assert conn == reused_conn
471 "http:httpbin.org:80" => %Conn{
476 } = Connections.get_state(name)
479 test "opens ssl connection and reuse it on next request", %{name: name} do
480 url = "https://httpbin.org"
481 :ok = Conn.open(url, name)
483 conn = Connections.checkin(url, name)
486 assert Process.alive?(conn)
488 reused_conn = Connections.checkin(url, name)
490 assert conn == reused_conn
494 "https:httpbin.org:443" => %Conn{
499 } = Connections.get_state(name)
502 test "remove frequently used and idle", %{name: name} do
504 https1 = "https://www.google.com"
505 https2 = "https://httpbin.org"
507 :ok = Conn.open(https1, name)
508 :ok = Conn.open(https2, name)
510 conn = Connections.checkin(https1, name)
513 Connections.checkin(https2, name)
518 "https:httpbin.org:443" => %Conn{
522 "https:www.google.com:443" => %Conn{
527 } = Connections.get_state(name)
529 :ok = Connections.checkout(conn, self, name)
530 http = "http://httpbin.org"
532 :ok = Conn.open(http, name)
533 conn = Connections.checkin(http, name)
537 "http:httpbin.org:80" => %Conn{
541 "https:httpbin.org:443" => %Conn{
546 } = Connections.get_state(name)
549 test "remove earlier used and idle", %{name: name} do
552 https1 = "https://www.google.com"
553 https2 = "https://httpbin.org"
554 :ok = Conn.open(https1, name)
555 :ok = Conn.open(https2, name)
558 Connections.checkin(https1, name)
559 conn = Connections.checkin(https1, name)
562 Connections.checkin(https2, name)
563 Connections.checkin(https2, name)
567 "https:httpbin.org:443" => %Conn{
571 "https:www.google.com:443" => %Conn{
576 } = Connections.get_state(name)
578 :ok = Connections.checkout(conn, self, name)
579 :ok = Connections.checkout(conn, self, name)
581 http = "http://httpbin.org"
582 :ok = Conn.open(http, name)
585 conn = Connections.checkin(http, name)
589 "http:httpbin.org:80" => %Conn{
593 "https:httpbin.org:443" => %Conn{
598 } = Connections.get_state(name)
601 test "doesn't open new conn on pool overflow", %{name: name} do
604 https1 = "https://www.google.com"
605 https2 = "https://httpbin.org"
606 :ok = Conn.open(https1, name)
607 :ok = Conn.open(https2, name)
609 Connections.checkin(https1, name)
610 conn1 = Connections.checkin(https1, name)
611 conn2 = Connections.checkin(https2, name)
615 "https:httpbin.org:443" => %Conn{
619 used_by: [{^self, _}]
621 "https:www.google.com:443" => %Conn{
625 used_by: [{^self, _}, {^self, _}]
628 } = Connections.get_state(name)
630 refute Connections.checkin("http://httpbin.org", name)
634 "https:httpbin.org:443" => %Conn{
638 used_by: [{^self, _}]
640 "https:www.google.com:443" => %Conn{
644 used_by: [{^self, _}, {^self, _}]
647 } = Connections.get_state(name)
650 test "get idle connection with the smallest crf", %{
655 https1 = "https://www.google.com"
656 https2 = "https://httpbin.org"
658 :ok = Conn.open(https1, name)
659 :ok = Conn.open(https2, name)
661 Connections.checkin(https1, name)
662 Connections.checkin(https2, name)
663 Connections.checkin(https1, name)
664 conn1 = Connections.checkin(https1, name)
665 conn2 = Connections.checkin(https2, name)
669 "https:httpbin.org:443" => %Conn{
673 used_by: [{^self, _}, {^self, _}],
676 "https:www.google.com:443" => %Conn{
680 used_by: [{^self, _}, {^self, _}, {^self, _}],
684 } = Connections.get_state(name)
688 :ok = Connections.checkout(conn1, self, name)
689 :ok = Connections.checkout(conn1, self, name)
690 :ok = Connections.checkout(conn1, self, name)
692 :ok = Connections.checkout(conn2, self, name)
693 :ok = Connections.checkout(conn2, self, name)
697 "https:httpbin.org:443" => %Conn{
703 "https:www.google.com:443" => %Conn{
710 } = Connections.get_state(name)
712 http = "http://httpbin.org"
713 :ok = Conn.open(http, name)
715 conn = Connections.checkin(http, name)
719 "https:www.google.com:443" => %Conn{
726 "http:httpbin.org:80" => %Conn{
730 used_by: [{^self, _}],
734 } = Connections.get_state(name)
740 describe "with proxy" do
741 test "as ip", %{name: name} do
742 url = "http://proxy-string.com"
743 key = "http:proxy-string.com:80"
744 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
746 conn = Connections.checkin(url, name)
755 } = Connections.get_state(name)
757 reused_conn = Connections.checkin(url, name)
759 assert reused_conn == conn
762 test "as host", %{name: name} do
763 url = "http://proxy-tuple-atom.com"
764 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
765 conn = Connections.checkin(url, name)
769 "http:proxy-tuple-atom.com:80" => %Conn{
774 } = Connections.get_state(name)
776 reused_conn = Connections.checkin(url, name)
778 assert reused_conn == conn
781 test "as ip and ssl", %{name: name} do
782 url = "https://proxy-string.com"
784 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
785 conn = Connections.checkin(url, name)
789 "https:proxy-string.com:443" => %Conn{
794 } = Connections.get_state(name)
796 reused_conn = Connections.checkin(url, name)
798 assert reused_conn == conn
801 test "as host and ssl", %{name: name} do
802 url = "https://proxy-tuple-atom.com"
803 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
804 conn = Connections.checkin(url, name)
808 "https:proxy-tuple-atom.com:443" => %Conn{
813 } = Connections.get_state(name)
815 reused_conn = Connections.checkin(url, name)
817 assert reused_conn == conn
820 test "with socks type", %{name: name} do
821 url = "http://proxy-socks.com"
823 :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
825 conn = Connections.checkin(url, name)
829 "http:proxy-socks.com:80" => %Conn{
834 } = Connections.get_state(name)
836 reused_conn = Connections.checkin(url, name)
838 assert reused_conn == conn
841 test "with socks4 type and ssl", %{name: name} do
842 url = "https://proxy-socks.com"
844 :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
846 conn = Connections.checkin(url, name)
850 "https:proxy-socks.com:443" => %Conn{
855 } = Connections.get_state(name)
857 reused_conn = Connections.checkin(url, name)
859 assert reused_conn == conn
865 crf = Connections.crf(1, 10, 1)
869 test "more used will have crf higher", %{crf: crf} do
871 crf1 = Connections.crf(1, 10, crf)
872 crf1 = Connections.crf(1, 10, crf1)
875 crf2 = Connections.crf(1, 10, crf)
880 test "recently used will have crf higher on equal references", %{crf: crf} do
882 crf1 = Connections.crf(3, 10, crf)
885 crf2 = Connections.crf(4, 10, crf)
890 test "equal crf on equal reference and time", %{crf: crf} do
892 crf1 = Connections.crf(1, 10, crf)
895 crf2 = Connections.crf(1, 10, crf)
900 test "recently used will have higher crf", %{crf: crf} do
901 crf1 = Connections.crf(2, 10, crf)
902 crf1 = Connections.crf(1, 10, crf1)
904 crf2 = Connections.crf(3, 10, crf)
905 crf2 = Connections.crf(4, 10, crf2)
910 describe "get_unused_conns/1" do
911 test "crf is equalent, sorting by reference", %{name: name} do
912 Connections.add_conn(name, "1", %Conn{
914 last_reference: now() - 1
917 Connections.add_conn(name, "2", %Conn{
919 last_reference: now()
922 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
925 test "reference is equalent, sorting by crf", %{name: name} do
926 Connections.add_conn(name, "1", %Conn{
931 Connections.add_conn(name, "2", %Conn{
936 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
939 test "higher crf and lower reference", %{name: name} do
940 Connections.add_conn(name, "1", %Conn{
943 last_reference: now() - 1
946 Connections.add_conn(name, "2", %Conn{
949 last_reference: now()
952 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
955 test "lower crf and lower reference", %{name: name} do
956 Connections.add_conn(name, "1", %Conn{
959 last_reference: now() - 1
962 Connections.add_conn(name, "2", %Conn{
965 last_reference: now()
968 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
972 test "count/1", %{name: name} do
973 assert Connections.count(name) == 0
974 Connections.add_conn(name, "1", %Conn{conn: self()})
975 assert Connections.count(name) == 1
976 Connections.remove_conn(name, "1")
977 assert Connections.count(name) == 0
981 :os.system_time(:second)