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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(http_url, name)
257 conn = Connections.checkin(http_url, name)
259 assert Process.alive?(conn)
261 refute Connections.checkin(https_url, name)
262 :ok = Connections.open_conn(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 :ok = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(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 = Connections.open_conn(https_url, name)
385 :ok = Connections.open_conn(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 = Connections.open_conn(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 reuse it on next request", %{name: name} do
441 url = "http://httpbin.org"
442 :ok = Connections.open_conn(url, name)
444 conn = Connections.checkin(url, name)
447 assert Process.alive?(conn)
449 reused_conn = Connections.checkin(url, name)
451 assert conn == reused_conn
455 "http:httpbin.org:80" => %Conn{
460 } = Connections.get_state(name)
463 test "opens ssl connection and reuse it on next request", %{name: name} do
464 url = "https://httpbin.org"
465 :ok = Connections.open_conn(url, name)
467 conn = Connections.checkin(url, name)
470 assert Process.alive?(conn)
472 reused_conn = Connections.checkin(url, name)
474 assert conn == reused_conn
478 "https:httpbin.org:443" => %Conn{
483 } = Connections.get_state(name)
486 test "remove frequently used and idle", %{name: name} do
488 https1 = "https://www.google.com"
489 https2 = "https://httpbin.org"
491 :ok = Connections.open_conn(https1, name)
492 :ok = Connections.open_conn(https2, name)
494 conn = Connections.checkin(https1, name)
497 Connections.checkin(https2, name)
502 "https:httpbin.org:443" => %Conn{
506 "https:www.google.com:443" => %Conn{
511 } = Connections.get_state(name)
513 :ok = Connections.checkout(conn, self, name)
514 http = "http://httpbin.org"
516 :ok = Connections.open_conn(http, name)
517 conn = Connections.checkin(http, name)
521 "http:httpbin.org:80" => %Conn{
525 "https:httpbin.org:443" => %Conn{
530 } = Connections.get_state(name)
533 test "remove earlier used and idle", %{name: name} do
536 https1 = "https://www.google.com"
537 https2 = "https://httpbin.org"
538 :ok = Connections.open_conn(https1, name)
539 :ok = Connections.open_conn(https2, name)
542 Connections.checkin(https1, name)
543 conn = Connections.checkin(https1, name)
546 Connections.checkin(https2, name)
547 Connections.checkin(https2, name)
551 "https:httpbin.org:443" => %Conn{
555 "https:www.google.com:443" => %Conn{
560 } = Connections.get_state(name)
562 :ok = Connections.checkout(conn, self, name)
563 :ok = Connections.checkout(conn, self, name)
565 http = "http://httpbin.org"
566 :ok = Connections.open_conn(http, name)
569 conn = Connections.checkin(http, name)
573 "http:httpbin.org:80" => %Conn{
577 "https:httpbin.org:443" => %Conn{
582 } = Connections.get_state(name)
585 test "doesn't open new conn on pool overflow", %{name: name} do
588 https1 = "https://www.google.com"
589 https2 = "https://httpbin.org"
590 :ok = Connections.open_conn(https1, name)
591 :ok = Connections.open_conn(https2, name)
593 Connections.checkin(https1, name)
594 conn1 = Connections.checkin(https1, name)
595 conn2 = Connections.checkin(https2, name)
599 "https:httpbin.org:443" => %Conn{
603 used_by: [{^self, _}]
605 "https:www.google.com:443" => %Conn{
609 used_by: [{^self, _}, {^self, _}]
612 } = Connections.get_state(name)
614 refute Connections.checkin("http://httpbin.org", name)
618 "https:httpbin.org:443" => %Conn{
622 used_by: [{^self, _}]
624 "https:www.google.com:443" => %Conn{
628 used_by: [{^self, _}, {^self, _}]
631 } = Connections.get_state(name)
634 test "get idle connection with the smallest crf", %{
639 https1 = "https://www.google.com"
640 https2 = "https://httpbin.org"
642 :ok = Connections.open_conn(https1, name)
643 :ok = Connections.open_conn(https2, name)
645 Connections.checkin(https1, name)
646 Connections.checkin(https2, name)
647 Connections.checkin(https1, name)
648 conn1 = Connections.checkin(https1, name)
649 conn2 = Connections.checkin(https2, name)
653 "https:httpbin.org:443" => %Conn{
657 used_by: [{^self, _}, {^self, _}],
660 "https:www.google.com:443" => %Conn{
664 used_by: [{^self, _}, {^self, _}, {^self, _}],
668 } = Connections.get_state(name)
672 :ok = Connections.checkout(conn1, self, name)
673 :ok = Connections.checkout(conn1, self, name)
674 :ok = Connections.checkout(conn1, self, name)
676 :ok = Connections.checkout(conn2, self, name)
677 :ok = Connections.checkout(conn2, self, name)
681 "https:httpbin.org:443" => %Conn{
687 "https:www.google.com:443" => %Conn{
694 } = Connections.get_state(name)
696 http = "http://httpbin.org"
697 :ok = Connections.open_conn(http, name)
699 conn = Connections.checkin(http, name)
703 "https:www.google.com:443" => %Conn{
710 "http:httpbin.org:80" => %Conn{
714 used_by: [{^self, _}],
718 } = Connections.get_state(name)
724 describe "with proxy" do
725 test "as ip", %{name: name} do
726 url = "http://proxy-string.com"
727 key = "http:proxy-string.com:80"
728 :ok = Connections.open_conn(url, name, proxy: {{127, 0, 0, 1}, 8123})
730 conn = Connections.checkin(url, name)
739 } = Connections.get_state(name)
741 reused_conn = Connections.checkin(url, name)
743 assert reused_conn == conn
746 test "as host", %{name: name} do
747 url = "http://proxy-tuple-atom.com"
748 :ok = Connections.open_conn(url, name, proxy: {'localhost', 9050})
749 conn = Connections.checkin(url, name)
753 "http:proxy-tuple-atom.com:80" => %Conn{
758 } = Connections.get_state(name)
760 reused_conn = Connections.checkin(url, name)
762 assert reused_conn == conn
765 test "as ip and ssl", %{name: name} do
766 url = "https://proxy-string.com"
768 :ok = Connections.open_conn(url, name, proxy: {{127, 0, 0, 1}, 8123})
769 conn = Connections.checkin(url, name)
773 "https:proxy-string.com:443" => %Conn{
778 } = Connections.get_state(name)
780 reused_conn = Connections.checkin(url, name)
782 assert reused_conn == conn
785 test "as host and ssl", %{name: name} do
786 url = "https://proxy-tuple-atom.com"
787 :ok = Connections.open_conn(url, name, proxy: {'localhost', 9050})
788 conn = Connections.checkin(url, name)
792 "https:proxy-tuple-atom.com:443" => %Conn{
797 } = Connections.get_state(name)
799 reused_conn = Connections.checkin(url, name)
801 assert reused_conn == conn
804 test "with socks type", %{name: name} do
805 url = "http://proxy-socks.com"
807 :ok = Connections.open_conn(url, name, proxy: {:socks5, 'localhost', 1234})
809 conn = Connections.checkin(url, name)
813 "http:proxy-socks.com:80" => %Conn{
818 } = Connections.get_state(name)
820 reused_conn = Connections.checkin(url, name)
822 assert reused_conn == conn
825 test "with socks4 type and ssl", %{name: name} do
826 url = "https://proxy-socks.com"
828 :ok = Connections.open_conn(url, name, proxy: {:socks4, 'localhost', 1234})
830 conn = Connections.checkin(url, name)
834 "https:proxy-socks.com:443" => %Conn{
839 } = Connections.get_state(name)
841 reused_conn = Connections.checkin(url, name)
843 assert reused_conn == conn
849 crf = Connections.crf(1, 10, 1)
853 test "more used will have crf higher", %{crf: crf} do
855 crf1 = Connections.crf(1, 10, crf)
856 crf1 = Connections.crf(1, 10, crf1)
859 crf2 = Connections.crf(1, 10, crf)
864 test "recently used will have crf higher on equal references", %{crf: crf} do
866 crf1 = Connections.crf(3, 10, crf)
869 crf2 = Connections.crf(4, 10, crf)
874 test "equal crf on equal reference and time", %{crf: crf} do
876 crf1 = Connections.crf(1, 10, crf)
879 crf2 = Connections.crf(1, 10, crf)
884 test "recently used will have higher crf", %{crf: crf} do
885 crf1 = Connections.crf(2, 10, crf)
886 crf1 = Connections.crf(1, 10, crf1)
888 crf2 = Connections.crf(3, 10, crf)
889 crf2 = Connections.crf(4, 10, crf2)
894 describe "get_unused_conns/1" do
895 test "crf is equalent, sorting by reference" do
899 last_reference: now() - 1
903 last_reference: now()
907 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
910 test "reference is equalent, sorting by crf" do
922 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
925 test "higher crf and lower reference" do
930 last_reference: now() - 1
935 last_reference: now()
939 assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(conns)
942 test "lower crf and lower reference" do
947 last_reference: now() - 1
952 last_reference: now()
956 assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
961 :os.system_time(:second)