-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Hi!
Thanks for the great library!
I think there is a race condition when the process is terminated explicitly (for example with std::exit
) while a server is processing queries and/or a client is executing some.
I crafted a small reproduction below (note that in the actual code we're not "sleeping" in the atexit
but have the same issue nonetheless):
#include "httplib.h"
struct ServerWrapper {
httplib::Server server;
std::unique_ptr<std::thread> s_thread;
};
static std::unique_ptr<ServerWrapper> s_wrapper;
void mainServer() {
s_wrapper->server.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
res.set_content("Quack", "text/plain");
});
s_wrapper->server.listen("localhost", 8080);
}
void exitThread() {
std::cout << "Will exit soon..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Calling std::exit now." << std::endl;
std::exit(0);
}
void runClient() {
httplib::Client cli("localhost", 8080);
while (true) {
auto res = cli.Get("/hi");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void atex() {
std::cout << "Entering std::atexit callback, will wait a bit and stop server" << std::endl;
// Wait a bit before stopping server to simulate delayed exit
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "[std::atexit] Waited, will stop server now." << std::endl;
s_wrapper->server.stop();
std::cout << "[std::atexit] Server stopped." << std::endl;
s_wrapper->s_thread->join();
std::cout << "[std::atexit] Server thread completed." << std::endl;
}
int main() {
std::atexit(atex);
s_wrapper = std::make_unique<ServerWrapper>();
s_wrapper->s_thread = std::make_unique<std::thread>(&mainServer);
std::thread cliThread(&runClient);
std::thread(&exitThread).join();
return 0;
}
Can be compiled with:
$ g++ -g -O0 -std=c++20 test.cc
Then execution looks like:
$ ./a.out
Will exit soon...
Calling std::exit now.
Entering std::atexit callback, will wait a bit and stop server
[1] 37193 segmentation fault ./a.out
(MacOS 15.3.1; g++: Apple clang version 16.0.0 (clang-1600.0.26.6)
)
The issue (if I understand it correctly) is that the static destructors are executed before the server is actually stopped. So it is possible for example that a server requests arrives after the static methods
object is destroyed, but still attempts to use it.
An easy fix would be to remove all static initializations (for example removing both this one and that one "fixes" the example above, but of course we could craft a different one to make it crash with another static resource).
But that is probably not acceptable for performance reasons.
The C++ standard guarantees that functions registered with std::atexit
are called in the reverse order of their registration, except that a function is called after any previously registered functions that had already been called at the time it was registered.
As a consumer of the library, this makes it quite cumbersome to make sure that we define the atexit
handler after each static initialization (to make sure it is called first, and has a chance to destroy the server before other static destructors)
Another way would be to hoist all static definition at the top of the file, so that caller of std::atexit
would be guaranteed to have their handler run first.
Of course there are more involved way to fix the problem.
I am happy to contribute to a fix when we settle on the right approach :-)