Skip to content

Commit df1c0de

Browse files
committed
test: clean up gRPC server process groups
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
1 parent 2ca56e7 commit df1c0de

1 file changed

Lines changed: 96 additions & 38 deletions

File tree

cpp/tests/linear_programming/grpc/grpc_integration_test.cpp

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <gtest/gtest.h>
3030

3131
#include <atomic>
32+
#include <cerrno>
3233
#include <cmath>
3334
#include <filesystem>
3435
#include <fstream>
@@ -71,14 +72,22 @@ using cuopt::mathematical_optimization::testing::GrpcTestLogCapture;
7172

7273
namespace {
7374

74-
// =============================================================================
75-
// Server Process Manager
76-
// =============================================================================
75+
// GRPC_INTEGRATION_TEST
76+
// `-- cuopt_grpc_server parent (pid_, leads process group pgid_ == pid_)
77+
// `-- GPU worker (inherits pgid_)
78+
//
79+
// The server is forked into its own process group so stop() can signal the whole
80+
// group and guarantee the GPU worker child is never left orphaned.
7781

7882
class ServerProcess {
7983
public:
80-
ServerProcess() : pid_(-1), port_(0) {}
81-
~ServerProcess() { stop(); }
84+
ServerProcess() : pid_(-1), pgid_(-1), port_(0) {}
85+
ServerProcess(const ServerProcess&) = delete;
86+
ServerProcess& operator=(const ServerProcess&) = delete;
87+
~ServerProcess()
88+
{
89+
if (!stop()) { std::cerr << "Failed to clean up test server process group\n"; }
90+
}
8291

8392
void set_tls_config(const std::string& root_certs,
8493
const std::string& client_cert = "",
@@ -91,21 +100,27 @@ class ServerProcess {
91100

92101
bool start(int port, const std::vector<std::string>& extra_args = {})
93102
{
94-
port_ = port;
103+
if (pid_ > 0) {
104+
std::cerr << "Cannot reuse a ServerProcess while it still owns a process lifecycle\n";
105+
return false;
106+
}
95107

96108
std::string server_path = find_server_binary();
97109
if (server_path.empty()) {
98110
std::cerr << "Could not find cuopt_grpc_server binary\n";
99111
return false;
100112
}
101113

102-
pid_ = fork();
114+
port_ = port;
115+
pid_ = fork();
103116
if (pid_ < 0) {
104117
std::cerr << "fork() failed\n";
105118
return false;
106119
}
107120

108121
if (pid_ == 0) {
122+
setpgid(0, 0); // child leads its own group; parent mirrors this below
123+
109124
std::vector<const char*> args;
110125
args.push_back(server_path.c_str());
111126
args.push_back("--port");
@@ -131,30 +146,38 @@ class ServerProcess {
131146
_exit(127);
132147
}
133148

134-
return wait_for_ready(15000);
149+
// Mirror the child's setpgid() so the group is established regardless of which
150+
// process runs first; the child is a fresh fork, so its group id is its pid.
151+
setpgid(pid_, pid_);
152+
pgid_ = pid_;
153+
154+
if (!wait_for_ready(15000)) {
155+
if (!stop()) { std::cerr << "Failed to clean up server after readiness failure\n"; }
156+
return false;
157+
}
158+
159+
return true;
135160
}
136161

137-
void stop()
162+
bool stop()
138163
{
139-
if (pid_ > 0) {
140-
kill(pid_, SIGTERM);
141-
142-
int status;
143-
int wait_ms = 0;
144-
while (wait_ms < 5000) {
145-
int ret = waitpid(pid_, &status, WNOHANG);
146-
if (ret != 0) break;
147-
std::this_thread::sleep_for(std::chrono::milliseconds(100));
148-
wait_ms += 100;
149-
}
164+
if (pid_ <= 0) return true;
165+
166+
// Ask the whole group (server + GPU worker) to exit, escalating to SIGKILL if
167+
// the server does not honor SIGTERM within the timeout.
168+
kill(-pgid_, SIGTERM);
169+
if (!reap_parent(std::chrono::seconds(5))) {
170+
kill(-pgid_, SIGKILL);
171+
int status = 0;
172+
while (waitpid(pid_, &status, 0) < 0 && errno == EINTR) {}
173+
}
150174

151-
if (waitpid(pid_, &status, WNOHANG) == 0) {
152-
kill(pid_, SIGKILL);
153-
waitpid(pid_, &status, 0);
154-
}
175+
// With the server reaped, the group is gone once the GPU worker exits too.
176+
bool group_exited = wait_for_group_exit(std::chrono::seconds(5));
177+
if (!group_exited) { std::cerr << "Server process group " << pgid_ << " did not exit\n"; }
155178

156-
pid_ = -1;
157-
}
179+
clear_lifecycle_state();
180+
return group_exited;
158181
}
159182

160183
int port() const { return port_; }
@@ -172,6 +195,40 @@ class ServerProcess {
172195
}
173196

174197
private:
198+
void clear_lifecycle_state()
199+
{
200+
pid_ = -1;
201+
pgid_ = -1;
202+
port_ = 0;
203+
}
204+
205+
// Reap the server parent, returning true once it has exited (within timeout).
206+
bool reap_parent(std::chrono::milliseconds timeout)
207+
{
208+
auto deadline = std::chrono::steady_clock::now() + timeout;
209+
while (true) {
210+
int status = 0;
211+
pid_t ret = waitpid(pid_, &status, WNOHANG);
212+
if (ret == pid_) return true;
213+
if (ret < 0 && errno == ECHILD) return true; // already reaped elsewhere
214+
if (ret < 0 && errno == EINTR) continue;
215+
if (std::chrono::steady_clock::now() >= deadline) return false;
216+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
217+
}
218+
}
219+
220+
// True once no process remains in the group. The parent must already be reaped,
221+
// otherwise its zombie keeps the group id alive.
222+
bool wait_for_group_exit(std::chrono::milliseconds timeout) const
223+
{
224+
auto deadline = std::chrono::steady_clock::now() + timeout;
225+
while (true) {
226+
if (kill(-pgid_, 0) != 0 && errno == ESRCH) return true;
227+
if (std::chrono::steady_clock::now() >= deadline) return false;
228+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
229+
}
230+
}
231+
175232
std::string find_in_path(const std::string& name)
176233
{
177234
const char* path_env = std::getenv("PATH");
@@ -246,8 +303,8 @@ class ServerProcess {
246303
grpc_client_t client(config);
247304
if (client.connect()) { return true; }
248305

249-
int status;
250-
if (waitpid(pid_, &status, WNOHANG) != 0) {
306+
int status = 0;
307+
if (waitpid(pid_, &status, WNOHANG) == pid_) {
251308
std::cerr << "Server process died during startup\n";
252309
return false;
253310
}
@@ -257,6 +314,7 @@ class ServerProcess {
257314
}
258315

259316
pid_t pid_;
317+
pid_t pgid_;
260318
int port_;
261319
std::string tls_root_certs_;
262320
std::string tls_client_cert_;
@@ -516,7 +574,7 @@ class DefaultServerTests : public GrpcIntegrationTestBase {
516574

517575
static void TearDownTestSuite()
518576
{
519-
if (s_server_) s_server_->stop();
577+
if (s_server_) EXPECT_TRUE(s_server_->stop());
520578
s_server_.reset();
521579
}
522580

@@ -1083,7 +1141,7 @@ class ChunkedUploadTests : public GrpcIntegrationTestBase {
10831141

10841142
static void TearDownTestSuite()
10851143
{
1086-
if (s_server_) s_server_->stop();
1144+
if (s_server_) EXPECT_TRUE(s_server_->stop());
10871145
s_server_.reset();
10881146
}
10891147

@@ -1399,7 +1457,7 @@ class PathSelectionTests : public GrpcIntegrationTestBase {
13991457

14001458
static void TearDownTestSuite()
14011459
{
1402-
if (s_server_) s_server_->stop();
1460+
if (s_server_) EXPECT_TRUE(s_server_->stop());
14031461
s_server_.reset();
14041462
}
14051463

@@ -1583,7 +1641,7 @@ TEST_F(PathSelectionTests, UnaryUploadMIPWithPathLogging)
15831641
class ErrorRecoveryTests : public GrpcIntegrationTestBase {
15841642
protected:
15851643
void SetUp() override { port_ = get_test_port(); }
1586-
void TearDown() override { server_.stop(); }
1644+
void TearDown() override { EXPECT_TRUE(server_.stop()); }
15871645

15881646
bool start_server(const std::vector<std::string>& extra_args = {})
15891647
{
@@ -1602,7 +1660,7 @@ TEST_F(ErrorRecoveryTests, ClientReconnectsAfterServerRestart)
16021660
auto status_before = client->check_status("test-job");
16031661
EXPECT_TRUE(status_before.success);
16041662

1605-
server_.stop();
1663+
ASSERT_TRUE(server_.stop());
16061664
EXPECT_FALSE(server_.is_running());
16071665

16081666
auto status_down = client->check_status("test-job");
@@ -1630,7 +1688,7 @@ TEST_F(ErrorRecoveryTests, ClientHandlesServerCrashDuringSolve)
16301688
ASSERT_TRUE(submit_result.success);
16311689

16321690
std::this_thread::sleep_for(std::chrono::milliseconds(500));
1633-
server_.stop();
1691+
EXPECT_TRUE(server_.stop());
16341692

16351693
auto status_result = client->check_status(submit_result.job_id);
16361694
EXPECT_FALSE(status_result.success);
@@ -1694,7 +1752,7 @@ TEST_F(ErrorRecoveryTests, ChunkedUploadAfterServerRestart)
16941752
auto result1 = client->solve_mip(problem, settings, false);
16951753
EXPECT_TRUE(result1.success) << result1.error_message;
16961754

1697-
server_.stop();
1755+
ASSERT_TRUE(server_.stop());
16981756
ASSERT_TRUE(start_server({"--max-message-mb", "256"}));
16991757

17001758
auto client2 = create_client(config);
@@ -1745,7 +1803,7 @@ class TlsServerTests : public GrpcIntegrationTestBase {
17451803

17461804
static void TearDownTestSuite()
17471805
{
1748-
if (s_server_) s_server_->stop();
1806+
if (s_server_) EXPECT_TRUE(s_server_->stop());
17491807
s_server_.reset();
17501808
}
17511809

@@ -1846,7 +1904,7 @@ class MtlsServerTests : public GrpcIntegrationTestBase {
18461904

18471905
static void TearDownTestSuite()
18481906
{
1849-
if (s_server_) s_server_->stop();
1907+
if (s_server_) EXPECT_TRUE(s_server_->stop());
18501908
s_server_.reset();
18511909
}
18521910

@@ -1917,7 +1975,7 @@ class ChunkValidationTests : public GrpcIntegrationTestBase {
19171975

19181976
static void TearDownTestSuite()
19191977
{
1920-
if (s_server_) s_server_->stop();
1978+
if (s_server_) EXPECT_TRUE(s_server_->stop());
19211979
s_server_.reset();
19221980
}
19231981

0 commit comments

Comments
 (0)