Skip to content

Commit fd1e773

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

1 file changed

Lines changed: 103 additions & 37 deletions

File tree

cpp/tests/linear_programming/grpc/grpc_integration_test.cpp

Lines changed: 103 additions & 37 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,46 @@ 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-
}
150-
151-
if (waitpid(pid_, &status, WNOHANG) == 0) {
152-
kill(pid_, SIGKILL);
153-
waitpid(pid_, &status, 0);
164+
if (pid_ <= 0) return true;
165+
166+
// Ask the whole group (server + GPU worker) to exit gracefully, reaping the
167+
// server parent so its zombie does not keep the group id alive.
168+
kill(-pgid_, SIGTERM);
169+
bool parent_reaped = reap_parent(std::chrono::seconds(5));
170+
171+
// Escalate to SIGKILL if the parent ignored SIGTERM, or if any group member
172+
// (e.g. the GPU worker) is still alive once the parent is gone.
173+
if (!parent_reaped || !wait_for_group_exit(std::chrono::seconds(5))) {
174+
kill(-pgid_, SIGKILL);
175+
if (!parent_reaped) {
176+
int status = 0;
177+
while (waitpid(pid_, &status, 0) < 0 && errno == EINTR) {}
154178
}
179+
}
155180

156-
pid_ = -1;
181+
// Only consider the lifecycle over once the full group is confirmed gone.
182+
if (!wait_for_group_exit(std::chrono::seconds(5))) {
183+
std::cerr << "Server process group " << pgid_ << " did not exit\n";
184+
return false;
157185
}
186+
187+
clear_lifecycle_state();
188+
return true;
158189
}
159190

160191
int port() const { return port_; }
@@ -172,6 +203,40 @@ class ServerProcess {
172203
}
173204

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

249-
int status;
250-
if (waitpid(pid_, &status, WNOHANG) != 0) {
314+
int status = 0;
315+
if (waitpid(pid_, &status, WNOHANG) == pid_) {
251316
std::cerr << "Server process died during startup\n";
252317
return false;
253318
}
@@ -257,6 +322,7 @@ class ServerProcess {
257322
}
258323

259324
pid_t pid_;
325+
pid_t pgid_;
260326
int port_;
261327
std::string tls_root_certs_;
262328
std::string tls_client_cert_;
@@ -516,7 +582,7 @@ class DefaultServerTests : public GrpcIntegrationTestBase {
516582

517583
static void TearDownTestSuite()
518584
{
519-
if (s_server_) s_server_->stop();
585+
if (s_server_) EXPECT_TRUE(s_server_->stop());
520586
s_server_.reset();
521587
}
522588

@@ -1083,7 +1149,7 @@ class ChunkedUploadTests : public GrpcIntegrationTestBase {
10831149

10841150
static void TearDownTestSuite()
10851151
{
1086-
if (s_server_) s_server_->stop();
1152+
if (s_server_) EXPECT_TRUE(s_server_->stop());
10871153
s_server_.reset();
10881154
}
10891155

@@ -1399,7 +1465,7 @@ class PathSelectionTests : public GrpcIntegrationTestBase {
13991465

14001466
static void TearDownTestSuite()
14011467
{
1402-
if (s_server_) s_server_->stop();
1468+
if (s_server_) EXPECT_TRUE(s_server_->stop());
14031469
s_server_.reset();
14041470
}
14051471

@@ -1583,7 +1649,7 @@ TEST_F(PathSelectionTests, UnaryUploadMIPWithPathLogging)
15831649
class ErrorRecoveryTests : public GrpcIntegrationTestBase {
15841650
protected:
15851651
void SetUp() override { port_ = get_test_port(); }
1586-
void TearDown() override { server_.stop(); }
1652+
void TearDown() override { EXPECT_TRUE(server_.stop()); }
15871653

15881654
bool start_server(const std::vector<std::string>& extra_args = {})
15891655
{
@@ -1602,7 +1668,7 @@ TEST_F(ErrorRecoveryTests, ClientReconnectsAfterServerRestart)
16021668
auto status_before = client->check_status("test-job");
16031669
EXPECT_TRUE(status_before.success);
16041670

1605-
server_.stop();
1671+
ASSERT_TRUE(server_.stop());
16061672
EXPECT_FALSE(server_.is_running());
16071673

16081674
auto status_down = client->check_status("test-job");
@@ -1630,7 +1696,7 @@ TEST_F(ErrorRecoveryTests, ClientHandlesServerCrashDuringSolve)
16301696
ASSERT_TRUE(submit_result.success);
16311697

16321698
std::this_thread::sleep_for(std::chrono::milliseconds(500));
1633-
server_.stop();
1699+
EXPECT_TRUE(server_.stop());
16341700

16351701
auto status_result = client->check_status(submit_result.job_id);
16361702
EXPECT_FALSE(status_result.success);
@@ -1694,7 +1760,7 @@ TEST_F(ErrorRecoveryTests, ChunkedUploadAfterServerRestart)
16941760
auto result1 = client->solve_mip(problem, settings, false);
16951761
EXPECT_TRUE(result1.success) << result1.error_message;
16961762

1697-
server_.stop();
1763+
ASSERT_TRUE(server_.stop());
16981764
ASSERT_TRUE(start_server({"--max-message-mb", "256"}));
16991765

17001766
auto client2 = create_client(config);
@@ -1745,7 +1811,7 @@ class TlsServerTests : public GrpcIntegrationTestBase {
17451811

17461812
static void TearDownTestSuite()
17471813
{
1748-
if (s_server_) s_server_->stop();
1814+
if (s_server_) EXPECT_TRUE(s_server_->stop());
17491815
s_server_.reset();
17501816
}
17511817

@@ -1846,7 +1912,7 @@ class MtlsServerTests : public GrpcIntegrationTestBase {
18461912

18471913
static void TearDownTestSuite()
18481914
{
1849-
if (s_server_) s_server_->stop();
1915+
if (s_server_) EXPECT_TRUE(s_server_->stop());
18501916
s_server_.reset();
18511917
}
18521918

@@ -1917,7 +1983,7 @@ class ChunkValidationTests : public GrpcIntegrationTestBase {
19171983

19181984
static void TearDownTestSuite()
19191985
{
1920-
if (s_server_) s_server_->stop();
1986+
if (s_server_) EXPECT_TRUE(s_server_->stop());
19211987
s_server_.reset();
19221988
}
19231989

0 commit comments

Comments
 (0)