Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

761 lines
17KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Pool.ConnectionsTest do
  5. use ExUnit.Case, async: true
  6. use Pleroma.Tests.Helpers
  7. import ExUnit.CaptureLog
  8. import Mox
  9. alias Pleroma.Gun.Conn
  10. alias Pleroma.GunMock
  11. alias Pleroma.Pool.Connections
  12. setup :verify_on_exit!
  13. setup_all do
  14. name = :test_connections
  15. {:ok, pid} = Connections.start_link({name, [checkin_timeout: 150]})
  16. {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock)
  17. on_exit(fn ->
  18. if Process.alive?(pid), do: GenServer.stop(name)
  19. end)
  20. {:ok, name: name}
  21. end
  22. defp open_mock(num \\ 1) do
  23. GunMock
  24. |> expect(:open, num, &start_and_register(&1, &2, &3))
  25. |> expect(:await_up, num, fn _, _ -> {:ok, :http} end)
  26. |> expect(:set_owner, num, fn _, _ -> :ok end)
  27. end
  28. defp connect_mock(mock) do
  29. mock
  30. |> expect(:connect, &connect(&1, &2))
  31. |> expect(:await, &await(&1, &2))
  32. end
  33. defp info_mock(mock), do: expect(mock, :info, &info(&1))
  34. defp start_and_register('gun-not-up.com', _, _), do: {:error, :timeout}
  35. defp start_and_register(host, port, _) do
  36. {:ok, pid} = Task.start_link(fn -> Process.sleep(1000) end)
  37. scheme =
  38. case port do
  39. 443 -> "https"
  40. _ -> "http"
  41. end
  42. Registry.register(GunMock, pid, %{
  43. origin_scheme: scheme,
  44. origin_host: host,
  45. origin_port: port
  46. })
  47. {:ok, pid}
  48. end
  49. defp info(pid) do
  50. [{_, info}] = Registry.lookup(GunMock, pid)
  51. info
  52. end
  53. defp connect(pid, _) do
  54. ref = make_ref()
  55. Registry.register(GunMock, ref, pid)
  56. ref
  57. end
  58. defp await(pid, ref) do
  59. [{_, ^pid}] = Registry.lookup(GunMock, ref)
  60. {:response, :fin, 200, []}
  61. end
  62. defp now, do: :os.system_time(:second)
  63. describe "alive?/2" do
  64. test "is alive", %{name: name} do
  65. assert Connections.alive?(name)
  66. end
  67. test "returns false if not started" do
  68. refute Connections.alive?(:some_random_name)
  69. end
  70. end
  71. test "opens connection and reuse it on next request", %{name: name} do
  72. open_mock()
  73. url = "http://some-domain.com"
  74. key = "http:some-domain.com:80"
  75. refute Connections.checkin(url, name)
  76. :ok = Conn.open(url, name)
  77. conn = Connections.checkin(url, name)
  78. assert is_pid(conn)
  79. assert Process.alive?(conn)
  80. self = self()
  81. %Connections{
  82. conns: %{
  83. ^key => %Conn{
  84. conn: ^conn,
  85. gun_state: :up,
  86. used_by: [{^self, _}],
  87. conn_state: :active
  88. }
  89. }
  90. } = Connections.get_state(name)
  91. reused_conn = Connections.checkin(url, name)
  92. assert conn == reused_conn
  93. %Connections{
  94. conns: %{
  95. ^key => %Conn{
  96. conn: ^conn,
  97. gun_state: :up,
  98. used_by: [{^self, _}, {^self, _}],
  99. conn_state: :active
  100. }
  101. }
  102. } = Connections.get_state(name)
  103. :ok = Connections.checkout(conn, self, name)
  104. %Connections{
  105. conns: %{
  106. ^key => %Conn{
  107. conn: ^conn,
  108. gun_state: :up,
  109. used_by: [{^self, _}],
  110. conn_state: :active
  111. }
  112. }
  113. } = Connections.get_state(name)
  114. :ok = Connections.checkout(conn, self, name)
  115. %Connections{
  116. conns: %{
  117. ^key => %Conn{
  118. conn: ^conn,
  119. gun_state: :up,
  120. used_by: [],
  121. conn_state: :idle
  122. }
  123. }
  124. } = Connections.get_state(name)
  125. end
  126. test "reuse connection for idna domains", %{name: name} do
  127. open_mock()
  128. url = "http://ですsome-domain.com"
  129. refute Connections.checkin(url, name)
  130. :ok = Conn.open(url, name)
  131. conn = Connections.checkin(url, name)
  132. assert is_pid(conn)
  133. assert Process.alive?(conn)
  134. self = self()
  135. %Connections{
  136. conns: %{
  137. "http:ですsome-domain.com:80" => %Conn{
  138. conn: ^conn,
  139. gun_state: :up,
  140. used_by: [{^self, _}],
  141. conn_state: :active
  142. }
  143. }
  144. } = Connections.get_state(name)
  145. reused_conn = Connections.checkin(url, name)
  146. assert conn == reused_conn
  147. end
  148. test "reuse for ipv4", %{name: name} do
  149. open_mock()
  150. url = "http://127.0.0.1"
  151. refute Connections.checkin(url, name)
  152. :ok = Conn.open(url, name)
  153. conn = Connections.checkin(url, name)
  154. assert is_pid(conn)
  155. assert Process.alive?(conn)
  156. self = self()
  157. %Connections{
  158. conns: %{
  159. "http:127.0.0.1:80" => %Conn{
  160. conn: ^conn,
  161. gun_state: :up,
  162. used_by: [{^self, _}],
  163. conn_state: :active
  164. }
  165. }
  166. } = Connections.get_state(name)
  167. reused_conn = Connections.checkin(url, name)
  168. assert conn == reused_conn
  169. :ok = Connections.checkout(conn, self, name)
  170. :ok = Connections.checkout(reused_conn, self, name)
  171. %Connections{
  172. conns: %{
  173. "http:127.0.0.1:80" => %Conn{
  174. conn: ^conn,
  175. gun_state: :up,
  176. used_by: [],
  177. conn_state: :idle
  178. }
  179. }
  180. } = Connections.get_state(name)
  181. end
  182. test "reuse for ipv6", %{name: name} do
  183. open_mock()
  184. url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
  185. refute Connections.checkin(url, name)
  186. :ok = Conn.open(url, name)
  187. conn = Connections.checkin(url, name)
  188. assert is_pid(conn)
  189. assert Process.alive?(conn)
  190. self = self()
  191. %Connections{
  192. conns: %{
  193. "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
  194. conn: ^conn,
  195. gun_state: :up,
  196. used_by: [{^self, _}],
  197. conn_state: :active
  198. }
  199. }
  200. } = Connections.get_state(name)
  201. reused_conn = Connections.checkin(url, name)
  202. assert conn == reused_conn
  203. end
  204. test "up and down ipv4", %{name: name} do
  205. open_mock()
  206. |> info_mock()
  207. |> allow(self(), name)
  208. self = self()
  209. url = "http://127.0.0.1"
  210. :ok = Conn.open(url, name)
  211. conn = Connections.checkin(url, name)
  212. send(name, {:gun_down, conn, nil, nil, nil})
  213. send(name, {:gun_up, conn, nil})
  214. %Connections{
  215. conns: %{
  216. "http:127.0.0.1:80" => %Conn{
  217. conn: ^conn,
  218. gun_state: :up,
  219. used_by: [{^self, _}],
  220. conn_state: :active
  221. }
  222. }
  223. } = Connections.get_state(name)
  224. end
  225. test "up and down ipv6", %{name: name} do
  226. self = self()
  227. open_mock()
  228. |> info_mock()
  229. |> allow(self, name)
  230. url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
  231. :ok = Conn.open(url, name)
  232. conn = Connections.checkin(url, name)
  233. send(name, {:gun_down, conn, nil, nil, nil})
  234. send(name, {:gun_up, conn, nil})
  235. %Connections{
  236. conns: %{
  237. "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
  238. conn: ^conn,
  239. gun_state: :up,
  240. used_by: [{^self, _}],
  241. conn_state: :active
  242. }
  243. }
  244. } = Connections.get_state(name)
  245. end
  246. test "reuses connection based on protocol", %{name: name} do
  247. open_mock(2)
  248. http_url = "http://some-domain.com"
  249. http_key = "http:some-domain.com:80"
  250. https_url = "https://some-domain.com"
  251. https_key = "https:some-domain.com:443"
  252. refute Connections.checkin(http_url, name)
  253. :ok = Conn.open(http_url, name)
  254. conn = Connections.checkin(http_url, name)
  255. assert is_pid(conn)
  256. assert Process.alive?(conn)
  257. refute Connections.checkin(https_url, name)
  258. :ok = Conn.open(https_url, name)
  259. https_conn = Connections.checkin(https_url, name)
  260. refute conn == https_conn
  261. reused_https = Connections.checkin(https_url, name)
  262. refute conn == reused_https
  263. assert reused_https == https_conn
  264. %Connections{
  265. conns: %{
  266. ^http_key => %Conn{
  267. conn: ^conn,
  268. gun_state: :up
  269. },
  270. ^https_key => %Conn{
  271. conn: ^https_conn,
  272. gun_state: :up
  273. }
  274. }
  275. } = Connections.get_state(name)
  276. end
  277. test "connection can't get up", %{name: name} do
  278. expect(GunMock, :open, &start_and_register(&1, &2, &3))
  279. url = "http://gun-not-up.com"
  280. assert capture_log(fn ->
  281. refute Conn.open(url, name)
  282. refute Connections.checkin(url, name)
  283. end) =~
  284. "Opening connection to http://gun-not-up.com failed with error {:error, :timeout}"
  285. end
  286. test "process gun_down message and then gun_up", %{name: name} do
  287. self = self()
  288. open_mock()
  289. |> info_mock()
  290. |> allow(self, name)
  291. url = "http://gun-down-and-up.com"
  292. key = "http:gun-down-and-up.com:80"
  293. :ok = Conn.open(url, name)
  294. conn = Connections.checkin(url, name)
  295. assert is_pid(conn)
  296. assert Process.alive?(conn)
  297. %Connections{
  298. conns: %{
  299. ^key => %Conn{
  300. conn: ^conn,
  301. gun_state: :up,
  302. used_by: [{^self, _}]
  303. }
  304. }
  305. } = Connections.get_state(name)
  306. send(name, {:gun_down, conn, :http, nil, nil})
  307. %Connections{
  308. conns: %{
  309. ^key => %Conn{
  310. conn: ^conn,
  311. gun_state: :down,
  312. used_by: [{^self, _}]
  313. }
  314. }
  315. } = Connections.get_state(name)
  316. send(name, {:gun_up, conn, :http})
  317. conn2 = Connections.checkin(url, name)
  318. assert conn == conn2
  319. assert is_pid(conn2)
  320. assert Process.alive?(conn2)
  321. %Connections{
  322. conns: %{
  323. ^key => %Conn{
  324. conn: _,
  325. gun_state: :up,
  326. used_by: [{^self, _}, {^self, _}]
  327. }
  328. }
  329. } = Connections.get_state(name)
  330. end
  331. test "async processes get same conn for same domain", %{name: name} do
  332. open_mock()
  333. url = "http://some-domain.com"
  334. :ok = Conn.open(url, name)
  335. tasks =
  336. for _ <- 1..5 do
  337. Task.async(fn ->
  338. Connections.checkin(url, name)
  339. end)
  340. end
  341. tasks_with_results = Task.yield_many(tasks)
  342. results =
  343. Enum.map(tasks_with_results, fn {task, res} ->
  344. res || Task.shutdown(task, :brutal_kill)
  345. end)
  346. conns = for {:ok, value} <- results, do: value
  347. %Connections{
  348. conns: %{
  349. "http:some-domain.com:80" => %Conn{
  350. conn: conn,
  351. gun_state: :up
  352. }
  353. }
  354. } = Connections.get_state(name)
  355. assert Enum.all?(conns, fn res -> res == conn end)
  356. end
  357. test "remove frequently used and idle", %{name: name} do
  358. open_mock(3)
  359. self = self()
  360. http_url = "http://some-domain.com"
  361. https_url = "https://some-domain.com"
  362. :ok = Conn.open(https_url, name)
  363. :ok = Conn.open(http_url, name)
  364. conn1 = Connections.checkin(https_url, name)
  365. [conn2 | _conns] =
  366. for _ <- 1..4 do
  367. Connections.checkin(http_url, name)
  368. end
  369. http_key = "http:some-domain.com:80"
  370. %Connections{
  371. conns: %{
  372. ^http_key => %Conn{
  373. conn: ^conn2,
  374. gun_state: :up,
  375. conn_state: :active,
  376. used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
  377. },
  378. "https:some-domain.com:443" => %Conn{
  379. conn: ^conn1,
  380. gun_state: :up,
  381. conn_state: :active,
  382. used_by: [{^self, _}]
  383. }
  384. }
  385. } = Connections.get_state(name)
  386. :ok = Connections.checkout(conn1, self, name)
  387. another_url = "http://another-domain.com"
  388. :ok = Conn.open(another_url, name)
  389. conn = Connections.checkin(another_url, name)
  390. %Connections{
  391. conns: %{
  392. "http:another-domain.com:80" => %Conn{
  393. conn: ^conn,
  394. gun_state: :up
  395. },
  396. ^http_key => %Conn{
  397. conn: _,
  398. gun_state: :up
  399. }
  400. }
  401. } = Connections.get_state(name)
  402. end
  403. describe "with proxy" do
  404. test "as ip", %{name: name} do
  405. open_mock()
  406. |> connect_mock()
  407. url = "http://proxy-string.com"
  408. key = "http:proxy-string.com:80"
  409. :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
  410. conn = Connections.checkin(url, name)
  411. %Connections{
  412. conns: %{
  413. ^key => %Conn{
  414. conn: ^conn,
  415. gun_state: :up
  416. }
  417. }
  418. } = Connections.get_state(name)
  419. reused_conn = Connections.checkin(url, name)
  420. assert reused_conn == conn
  421. end
  422. test "as host", %{name: name} do
  423. open_mock()
  424. |> connect_mock()
  425. url = "http://proxy-tuple-atom.com"
  426. :ok = Conn.open(url, name, proxy: {'localhost', 9050})
  427. conn = Connections.checkin(url, name)
  428. %Connections{
  429. conns: %{
  430. "http:proxy-tuple-atom.com:80" => %Conn{
  431. conn: ^conn,
  432. gun_state: :up
  433. }
  434. }
  435. } = Connections.get_state(name)
  436. reused_conn = Connections.checkin(url, name)
  437. assert reused_conn == conn
  438. end
  439. test "as ip and ssl", %{name: name} do
  440. open_mock()
  441. |> connect_mock()
  442. url = "https://proxy-string.com"
  443. :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123})
  444. conn = Connections.checkin(url, name)
  445. %Connections{
  446. conns: %{
  447. "https:proxy-string.com:443" => %Conn{
  448. conn: ^conn,
  449. gun_state: :up
  450. }
  451. }
  452. } = Connections.get_state(name)
  453. reused_conn = Connections.checkin(url, name)
  454. assert reused_conn == conn
  455. end
  456. test "as host and ssl", %{name: name} do
  457. open_mock()
  458. |> connect_mock()
  459. url = "https://proxy-tuple-atom.com"
  460. :ok = Conn.open(url, name, proxy: {'localhost', 9050})
  461. conn = Connections.checkin(url, name)
  462. %Connections{
  463. conns: %{
  464. "https:proxy-tuple-atom.com:443" => %Conn{
  465. conn: ^conn,
  466. gun_state: :up
  467. }
  468. }
  469. } = Connections.get_state(name)
  470. reused_conn = Connections.checkin(url, name)
  471. assert reused_conn == conn
  472. end
  473. test "with socks type", %{name: name} do
  474. open_mock()
  475. url = "http://proxy-socks.com"
  476. :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234})
  477. conn = Connections.checkin(url, name)
  478. %Connections{
  479. conns: %{
  480. "http:proxy-socks.com:80" => %Conn{
  481. conn: ^conn,
  482. gun_state: :up
  483. }
  484. }
  485. } = Connections.get_state(name)
  486. reused_conn = Connections.checkin(url, name)
  487. assert reused_conn == conn
  488. end
  489. test "with socks4 type and ssl", %{name: name} do
  490. open_mock()
  491. url = "https://proxy-socks.com"
  492. :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234})
  493. conn = Connections.checkin(url, name)
  494. %Connections{
  495. conns: %{
  496. "https:proxy-socks.com:443" => %Conn{
  497. conn: ^conn,
  498. gun_state: :up
  499. }
  500. }
  501. } = Connections.get_state(name)
  502. reused_conn = Connections.checkin(url, name)
  503. assert reused_conn == conn
  504. end
  505. end
  506. describe "crf/3" do
  507. setup do
  508. crf = Connections.crf(1, 10, 1)
  509. {:ok, crf: crf}
  510. end
  511. test "more used will have crf higher", %{crf: crf} do
  512. # used 3 times
  513. crf1 = Connections.crf(1, 10, crf)
  514. crf1 = Connections.crf(1, 10, crf1)
  515. # used 2 times
  516. crf2 = Connections.crf(1, 10, crf)
  517. assert crf1 > crf2
  518. end
  519. test "recently used will have crf higher on equal references", %{crf: crf} do
  520. # used 3 sec ago
  521. crf1 = Connections.crf(3, 10, crf)
  522. # used 4 sec ago
  523. crf2 = Connections.crf(4, 10, crf)
  524. assert crf1 > crf2
  525. end
  526. test "equal crf on equal reference and time", %{crf: crf} do
  527. # used 2 times
  528. crf1 = Connections.crf(1, 10, crf)
  529. # used 2 times
  530. crf2 = Connections.crf(1, 10, crf)
  531. assert crf1 == crf2
  532. end
  533. test "recently used will have higher crf", %{crf: crf} do
  534. crf1 = Connections.crf(2, 10, crf)
  535. crf1 = Connections.crf(1, 10, crf1)
  536. crf2 = Connections.crf(3, 10, crf)
  537. crf2 = Connections.crf(4, 10, crf2)
  538. assert crf1 > crf2
  539. end
  540. end
  541. describe "get_unused_conns/1" do
  542. test "crf is equalent, sorting by reference", %{name: name} do
  543. Connections.add_conn(name, "1", %Conn{
  544. conn_state: :idle,
  545. last_reference: now() - 1
  546. })
  547. Connections.add_conn(name, "2", %Conn{
  548. conn_state: :idle,
  549. last_reference: now()
  550. })
  551. assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
  552. end
  553. test "reference is equalent, sorting by crf", %{name: name} do
  554. Connections.add_conn(name, "1", %Conn{
  555. conn_state: :idle,
  556. crf: 1.999
  557. })
  558. Connections.add_conn(name, "2", %Conn{
  559. conn_state: :idle,
  560. crf: 2
  561. })
  562. assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
  563. end
  564. test "higher crf and lower reference", %{name: name} do
  565. Connections.add_conn(name, "1", %Conn{
  566. conn_state: :idle,
  567. crf: 3,
  568. last_reference: now() - 1
  569. })
  570. Connections.add_conn(name, "2", %Conn{
  571. conn_state: :idle,
  572. crf: 2,
  573. last_reference: now()
  574. })
  575. assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name)
  576. end
  577. test "lower crf and lower reference", %{name: name} do
  578. Connections.add_conn(name, "1", %Conn{
  579. conn_state: :idle,
  580. crf: 1.99,
  581. last_reference: now() - 1
  582. })
  583. Connections.add_conn(name, "2", %Conn{
  584. conn_state: :idle,
  585. crf: 2,
  586. last_reference: now()
  587. })
  588. assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name)
  589. end
  590. end
  591. test "count/1" do
  592. name = :test_count
  593. {:ok, _} = Connections.start_link({name, [checkin_timeout: 150]})
  594. assert Connections.count(name) == 0
  595. Connections.add_conn(name, "1", %Conn{conn: self()})
  596. assert Connections.count(name) == 1
  597. Connections.remove_conn(name, "1")
  598. assert Connections.count(name) == 0
  599. end
  600. end