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