ThreadPool: optional limit for jobs queue (#1741)

For very busy servers, the internal jobs queue where accepted
sockets are enqueued can grow without limit.
This is a problem for two reasons:
 - queueing too much work causes the server to respond with huge latency,
   resulting in repetead timeouts on the clients; it is definitely
   better to reject the connection early, so that the client
   receives the backpressure signal as soon as the queue is
   becoming too large
 - the jobs list can eventually cause an out of memory condition
This commit is contained in:
vmaffione 2023-12-24 14:20:22 +01:00 committed by GitHub
parent 31cdcc3c3a
commit 374d058de7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 10 deletions

View file

@ -433,6 +433,17 @@ If you want to set the thread count at runtime, there is no convenient way... Bu
svr.new_task_queue = [] { return new ThreadPool(12); }; svr.new_task_queue = [] { return new ThreadPool(12); };
``` ```
You can also provide an optional parameter to limit the maximum number
of pending requests, i.e. requests `accept()`ed by the listener but
still waiting to be serviced by worker threads.
```cpp
svr.new_task_queue = [] { return new ThreadPool(/*num_threads=*/12, /*max_queued_requests=*/18); };
```
Default limit is 0 (unlimited). Once the limit is reached, the listener
will shutdown the client connection.
### Override the default thread pool with yours ### Override the default thread pool with yours
You can supply your own thread pool implementation according to your need. You can supply your own thread pool implementation according to your need.
@ -444,8 +455,10 @@ public:
pool_.start_with_thread_count(n); pool_.start_with_thread_count(n);
} }
virtual void enqueue(std::function<void()> fn) override { virtual bool enqueue(std::function<void()> fn) override {
pool_.enqueue(fn); /* Return true if the task was actually enqueued, or false
* if the caller must drop the corresponding connection. */
return pool_.enqueue(fn);
} }
virtual void shutdown() override { virtual void shutdown() override {

View file

@ -653,7 +653,7 @@ public:
TaskQueue() = default; TaskQueue() = default;
virtual ~TaskQueue() = default; virtual ~TaskQueue() = default;
virtual void enqueue(std::function<void()> fn) = 0; virtual bool enqueue(std::function<void()> fn) = 0;
virtual void shutdown() = 0; virtual void shutdown() = 0;
virtual void on_idle() {} virtual void on_idle() {}
@ -661,7 +661,8 @@ public:
class ThreadPool : public TaskQueue { class ThreadPool : public TaskQueue {
public: public:
explicit ThreadPool(size_t n) : shutdown_(false) { explicit ThreadPool(size_t n, size_t mqr = 0)
: shutdown_(false), max_queued_requests_(mqr) {
while (n) { while (n) {
threads_.emplace_back(worker(*this)); threads_.emplace_back(worker(*this));
n--; n--;
@ -671,13 +672,17 @@ public:
ThreadPool(const ThreadPool &) = delete; ThreadPool(const ThreadPool &) = delete;
~ThreadPool() override = default; ~ThreadPool() override = default;
void enqueue(std::function<void()> fn) override { bool enqueue(std::function<void()> fn) override {
{ {
std::unique_lock<std::mutex> lock(mutex_); std::unique_lock<std::mutex> lock(mutex_);
if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) {
return false;
}
jobs_.push_back(std::move(fn)); jobs_.push_back(std::move(fn));
} }
cond_.notify_one(); cond_.notify_one();
return true;
} }
void shutdown() override { void shutdown() override {
@ -727,6 +732,7 @@ private:
std::list<std::function<void()>> jobs_; std::list<std::function<void()>> jobs_;
bool shutdown_; bool shutdown_;
size_t max_queued_requests_ = 0;
std::condition_variable cond_; std::condition_variable cond_;
std::mutex mutex_; std::mutex mutex_;
@ -6319,7 +6325,11 @@ inline bool Server::listen_internal() {
#endif #endif
} }
task_queue->enqueue([this, sock]() { process_and_close_socket(sock); }); if (!task_queue->enqueue(
[this, sock]() { process_and_close_socket(sock); })) {
detail::shutdown_socket(sock);
detail::close_socket(sock);
}
} }
task_queue->shutdown(); task_queue->shutdown();

View file

@ -6511,18 +6511,103 @@ TEST(SocketStream, is_writable_INET) {
#endif // #ifndef _WIN32 #endif // #ifndef _WIN32
TEST(TaskQueueTest, IncreaseAtomicInteger) { TEST(TaskQueueTest, IncreaseAtomicInteger) {
static constexpr unsigned int number_of_task{1000000}; static constexpr unsigned int number_of_tasks{1000000};
std::atomic_uint count{0}; std::atomic_uint count{0};
std::unique_ptr<TaskQueue> task_queue{ std::unique_ptr<TaskQueue> task_queue{
new ThreadPool{CPPHTTPLIB_THREAD_POOL_COUNT}}; new ThreadPool{CPPHTTPLIB_THREAD_POOL_COUNT}};
for (unsigned int i = 0; i < number_of_task; ++i) { for (unsigned int i = 0; i < number_of_tasks; ++i) {
task_queue->enqueue( auto queued = task_queue->enqueue(
[&count] { count.fetch_add(1, std::memory_order_relaxed); }); [&count] { count.fetch_add(1, std::memory_order_relaxed); });
EXPECT_TRUE(queued);
}
EXPECT_NO_THROW(task_queue->shutdown());
EXPECT_EQ(number_of_tasks, count.load());
}
TEST(TaskQueueTest, IncreaseAtomicIntegerWithQueueLimit) {
static constexpr unsigned int number_of_tasks{1000000};
static constexpr unsigned int qlimit{2};
unsigned int queued_count{0};
std::atomic_uint count{0};
std::unique_ptr<TaskQueue> task_queue{
new ThreadPool{/*num_threads=*/1, qlimit}};
for (unsigned int i = 0; i < number_of_tasks; ++i) {
if (task_queue->enqueue(
[&count] { count.fetch_add(1, std::memory_order_relaxed); })) {
queued_count++;
}
}
EXPECT_NO_THROW(task_queue->shutdown());
EXPECT_EQ(queued_count, count.load());
EXPECT_TRUE(queued_count <= number_of_tasks);
EXPECT_TRUE(queued_count >= qlimit);
}
TEST(TaskQueueTest, MaxQueuedRequests) {
static constexpr unsigned int qlimit{3};
std::unique_ptr<TaskQueue> task_queue{new ThreadPool{1, qlimit}};
std::condition_variable sem_cv;
std::mutex sem_mtx;
int credits = 0;
bool queued;
/* Fill up the queue with tasks that will block until we give them credits to
* complete. */
for (unsigned int n = 0; n <= qlimit;) {
queued = task_queue->enqueue([&sem_mtx, &sem_cv, &credits] {
std::unique_lock<std::mutex> lock(sem_mtx);
while (credits <= 0) {
sem_cv.wait(lock);
}
/* Consume the credit and signal the test code if they are all gone. */
if (--credits == 0) { sem_cv.notify_one(); }
});
if (n < qlimit) {
/* The first qlimit enqueues must succeed. */
EXPECT_TRUE(queued);
} else {
/* The last one will succeed only when the worker thread
* starts and dequeues the first blocking task. Although
* not necessary for the correctness of this test, we sleep for
* a short while to avoid busy waiting. */
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (queued) { n++; }
}
/* Further enqueues must fail since the queue is full. */
for (auto i = 0; i < 4; i++) {
queued = task_queue->enqueue([] {});
EXPECT_FALSE(queued);
}
/* Give the credits to allow the previous tasks to complete. */
{
std::unique_lock<std::mutex> lock(sem_mtx);
credits += qlimit + 1;
}
sem_cv.notify_all();
/* Wait for all the credits to be consumed. */
{
std::unique_lock<std::mutex> lock(sem_mtx);
while (credits > 0) {
sem_cv.wait(lock);
}
}
/* Check that we are able again to enqueue at least qlimit tasks. */
for (unsigned int i = 0; i < qlimit; i++) {
queued = task_queue->enqueue([] {});
EXPECT_TRUE(queued);
} }
EXPECT_NO_THROW(task_queue->shutdown()); EXPECT_NO_THROW(task_queue->shutdown());
EXPECT_EQ(number_of_task, count.load());
} }
TEST(RedirectTest, RedirectToUrlWithQueryParameters) { TEST(RedirectTest, RedirectToUrlWithQueryParameters) {