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)
26 on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
29 Connections.start_link({name, [max_connections: 2, receive_connection_timeout: 1_500]})
34 describe "alive?/2" do
35 test "is alive", %{name: name} do
36 assert Connections.alive?(name)
39 test "returns false if not started" do
40 refute Connections.alive?(:some_random_name)
44 test "opens connection and reuse it on next request", %{name: name} do
45 url = "http://some-domain.com"
46 key = "http:some-domain.com:80"
47 refute Connections.checkin(url, name)
48 :ok = Conn.open(url, name)
50 conn = Connections.checkin(url, name)
52 assert Process.alive?(conn)
61 used_by: [{^self, _}],
65 } = Connections.get_state(name)
67 reused_conn = Connections.checkin(url, name)
69 assert conn == reused_conn
76 used_by: [{^self, _}, {^self, _}],
80 } = Connections.get_state(name)
82 :ok = Connections.checkout(conn, self, name)
89 used_by: [{^self, _}],
93 } = Connections.get_state(name)
95 :ok = Connections.checkout(conn, self, name)
106 } = Connections.get_state(name)
109 test "reuse connection for idna domains", %{name: name} do
110 url = "http://ですsome-domain.com"
111 refute Connections.checkin(url, name)
113 :ok = Conn.open(url, name)
115 conn = Connections.checkin(url, name)
117 assert Process.alive?(conn)
123 "http:ですsome-domain.com:80" => %Conn{
126 used_by: [{^self, _}],
130 } = Connections.get_state(name)
132 reused_conn = Connections.checkin(url, name)
134 assert conn == reused_conn
137 test "reuse for ipv4", %{name: name} do
138 url = "http://127.0.0.1"
140 refute Connections.checkin(url, name)
142 :ok = Conn.open(url, name)
144 conn = Connections.checkin(url, name)
146 assert Process.alive?(conn)
152 "http:127.0.0.1:80" => %Conn{
155 used_by: [{^self, _}],
159 } = Connections.get_state(name)
161 reused_conn = Connections.checkin(url, name)
163 assert conn == reused_conn
165 :ok = Connections.checkout(conn, self, name)
166 :ok = Connections.checkout(reused_conn, self, name)
170 "http:127.0.0.1:80" => %Conn{
177 } = Connections.get_state(name)
180 test "reuse for ipv6", %{name: name} do
181 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
183 refute Connections.checkin(url, name)
185 :ok = Conn.open(url, name)
187 conn = Connections.checkin(url, name)
189 assert Process.alive?(conn)
195 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
198 used_by: [{^self, _}],
202 } = Connections.get_state(name)
204 reused_conn = Connections.checkin(url, name)
206 assert conn == reused_conn
209 test "up and down ipv4", %{name: name} do
211 url = "http://127.0.0.1"
212 :ok = Conn.open(url, name)
213 conn = Connections.checkin(url, name)
214 send(name, {:gun_down, conn, nil, nil, nil})
215 send(name, {:gun_up, conn, nil})
219 "http:127.0.0.1:80" => %Conn{
222 used_by: [{^self, _}],
226 } = Connections.get_state(name)
229 test "up and down ipv6", %{name: name} do
231 url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
232 :ok = Conn.open(url, name)
233 conn = Connections.checkin(url, name)
234 send(name, {:gun_down, conn, nil, nil, nil})
235 send(name, {:gun_up, conn, nil})
239 "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
242 used_by: [{^self, _}],
246 } = Connections.get_state(name)
249 test "reuses connection based on protocol", %{name: name} do
250 http_url = "http://some-domain.com"
251 http_key = "http:some-domain.com:80"
252 https_url = "https://some-domain.com"
253 https_key = "https:some-domain.com:443"
255 refute Connections.checkin(http_url, name)
256 :ok = Conn.open(http_url, name)
257 conn = Connections.checkin(http_url, name)
259 assert Process.alive?(conn)
261 refute Connections.checkin(https_url, name)
262 :ok = Conn.open(https_url, name)
263 https_conn = Connections.checkin(https_url, name)
265 refute conn == https_conn
267 reused_https = Connections.checkin(https_url, name)
269 refute conn == reused_https
271 assert reused_https == https_conn
284 } = Connections.get_state(name)
287 test "connection can't get up", %{name: name} do
288 url = "http://gun-not-up.com"
290 assert capture_log(fn ->
291 refute Conn.open(url, name)
292 refute Connections.checkin(url, name)
294 "Received error on opening connection http://gun-not-up.com {:error, :timeout}"
297 test "process gun_down message and then gun_up", %{name: name} do
299 url = "http://gun-down-and-up.com"
300 key = "http:gun-down-and-up.com:80"
301 :ok = Conn.open(url, name)
302 conn = Connections.checkin(url, name)
305 assert Process.alive?(conn)
312 used_by: [{^self, _}]
315 } = Connections.get_state(name)
317 send(name, {:gun_down, conn, :http, nil, nil})
324 used_by: [{^self, _}]
327 } = Connections.get_state(name)
329 send(name, {:gun_up, conn, :http})
331 conn2 = Connections.checkin(url, name)
335 assert Process.alive?(conn2)
342 used_by: [{^self, _}, {^self, _}]
345 } = Connections.get_state(name)
348 test "async processes get same conn for same domain", %{name: name} do
349 url = "http://some-domain.com"
350 :ok = Conn.open(url, name)
355 Connections.checkin(url, name)
359 tasks_with_results = Task.yield_many(tasks)
362 Enum.map(tasks_with_results, fn {task, res} ->
363 res || Task.shutdown(task, :brutal_kill)
366 conns = for {:ok, value} <- results, do: value
370 "http:some-domain.com:80" => %Conn{
375 } = Connections.get_state(name)
377 assert Enum.all?(conns, fn res -> res == conn end)
380 test "remove frequently used and idle", %{name: name} do
382 http_url = "http://some-domain.com"
383 https_url = "https://some-domain.com"
384 :ok = Conn.open(https_url, name)
385 :ok = Conn.open(http_url, name)
387 conn1 = Connections.checkin(https_url, name)
391 Connections.checkin(http_url, name)
394 http_key = "http:some-domain.com:80"
402 used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
404 "https:some-domain.com:443" => %Conn{
408 used_by: [{^self, _}]
411 } = Connections.get_state(name)
413 :ok = Connections.checkout(conn1, self, name)
415 another_url = "http://another-domain.com"
416 :ok = Conn.open(another_url, name)
417 conn = Connections.checkin(another_url, name)
421 "http:another-domain.com:80" => %Conn{
430 } = Connections.get_state(name)
433 describe "integration test" do
434 @describetag :integration
437 Pleroma.Config.put(API, Pleroma.Gun)
440 test "opens connection and change owner", %{name: name} do
441 url = "https://httpbin.org"
442 :ok = Conn.open(url, name)
443 conn = Connections.checkin(url, name)
445 pid = Process.whereis(name)
447 assert :gun.info(conn).owner == pid
450 test "opens connection and reuse it on next request", %{name: name} do
451 url = "http://httpbin.org"
452 :ok = Conn.open(url, name)
454 conn = Connections.checkin(url, name)
457 assert Process.alive?(conn)
459 reused_conn = Connections.checkin(url, name)
461 assert conn == reused_conn
465 "http:httpbin.org:80" => %Conn{
470 } = Connections.get_state(name)
473 test "opens ssl connection and reuse it on next request", %{name: name} do
474 url = "https://httpbin.org"
475 :ok = Conn.open(url, name)
477 conn = Connections.checkin(url, name)
480 assert Process.alive?(conn)
482 reused_conn = Connections.checkin(url, name)
484 assert conn == reused_conn
488 "https:httpbin.org:443" => %Conn{
493 } = Connections.get_state(name)
496 test "remove frequently used and idle", %{name: name} do
498 https1 = "https://www.google.com"
499 https2 = "https://httpbin.org"
501 :ok = Conn.open(https1, name)
502 :ok = Conn.open(https2, name)
504 conn = Connections.checkin(https1, name)
507 Connections.checkin(https2, name)
512 "https:httpbin.org:443" => %Conn{
516 "https:www.google.com:443" => %Conn{
521 } = Connections.get_state(name)
523 :ok = Connections.checkout(conn, self, name)
524 http = "http://httpbin.org"
526 :ok = Conn.open(http, name)
527 conn = Connections.checkin(http, name)
531 "http:httpbin.org:80" => %Conn{
535 "https:httpbin.org:443" => %Conn{
540 } = Connections.get_state(name)
543 test "remove earlier used and idle", %{name: name} do
546 https1 = "https://www.google.com"
547 https2 = "https://httpbin.org"
548 :ok = Conn.open(https1, name)
549 :ok = Conn.open(https2, name)
552 Connections.checkin(https1, name)
553 conn = Connections.checkin(https1, name)
556 Connections.checkin(https2, name)
557 Connections.checkin(https2, name)
561 "https:httpbin.org:443" => %Conn{
565 "https:www.google.com:443" => %Conn{
570 } = Connections.get_state(name)
572 :ok = Connections.checkout(conn, self, name)
573 :ok = Connections.checkout(conn, self, name)
575 http = "http://httpbin.org"
576 :ok = Conn.open(http, name)
579 conn = Connections.checkin(http, name)
583 "http:httpbin.org:80" => %Conn{
587 "https:httpbin.org:443" => %Conn{
592 } = Connections.get_state(name)
595 test "doesn't open new conn on pool overflow", %{name: name} do
598 https1 = "https://www.google.com"
599 https2 = "https://httpbin.org"
600 :ok = Conn.open(https1, name)
601 :ok = Conn.open(https2, name)
603 Connections.checkin(https1, name)
604 conn1 = Connections.checkin(https1, name)
605 conn2 = Connections.checkin(https2, name)
609 "https:httpbin.org:443" => %Conn{
613 used_by: [{^self, _}]
615 "https:www.google.com:443" => %Conn{
619 used_by: [{^self, _}, {^self, _}]
622 } = Connections.get_state(name)
624 refute Connections.checkin("http://httpbin.org", name)
628 "https:httpbin.org:443" => %Conn{
632 used_by: [{^self, _}]
634 "https:www.google.com:443" => %Conn{
638 used_by: [{^self, _}, {^self, _}]
641 } = Connections.get_state(name)
644 test "get idle connection with the smallest crf", %{
649 https1 = "https://www.google.com"
650 https2 = "https://httpbin.org"
652 :ok = Conn.open(https1, name)
653 :ok = Conn.open(https2, name)
655 Connections.checkin(https1, name)
656 Connections.checkin(https2, name)
657 Connections.checkin(https1, name)
658 conn1 = Connections.checkin(https1, name)
659 conn2 = Connections.checkin(https2, name)
663 "https:httpbin.org:443" => %Conn{
667 used_by: [{^self, _}, {^self, _}],
670 "https:www.google.com:443" => %Conn{
674 used_by: [{^self, _}, {^self, _}, {^self, _}],
678 } = Connections.get_state(name)
682 :ok = Connections.checkout(conn1, self, name)
683 :ok = Connections.checkout(conn1, self, name)
684 :ok = Connections.checkout(conn1, self, name)
686 :ok = Connections.checkout(conn2, self, name)
687 :ok = Connections.checkout(conn2, self, name)
691 "https:httpbin.org:443" => %Conn{
697 "https:www.google.com:443" => %Conn{
704 } = Connections.get_state(name)
706 http = "http://httpbin.org"
707 :ok = Conn.open(http, name)
709 conn = Connections.checkin(http, name)
713 "https:www.google.com:443" => %Conn{
720 "http:httpbin.org:80" => %Conn{
724 used_by: [{^self, _}],
728 } = Connections.get_state(name)
734 describe "with proxy" do
735 test "as ip", %{name: name} do
736 url = "http://proxy-string.com"
737 key = "http:proxy-string.com:80"
738 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
740 conn = Connections.checkin(url, name)
749 } = Connections.get_state(name)
751 reused_conn = Connections.checkin(url, name)
753 assert reused_conn == conn
756 test "as host", %{name: name} do
757 url = "http://proxy-tuple-atom.com"
758 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
759 conn = Connections.checkin(url, name)
763 "http:proxy-tuple-atom.com:80" => %Conn{
768 } = Connections.get_state(name)
770 reused_conn = Connections.checkin(url, name)
772 assert reused_conn == conn
775 test "as ip and ssl", %{name: name} do
776 url = "https://proxy-string.com"
778 :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
779 conn = Connections.checkin(url, name)
783 "https:proxy-string.com:443" => %Conn{
788 } = Connections.get_state(name)
790 reused_conn = Connections.checkin(url, name)
792 assert reused_conn == conn
795 test "as host and ssl", %{name: name} do
796 url = "https://proxy-tuple-atom.com"
797 :ok = Conn.open(url, name, proxy: {'localhost', 9050})
798 conn = Connections.checkin(url, name)
802 "https:proxy-tuple-atom.com:443" => %Conn{
807 } = Connections.get_state(name)
809 reused_conn = Connections.checkin(url, name)
811 assert reused_conn == conn
814 test "with socks type", %{name: name} do
815 url = "http://proxy-socks.com"
817 :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
819 conn = Connections.checkin(url, name)
823 "http:proxy-socks.com:80" => %Conn{
828 } = Connections.get_state(name)
830 reused_conn = Connections.checkin(url, name)
832 assert reused_conn == conn
835 test "with socks4 type and ssl", %{name: name} do
836 url = "https://proxy-socks.com"
838 :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
840 conn = Connections.checkin(url, name)
844 "https:proxy-socks.com:443" => %Conn{
849 } = Connections.get_state(name)
851 reused_conn = Connections.checkin(url, name)
853 assert reused_conn == conn
859 crf = Connections.crf(1, 10, 1)
863 test "more used will have crf higher", %{crf: crf} do
865 crf1 = Connections.crf(1, 10, crf)
866 crf1 = Connections.crf(1, 10, crf1)
869 crf2 = Connections.crf(1, 10, crf)
874 test "recently used will have crf higher on equal references", %{crf: crf} do
876 crf1 = Connections.crf(3, 10, crf)
879 crf2 = Connections.crf(4, 10, crf)
884 test "equal crf on equal reference and time", %{crf: crf} do
886 crf1 = Connections.crf(1, 10, crf)
889 crf2 = Connections.crf(1, 10, crf)
894 test "recently used will have higher crf", %{crf: crf} do
895 crf1 = Connections.crf(2, 10, crf)
896 crf1 = Connections.crf(1, 10, crf1)
898 crf2 = Connections.crf(3, 10, crf)
899 crf2 = Connections.crf(4, 10, crf2)
904 describe "get_unused_conns/1" do
905 test "crf is equalent, sorting by reference", %{name: name} do
906 Connections.add_conn(name, "1", %Conn{
908 last_reference: now() - 1
911 Connections.add_conn(name, "2", %Conn{
913 last_reference: now()
916 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
919 test "reference is equalent, sorting by crf", %{name: name} do
920 Connections.add_conn(name, "1", %Conn{
925 Connections.add_conn(name, "2", %Conn{
930 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
933 test "higher crf and lower reference", %{name: name} do
934 Connections.add_conn(name, "1", %Conn{
937 last_reference: now() - 1
940 Connections.add_conn(name, "2", %Conn{
943 last_reference: now()
946 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
949 test "lower crf and lower reference", %{name: name} do
950 Connections.add_conn(name, "1", %Conn{
953 last_reference: now() - 1
956 Connections.add_conn(name, "2", %Conn{
959 last_reference: now()
962 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
966 test "count/1", %{name: name} do
967 assert Connections.count(name) == 0
968 Connections.add_conn(name, "1", %Conn{conn: self()})
969 assert Connections.count(name) == 1
970 Connections.remove_conn(name, "1")
971 assert Connections.count(name) == 0
975 :os.system_time(:second)