/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #define BOOST_TEST_MODULE TServerIntegrationTest #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gen-cpp/ParentService.h" #include #include using apache::thrift::concurrency::Guard; using apache::thrift::concurrency::Monitor; using apache::thrift::concurrency::Mutex; using apache::thrift::concurrency::Synchronized; using apache::thrift::protocol::TBinaryProtocol; using apache::thrift::protocol::TBinaryProtocolFactory; using apache::thrift::protocol::TProtocol; using apache::thrift::protocol::TProtocolFactory; using apache::thrift::transport::TServerSocket; using apache::thrift::transport::TServerTransport; using apache::thrift::transport::TSocket; using apache::thrift::transport::TTransport; using apache::thrift::transport::TTransportException; using apache::thrift::transport::TTransportFactory; using apache::thrift::server::TServer; using apache::thrift::server::TServerEventHandler; using apache::thrift::server::TSimpleServer; using apache::thrift::server::TThreadPoolServer; using apache::thrift::server::TThreadedServer; using std::dynamic_pointer_cast; using std::make_shared; using std::shared_ptr; using apache::thrift::test::ParentServiceClient; using apache::thrift::test::ParentServiceIf; using apache::thrift::test::ParentServiceIfFactory; using apache::thrift::test::ParentServiceIfSingletonFactory; using apache::thrift::test::ParentServiceProcessor; using apache::thrift::test::ParentServiceProcessorFactory; using apache::thrift::TProcessor; using apache::thrift::TProcessorFactory; using boost::posix_time::milliseconds; /** * preServe runs after listen() is successful, when we can connect */ class TServerReadyEventHandler : public TServerEventHandler, public Monitor { public: TServerReadyEventHandler() : isListening_(false), accepted_(0) {} ~TServerReadyEventHandler() override = default; void preServe() override { Synchronized sync(*this); isListening_ = true; notify(); } void* createContext(shared_ptr input, shared_ptr output) override { Synchronized sync(*this); ++accepted_; notify(); (void)input; (void)output; return nullptr; } bool isListening() const { return isListening_; } uint64_t acceptedCount() const { return accepted_; } private: bool isListening_; uint64_t accepted_; }; /** * Reusing another generated test, just something to serve up */ class ParentHandler : public ParentServiceIf { public: ParentHandler() : generation_(0) {} int32_t incrementGeneration() override { Guard g(mutex_); return ++generation_; } int32_t getGeneration() override { Guard g(mutex_); return generation_; } void addString(const std::string& s) override { Guard g(mutex_); strings_.push_back(s); } void getStrings(std::vector& _return) override { Guard g(mutex_); _return = strings_; } void getDataWait(std::string& _return, const int32_t length) override { THRIFT_UNUSED_VARIABLE(_return); THRIFT_UNUSED_VARIABLE(length); } void onewayWait() override {} void exceptionWait(const std::string& message) override { THRIFT_UNUSED_VARIABLE(message); } void unexpectedExceptionWait(const std::string& message) override { THRIFT_UNUSED_VARIABLE(message); } protected: Mutex mutex_; int32_t generation_; std::vector strings_; }; void autoSocketCloser(TSocket* pSock) { pSock->close(); delete pSock; } template class TServerIntegrationTestFixture { public: TServerIntegrationTestFixture(const shared_ptr& _processorFactory) : pServer(new TServerType(_processorFactory, shared_ptr( new TServerSocket("localhost", 0)), shared_ptr(new TTransportFactory), shared_ptr(new TBinaryProtocolFactory))), pEventHandler(shared_ptr(new TServerReadyEventHandler)), bStressDone(false), bStressConnectionCount(0), bStressRequestCount(0) { pServer->setServerEventHandler(pEventHandler); } TServerIntegrationTestFixture(const shared_ptr& _processor) : pServer( new TServerType(_processor, shared_ptr(new TServerSocket("localhost", 0)), shared_ptr(new TTransportFactory), shared_ptr(new TBinaryProtocolFactory))), pEventHandler(shared_ptr(new TServerReadyEventHandler)), bStressDone(false), bStressConnectionCount(0), bStressRequestCount(0) { pServer->setServerEventHandler(pEventHandler); } void startServer() { pServerThread.reset(new boost::thread(std::bind(&TServerType::serve, pServer.get()))); // block until listen() completes so clients will be able to connect Synchronized sync(*(pEventHandler.get())); while (!pEventHandler->isListening()) { pEventHandler->wait(); } BOOST_TEST_MESSAGE(" server is listening"); } void blockUntilAccepted(uint64_t numAccepted) { Synchronized sync(*(pEventHandler.get())); while (pEventHandler->acceptedCount() < numAccepted) { pEventHandler->wait(); } BOOST_TEST_MESSAGE(boost::format(" server has accepted %1%") % numAccepted); } void stopServer() { if (pServerThread) { pServer->stop(); BOOST_TEST_MESSAGE(" server stop completed"); pServerThread->join(); BOOST_TEST_MESSAGE(" server thread joined"); pServerThread.reset(); } } ~TServerIntegrationTestFixture() { stopServer(); } /** * Performs a baseline test where some clients are opened and issue a single operation * and then disconnect at different intervals. * \param[in] numToMake the number of concurrent clients * \param[in] expectedHWM the high water mark we expect of concurrency * \param[in] purpose a description of the test for logging purposes */ void baseline(int64_t numToMake, int64_t expectedHWM, const std::string& purpose) { BOOST_TEST_MESSAGE(boost::format("Testing %1%: %2% with %3% clients, expect %4% HWM") % typeid(TServerType).name() % purpose % numToMake % expectedHWM); startServer(); std::vector > holdSockets; std::vector > holdThreads; for (int64_t i = 0; i < numToMake; ++i) { shared_ptr pClientSock(new TSocket("localhost", getServerPort()), autoSocketCloser); holdSockets.push_back(pClientSock); shared_ptr pClientProtocol(new TBinaryProtocol(pClientSock)); ParentServiceClient client(pClientProtocol); pClientSock->open(); client.incrementGeneration(); holdThreads.push_back(shared_ptr( new boost::thread(std::bind(&TServerIntegrationTestFixture::delayClose, this, pClientSock, milliseconds(10 * numToMake))))); } BOOST_CHECK_EQUAL(expectedHWM, pServer->getConcurrentClientCountHWM()); BOOST_FOREACH (shared_ptr pThread, holdThreads) { pThread->join(); } holdThreads.clear(); holdSockets.clear(); stopServer(); } /** * Helper method used to close a connection after a delay. * \param[in] toClose the connection to close * \param[in] after the delay to impose */ void delayClose(shared_ptr toClose, boost::posix_time::time_duration after) { boost::this_thread::sleep(after); toClose->close(); } /** * \returns the server port number */ int getServerPort() { auto* pSock = dynamic_cast(pServer->getServerTransport().get()); if (!pSock) { throw std::logic_error("how come?"); } return pSock->getPort(); } /** * Performs a stress test by spawning threads that connect, do a number of operations * and disconnect, then a random delay, then do it over again. This is done for a fixed * period of time to test for concurrency correctness. * \param[in] numToMake the number of concurrent clients */ void stress(int64_t numToMake, const boost::posix_time::time_duration& duration) { BOOST_TEST_MESSAGE(boost::format("Stress testing %1% with %2% clients for %3% seconds") % typeid(TServerType).name() % numToMake % duration.total_seconds()); startServer(); std::vector > holdThreads; for (int64_t i = 0; i < numToMake; ++i) { holdThreads.push_back(shared_ptr( new boost::thread(std::bind(&TServerIntegrationTestFixture::stressor, this)))); } boost::this_thread::sleep(duration); bStressDone = true; BOOST_TEST_MESSAGE(boost::format(" serviced %1% connections (HWM %2%) totaling %3% requests") % bStressConnectionCount % pServer->getConcurrentClientCountHWM() % bStressRequestCount); BOOST_FOREACH (shared_ptr pThread, holdThreads) { pThread->join(); } holdThreads.clear(); BOOST_CHECK(bStressRequestCount > 0); stopServer(); } /** * Helper method to stress the system */ void stressor() { while (!bStressDone) { shared_ptr pSocket(new TSocket("localhost", getServerPort()), autoSocketCloser); shared_ptr pProtocol(new TBinaryProtocol(pSocket)); ParentServiceClient client(pProtocol); pSocket->open(); bStressConnectionCount.fetch_add(1, std::memory_order_relaxed); for (int i = 0; i < rand() % 1000; ++i) { client.incrementGeneration(); bStressRequestCount.fetch_add(1, std::memory_order_relaxed); } } } shared_ptr pServer; shared_ptr pEventHandler; shared_ptr pServerThread; std::atomic bStressDone; std::atomic bStressConnectionCount; std::atomic bStressRequestCount; }; template class TServerIntegrationProcessorFactoryTestFixture : public TServerIntegrationTestFixture { public: TServerIntegrationProcessorFactoryTestFixture() : TServerIntegrationTestFixture(make_shared( make_shared( make_shared()))) {} }; template class TServerIntegrationProcessorTestFixture : public TServerIntegrationTestFixture { public: TServerIntegrationProcessorTestFixture() : TServerIntegrationTestFixture( make_shared(make_shared())) {} }; BOOST_AUTO_TEST_SUITE(constructors) BOOST_FIXTURE_TEST_CASE(test_simple_factory, TServerIntegrationProcessorFactoryTestFixture) { baseline(3, 1, "factory"); } BOOST_FIXTURE_TEST_CASE(test_simple, TServerIntegrationProcessorTestFixture) { baseline(3, 1, "processor"); } BOOST_FIXTURE_TEST_CASE(test_threaded_factory, TServerIntegrationProcessorFactoryTestFixture) { baseline(10, 10, "factory"); } BOOST_FIXTURE_TEST_CASE(test_threaded, TServerIntegrationProcessorTestFixture) { baseline(10, 10, "processor"); } BOOST_FIXTURE_TEST_CASE(test_threaded_bound, TServerIntegrationProcessorTestFixture) { pServer->setConcurrentClientLimit(4); baseline(10, 4, "limit by server framework"); } BOOST_FIXTURE_TEST_CASE(test_threaded_stress, TServerIntegrationProcessorFactoryTestFixture) { stress(10, boost::posix_time::seconds(3)); } BOOST_FIXTURE_TEST_CASE(test_threadpool_factory, TServerIntegrationProcessorFactoryTestFixture) { pServer->getThreadManager()->threadFactory( shared_ptr( new apache::thrift::concurrency::ThreadFactory)); pServer->getThreadManager()->start(); // thread factory has 4 threads as a default // thread factory however is a bad way to limit concurrent clients // as accept() will be called to grab a 5th client socket, in this case // and then the thread factory will block adding the thread to manage // that client. baseline(10, 5, "limit by thread manager"); } BOOST_FIXTURE_TEST_CASE(test_threadpool, TServerIntegrationProcessorTestFixture) { pServer->getThreadManager()->threadFactory( shared_ptr( new apache::thrift::concurrency::ThreadFactory)); pServer->getThreadManager()->start(); // thread factory has 4 threads as a default // thread factory however is a bad way to limit concurrent clients // as accept() will be called to grab a 5th client socket, in this case // and then the thread factory will block adding the thread to manage // that client. baseline(10, 5, "limit by thread manager"); } BOOST_FIXTURE_TEST_CASE(test_threadpool_bound, TServerIntegrationProcessorTestFixture) { pServer->getThreadManager()->threadFactory( shared_ptr( new apache::thrift::concurrency::ThreadFactory)); pServer->getThreadManager()->start(); pServer->setConcurrentClientLimit(4); baseline(10, 4, "server framework connection limit"); } BOOST_FIXTURE_TEST_CASE(test_threadpool_stress, TServerIntegrationProcessorTestFixture) { pServer->getThreadManager()->threadFactory( shared_ptr( new apache::thrift::concurrency::ThreadFactory)); pServer->getThreadManager()->start(); stress(10, boost::posix_time::seconds(3)); } BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(TServerIntegrationTest, TServerIntegrationProcessorTestFixture) BOOST_AUTO_TEST_CASE(test_stop_with_interruptable_clients_connected) { // This tests THRIFT-2441 new behavior: stopping the server disconnects clients BOOST_TEST_MESSAGE("Testing stop with interruptable clients"); startServer(); shared_ptr pClientSock1(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock1->open(); shared_ptr pClientSock2(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock2->open(); // Ensure they have been accepted blockUntilAccepted(2); // The test fixture destructor will force the sockets to disconnect // Prior to THRIFT-2441, pServer->stop() would hang until clients disconnected stopServer(); // extra proof the server end disconnected the clients uint8_t buf[1]; BOOST_CHECK_EQUAL(0, pClientSock1->read(&buf[0], 1)); // 0 = disconnected BOOST_CHECK_EQUAL(0, pClientSock2->read(&buf[0], 1)); // 0 = disconnected } BOOST_AUTO_TEST_CASE(test_stop_with_uninterruptable_clients_connected) { // This tests pre-THRIFT-2441 behavior: stopping the server blocks until clients // disconnect. BOOST_TEST_MESSAGE("Testing stop with uninterruptable clients"); dynamic_pointer_cast(pServer->getServerTransport()) ->setInterruptableChildren(false); // returns to pre-THRIFT-2441 behavior startServer(); shared_ptr pClientSock1(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock1->open(); shared_ptr pClientSock2(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock2->open(); // Ensure they have been accepted blockUntilAccepted(2); boost::thread t1(std::bind(&TServerIntegrationTestFixture::delayClose, this, pClientSock1, milliseconds(250))); boost::thread t2(std::bind(&TServerIntegrationTestFixture::delayClose, this, pClientSock2, milliseconds(250))); // Once the clients disconnect the server will stop stopServer(); BOOST_CHECK(pServer->getConcurrentClientCountHWM() > 0); t1.join(); t2.join(); } BOOST_AUTO_TEST_CASE(test_concurrent_client_limit) { startServer(); BOOST_TEST_MESSAGE("Testing the concurrent client limit"); BOOST_CHECK_EQUAL(INT64_MAX, pServer->getConcurrentClientLimit()); pServer->setConcurrentClientLimit(2); BOOST_CHECK_EQUAL(0, pServer->getConcurrentClientCount()); BOOST_CHECK_EQUAL(2, pServer->getConcurrentClientLimit()); shared_ptr pClientSock1(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock1->open(); blockUntilAccepted(1); BOOST_CHECK_EQUAL(1, pServer->getConcurrentClientCount()); shared_ptr pClientSock2(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock2->open(); blockUntilAccepted(2); BOOST_CHECK_EQUAL(2, pServer->getConcurrentClientCount()); // a third client cannot connect until one of the other two closes boost::thread t2(std::bind(&TServerIntegrationTestFixture::delayClose, this, pClientSock2, milliseconds(250))); shared_ptr pClientSock3(new TSocket("localhost", getServerPort()), autoSocketCloser); pClientSock2->open(); blockUntilAccepted(2); BOOST_CHECK_EQUAL(2, pServer->getConcurrentClientCount()); BOOST_CHECK_EQUAL(2, pServer->getConcurrentClientCountHWM()); stopServer(); BOOST_CHECK(pServer->getConcurrentClientCountHWM() > 0); t2.join(); } BOOST_AUTO_TEST_SUITE_END() // A handler whose incrementGeneration() parks the worker until the test releases // it, so the single-worker pool can be driven into saturation on demand. class BlockingParentHandler : public ParentServiceIf { public: BlockingParentHandler() : entered_(0), released_(false) {} int32_t incrementGeneration() override { { Synchronized s(enteredMon_); ++entered_; enteredMon_.notifyAll(); } { Synchronized s(releaseMon_); while (!released_) { releaseMon_.waitForTimeRelative(200); } } return 1; } int32_t getGeneration() override { return 0; } void addString(const std::string&) override {} void getStrings(std::vector& _return) override { _return.clear(); } void getDataWait(std::string& _return, const int32_t) override { THRIFT_UNUSED_VARIABLE(_return); } void onewayWait() override {} void exceptionWait(const std::string&) override {} void unexpectedExceptionWait(const std::string&) override {} // Blocks until at least n calls have entered the handler, or maxWaits lapse. bool awaitEntered(int n, int maxWaits) { Synchronized s(enteredMon_); int waits = 0; while (entered_ < n) { if (waits++ >= maxWaits) { return false; } enteredMon_.waitForTimeRelative(100); } return true; } void release() { Synchronized s(releaseMon_); released_ = true; releaseMon_.notifyAll(); } private: Monitor enteredMon_; Monitor releaseMon_; int entered_; bool released_; }; // When a TThreadPoolServer's ThreadManager::add() rejects a dispatch -- a full // pending queue with a non-blocking timeout gives TooManyPendingTasksException, // a positive timeout that lapses gives TimedOutException, both derived from // TException rather than TTransportException -- the exception used to escape // serve() and stop the whole server. serve() now drops just that one client and // keeps accepting, like the Java TThreadPoolServer, which closes the latest // connection on a RejectedExecutionException and continues. // // Run serve() under a wrapper that records an escaping exception rather than // letting it reach std::terminate, so the two states are: serve() ended (before // the fix) versus serve() still accepting (after it). static void run_dispatch_rejection_case(int64_t dispatchTimeout) { auto handler = make_shared(); auto processor = make_shared(handler); auto tm = apache::thrift::concurrency::ThreadManager::newSimpleThreadManager(1, 1); tm->threadFactory(make_shared()); tm->start(); auto serverSocket = make_shared("localhost", 0); auto server = make_shared(processor, serverSocket, make_shared(), make_shared(), tm); server->setTimeout(dispatchTimeout); auto ready = make_shared(); server->setServerEventHandler(ready); std::atomic serveThrew(false); boost::thread serveThread([&server, &serveThrew]() { try { server->serve(); } catch (...) { serveThrew = true; } }); { Synchronized sync(*ready); int waits = 0; while (!ready->isListening() && waits++ < 200) { ready->waitForTimeRelative(50); } } BOOST_REQUIRE(ready->isListening()); int port = serverSocket->getPort(); BOOST_REQUIRE_GT(port, 0); auto connect = [port]() { auto sock = make_shared("localhost", port); sock->setConnTimeout(2000); sock->setSendTimeout(2000); sock->setRecvTimeout(2000); sock->open(); return sock; }; // Client A takes the one worker; its call parks in the handler. auto sockA = connect(); { ParentServiceClient clientA(make_shared(sockA)); clientA.send_incrementGeneration(); } BOOST_REQUIRE_MESSAGE(handler->awaitEntered(1, 50), "the single worker never picked up the first client"); // Fire more clients than the busy worker plus the one pending slot can hold. // serve() accepts each and tries to dispatch it; the ones past the queue are // rejected by ThreadManager::add(). std::vector > fired; int droppedWhileSending = 0; for (int i = 0; i < 6; ++i) { try { auto s = connect(); try { ParentServiceClient c(make_shared(s)); c.send_incrementGeneration(); fired.push_back(s); } catch (const TTransportException& e) { // serve() can reject and drop a client before its request is fully // written. Windows then fails the send with an aborted connection, // so that client never gets to read end-of-stream below. It is a drop // all the same; a send that merely timed out is not. if (e.getType() != TTransportException::TIMED_OUT) { ++droppedWhileSending; } } } catch (...) { // Once serve() is gone (before the fix) a later connect/send may fail. } } // Give serve() time to accept and attempt to dispatch all of them. A positive // dispatch timeout makes each rejection wait that long, so allow for several. boost::this_thread::sleep(milliseconds(1500)); // Two-state assertion: before the fix a rejected dispatch escaped serve(). BOOST_CHECK_MESSAGE(!serveThrew, "a rejected dispatch ended the server"); // Non-vacuous: a rejected client is closed by the server, so at least one of // the fired clients failed to send or reads end-of-stream. Only true if a // rejection happened. if (!serveThrew) { int closed = droppedWhileSending; for (const auto& s : fired) { try { uint8_t b[1]; if (s->read(b, 1) == 0) { ++closed; // clean end-of-stream } } catch (const TTransportException& e) { // The server drops the connection with the request still unread, which // some platforms report to the client as a reset rather than a clean // EOF. Either is a drop. Only the one still-queued client that is simply // waiting (a receive timeout) is not. if (e.getType() != TTransportException::TIMED_OUT) { ++closed; } } } BOOST_CHECK_MESSAGE(closed > 0, "no client was dropped; the pool was not driven into rejection"); } // Release the parked worker and drop the saturation connections. A connected // client is served in a loop, so the single worker would otherwise stay parked // on client A's idle keep-alive socket; closing them lets it drain and become // free for a fresh client. handler->release(); for (const auto& s : fired) { try { s->close(); } catch (...) {} } try { sockA->close(); } catch (...) {} // The server must now serve a fresh client. if (!serveThrew) { bool served = false; for (int attempt = 0; attempt < 40 && !served; ++attempt) { try { auto z = connect(); ParentServiceClient cz(make_shared(z)); served = (cz.incrementGeneration() != 0); z->close(); } catch (...) { boost::this_thread::sleep(milliseconds(50)); } } BOOST_CHECK_MESSAGE(served, "the server did not serve a fresh client after the rejection"); } // Teardown: stop the server, join, then stop the pool. server->stop(); serveThread.join(); tm->stop(); // The concurrent-client count must settle to exactly zero. A negative value // would mean a dropped client was counted down twice -- its deleter already // adjusts the count when the rejected client goes out of scope in serve(). int64_t count = -1; for (int i = 0; i < 300; ++i) { count = server->getConcurrentClientCount(); if (count == 0) { break; } boost::this_thread::sleep(milliseconds(10)); } BOOST_CHECK_EQUAL(count, 0); } BOOST_AUTO_TEST_SUITE(dispatch_rejection) // Non-blocking dispatch (timeout -1): a full pending queue is rejected at once // with TooManyPendingTasksException. BOOST_AUTO_TEST_CASE(too_many_pending_tasks_does_not_end_the_server) { run_dispatch_rejection_case(-1); } // Positive dispatch timeout: a pending queue that stays full past the timeout is // rejected with TimedOutException. BOOST_AUTO_TEST_CASE(dispatch_timeout_does_not_end_the_server) { run_dispatch_rejection_case(100); } BOOST_AUTO_TEST_SUITE_END() // --------------------------------------------------------------------------- // A worker thread that cannot be started must not stop the whole server. // // TThreadedServer::onClientConnected() starts a worker thread for each accepted // client. If the thread cannot be started (for example the process is out of // threads) std::thread's constructor throws. That exception is not a // TTransportException, so the accept loop's only catch does not handle it: it // unwinds out of serve() and the server stops accepting. The fix catches the // failure, drops the one client, and keeps serving. // // A real std::thread cannot be made to fail on demand, so a thread whose // start() always throws is injected through the ThreadFactory that the // TThreadedServer constructor accepts. // --------------------------------------------------------------------------- #include #include #include #include #include using apache::thrift::concurrency::Runnable; using apache::thrift::concurrency::SystemResourceException; using apache::thrift::concurrency::Thread; using apache::thrift::concurrency::ThreadFactory; // A Thread whose start() always fails, simulating the OS refusing to create // another worker thread. It is created non-detached, matching TThreadedServer's // default ThreadFactory(false). Destroying this never-started thread is safe only // because of the null-guard added to ~Thread()/join() in Thread.h (part of this // fix): without it, ~Thread dereferences the null thread_ and crashes. class FailingThread : public Thread { public: FailingThread(bool detached, shared_ptr runnable, shared_ptr > startAttempts) : Thread(detached, runnable), startAttempts_(startAttempts) {} void start() override { ++(*startAttempts_); throw SystemResourceException("cannot create worker thread"); } private: shared_ptr > startAttempts_; }; // A ThreadFactory that hands out FailingThreads and counts how many times a // worker-thread start was attempted, so the test can prove the failing path was // actually exercised. class FailingThreadFactory : public ThreadFactory { public: explicit FailingThreadFactory(shared_ptr > startAttempts) : ThreadFactory(false), startAttempts_(startAttempts) {} shared_ptr newThread(shared_ptr runnable) const override { return std::make_shared(isDetached(), runnable, startAttempts_); } private: shared_ptr > startAttempts_; }; BOOST_AUTO_TEST_SUITE(ThreadStartFailure) BOOST_AUTO_TEST_CASE(test_threaded_server_thread_start_failure) { BOOST_TEST_MESSAGE("Testing that a failed worker-thread start does not stop the server"); shared_ptr > startAttempts = std::make_shared >(0); shared_ptr processor = make_shared(make_shared()); shared_ptr serverSocket = make_shared("localhost", 0); shared_ptr readyHandler = make_shared(); // Allocated with new, not on the stack: on the (unfixed) regression path // serve() unwinds and leaves a client in activeClientMap_ that can never be // drained, and running ~TThreadedServer would re-enter the server's own maps // while they are being destroyed (a use-after-free). The server is therefore // deleted only when serve() returned normally (the fixed path); the shipped // code always takes that branch, so this test does not leak in CI. auto* server = new TThreadedServer(processor, serverSocket, make_shared(), make_shared(), make_shared(startAttempts)); server->setServerEventHandler(readyHandler); std::atomic serveEnded(false); std::atomic serveThrew(false); std::string serveError; std::thread serveThread([&]() { try { server->serve(); } catch (const std::exception& ex) { serveThrew = true; serveError = ex.what(); } catch (...) { serveThrew = true; serveError = "unknown exception"; } serveEnded = true; }); // Wait (bounded) until the server is listening so a client can connect. bool listening = false; for (int i = 0; i < 200 && !listening && !serveEnded; ++i) { { Synchronized sync(*readyHandler); listening = readyHandler->isListening(); } if (!listening) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } if (listening) { int port = serverSocket->getPort(); // First client: drives accept() -> onClientConnected(), whose attempt to // start a worker thread throws. Wait until that attempt is observed (it // happens in both the fixed and unfixed builds) or serve() ends. // // Connecting is not enough: TServerSocket::listen() sets TCP_DEFER_ACCEPT, // so accept() does not wake until the client sends data. Send one byte so // the server accepts promptly; it is never read, because the worker thread // that would process it never starts. shared_ptr clientSock1 = make_shared("localhost", port); try { clientSock1->open(); uint8_t wake = 0; clientSock1->write(&wake, 1); } catch (const std::exception&) {} for (int i = 0; i < 200 && startAttempts->load() < 1 && !serveEnded; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } // Second client: in the fixed build the accept loop is still running and // processes it (a second start attempt); in the unfixed build serve() has // already unwound, so although the listening socket is still open and the // connect succeeds, nothing accepts it and no second attempt happens. shared_ptr clientSock2 = make_shared("localhost", port); try { clientSock2->open(); uint8_t wake = 0; clientSock2->write(&wake, 1); } catch (const std::exception&) {} for (int i = 0; i < 200 && startAttempts->load() < 2 && !serveEnded; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } try { clientSock1->close(); } catch (...) {} try { clientSock2->close(); } catch (...) {} } // Snapshot the observations before stopping the server: stop() makes serve() // return in the fixed build, which would otherwise erase the distinction. bool stillServing = !serveEnded.load(); int attempts = startAttempts->load(); // Settle the serve thread before asserting, so serveError is safe to read and // a failing assertion cannot leave a running thread behind to std::terminate. try { server->stop(); } catch (...) {} serveThread.join(); bool threw = serveThrew.load(); std::string err = serveError; BOOST_REQUIRE_MESSAGE(listening, "server never reached the listening state"); // The failing start path must have run at least once, otherwise the test // proves nothing (the connection never reached onClientConnected). BOOST_CHECK_MESSAGE(attempts >= 1, "no worker-thread start was ever attempted; the client did not reach onClientConnected"); // FIXED behavior, which FAILS against the unfixed code: // - serve() is still running after the failed start (serveEnded == false), and // - the server kept accepting, so a second start was attempted. BOOST_CHECK_MESSAGE(stillServing, "serve() stopped after a worker thread failed to start (threw=" << threw << ", error=\"" << err << "\"); the server should drop the client and keep serving"); BOOST_CHECK_MESSAGE(attempts >= 2, "server stopped accepting after a failed worker-thread start; start attempts observed = " << attempts << " (expected >= 2)"); if (!threw) { delete server; } // else: regression path, server deliberately leaked (see note above). } BOOST_AUTO_TEST_SUITE_END()