|
21 | 21 | #include <fcntl.h> |
22 | 22 | #include <gtest/gtest-message.h> |
23 | 23 | #include <gtest/gtest-test-part.h> |
| 24 | +#include <netinet/in.h> |
24 | 25 | #include <sys/mman.h> |
| 26 | +#include <sys/socket.h> |
25 | 27 | #include <sys/stat.h> |
26 | 28 | #include <unistd.h> |
27 | 29 |
|
| 30 | +#include <atomic> |
28 | 31 | #include <boost/algorithm/string/predicate.hpp> |
| 32 | +#include <chrono> |
| 33 | +#include <cstring> |
29 | 34 | #include <filesystem> |
| 35 | +#include <thread> |
30 | 36 |
|
31 | 37 | #include "gtest/gtest_pred_impl.h" |
32 | 38 | #include "io/fs/local_file_system.h" |
@@ -669,4 +675,57 @@ TEST_F(HttpClientTest, batch_download) { |
669 | 675 | EXPECT_TRUE(st.ok()); |
670 | 676 | } |
671 | 677 |
|
| 678 | +TEST_F(HttpClientTest, abort_in_flight_request) { |
| 679 | + // A listening socket that is never accepted from. The kernel completes the handshake |
| 680 | + // and buffers the request, so the client believes it is connected, but no response ever |
| 681 | + // comes back. This is what an audit stream load looks like when the http service that |
| 682 | + // was supposed to serve it has been torn down. |
| 683 | + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); |
| 684 | + ASSERT_GE(listen_fd, 0); |
| 685 | + struct sockaddr_in addr; |
| 686 | + memset(&addr, 0, sizeof(addr)); |
| 687 | + addr.sin_family = AF_INET; |
| 688 | + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 689 | + addr.sin_port = 0; // let the kernel pick a free port |
| 690 | + ASSERT_EQ(0, bind(listen_fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr))); |
| 691 | + ASSERT_EQ(0, listen(listen_fd, 8)); |
| 692 | + socklen_t addr_len = sizeof(addr); |
| 693 | + ASSERT_EQ(0, getsockname(listen_fd, reinterpret_cast<struct sockaddr*>(&addr), &addr_len)); |
| 694 | + std::string url = "http://127.0.0.1:" + std::to_string(ntohs(addr.sin_port)) + "/no_answer"; |
| 695 | + |
| 696 | + HttpClient client; |
| 697 | + auto st = client.init(url); |
| 698 | + EXPECT_TRUE(st.ok()) << st; |
| 699 | + client.set_method(GET); |
| 700 | + // Much longer than the abort is expected to take, so that finishing early can only be |
| 701 | + // the abort callback and not the timeout. |
| 702 | + client.set_timeout_ms(60 * 1000); |
| 703 | + std::atomic<bool> should_abort {false}; |
| 704 | + client.set_abort_callback([&should_abort]() { return should_abort.load(); }); |
| 705 | + |
| 706 | + // Ask for the abort only once the request is on the wire, like a shutdown starting |
| 707 | + // while a worker is already blocked inside execute(). |
| 708 | + std::thread aborter([&should_abort]() { |
| 709 | + std::this_thread::sleep_for(std::chrono::milliseconds(300)); |
| 710 | + should_abort = true; |
| 711 | + }); |
| 712 | + |
| 713 | + auto start = std::chrono::steady_clock::now(); |
| 714 | + std::string response; |
| 715 | + st = client.execute(&response); |
| 716 | + auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>( |
| 717 | + std::chrono::steady_clock::now() - start) |
| 718 | + .count(); |
| 719 | + aborter.join(); |
| 720 | + close(listen_fd); |
| 721 | + |
| 722 | + EXPECT_FALSE(st.ok()); |
| 723 | + // The request really was stuck waiting rather than failing outright, ... |
| 724 | + EXPECT_GE(elapsed_ms, 300) << "request did not reach the server, it took " << elapsed_ms |
| 725 | + << "ms"; |
| 726 | + // ... and the abort ended it instead of CURLOPT_TIMEOUT_MS. libcurl polls the callback |
| 727 | + // about once a second while the connection is idle. |
| 728 | + EXPECT_LT(elapsed_ms, 15000) << "request was not aborted, it took " << elapsed_ms << "ms"; |
| 729 | +} |
| 730 | + |
672 | 731 | } // namespace doris |
0 commit comments