Skip to content

Commit 1bbc8d7

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

1 file changed

Lines changed: 137 additions & 38 deletions

File tree

cpp/tests/linear_programming/grpc/grpc_integration_test.cpp

Lines changed: 137 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,33 +146,42 @@ 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_; }
184+
pid_t verified_process_group_id() const { return pgid_; }
161185

162186
bool is_running() const
163187
{
@@ -172,6 +196,40 @@ class ServerProcess {
172196
}
173197

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

249-
int status;
250-
if (waitpid(pid_, &status, WNOHANG) != 0) {
307+
int status = 0;
308+
if (waitpid(pid_, &status, WNOHANG) == pid_) {
251309
std::cerr << "Server process died during startup\n";
252310
return false;
253311
}
@@ -257,6 +315,7 @@ class ServerProcess {
257315
}
258316

259317
pid_t pid_;
318+
pid_t pgid_;
260319
int port_;
261320
std::string tls_root_certs_;
262321
std::string tls_client_cert_;
@@ -516,7 +575,7 @@ class DefaultServerTests : public GrpcIntegrationTestBase {
516575

517576
static void TearDownTestSuite()
518577
{
519-
if (s_server_) s_server_->stop();
578+
if (s_server_) EXPECT_TRUE(s_server_->stop());
520579
s_server_.reset();
521580
}
522581

@@ -1083,7 +1142,7 @@ class ChunkedUploadTests : public GrpcIntegrationTestBase {
10831142

10841143
static void TearDownTestSuite()
10851144
{
1086-
if (s_server_) s_server_->stop();
1145+
if (s_server_) EXPECT_TRUE(s_server_->stop());
10871146
s_server_.reset();
10881147
}
10891148

@@ -1399,7 +1458,7 @@ class PathSelectionTests : public GrpcIntegrationTestBase {
13991458

14001459
static void TearDownTestSuite()
14011460
{
1402-
if (s_server_) s_server_->stop();
1461+
if (s_server_) EXPECT_TRUE(s_server_->stop());
14031462
s_server_.reset();
14041463
}
14051464

@@ -1576,14 +1635,51 @@ TEST_F(PathSelectionTests, UnaryUploadMIPWithPathLogging)
15761635
<< log_capture.get_server_logs();
15771636
}
15781637

1638+
// =============================================================================
1639+
// Server Process Lifecycle Tests
1640+
// =============================================================================
1641+
1642+
class ServerProcessLifecycleTests : public GrpcIntegrationTestBase {
1643+
protected:
1644+
void SetUp() override { port_ = get_test_port(); }
1645+
void TearDown() override { EXPECT_TRUE(server_.stop()); }
1646+
1647+
ServerProcess server_;
1648+
};
1649+
1650+
::testing::AssertionResult VerifiedProcessGroupIsGone(pid_t pgid)
1651+
{
1652+
errno = 0;
1653+
if (kill(-pgid, 0) < 0 && errno == ESRCH) { return ::testing::AssertionSuccess(); }
1654+
return ::testing::AssertionFailure()
1655+
<< "Verified server process group " << pgid << " still exists (errno " << errno << ")";
1656+
}
1657+
1658+
TEST_F(ServerProcessLifecycleTests, IdleServerStopCleansProcessGroup)
1659+
{
1660+
ASSERT_TRUE(server_.start(port_));
1661+
pid_t server_pgid = server_.verified_process_group_id();
1662+
ASSERT_GT(server_pgid, 0);
1663+
EXPECT_TRUE(server_.stop());
1664+
EXPECT_FALSE(server_.is_running());
1665+
EXPECT_TRUE(VerifiedProcessGroupIsGone(server_pgid));
1666+
}
1667+
1668+
TEST_F(ServerProcessLifecycleTests, StopIsIdempotent)
1669+
{
1670+
ASSERT_TRUE(server_.start(port_));
1671+
ASSERT_TRUE(server_.stop());
1672+
EXPECT_TRUE(server_.stop());
1673+
}
1674+
15791675
// =============================================================================
15801676
// Error Recovery Tests (per-test server lifecycle)
15811677
// =============================================================================
15821678

15831679
class ErrorRecoveryTests : public GrpcIntegrationTestBase {
15841680
protected:
15851681
void SetUp() override { port_ = get_test_port(); }
1586-
void TearDown() override { server_.stop(); }
1682+
void TearDown() override { EXPECT_TRUE(server_.stop()); }
15871683

15881684
bool start_server(const std::vector<std::string>& extra_args = {})
15891685
{
@@ -1602,7 +1698,7 @@ TEST_F(ErrorRecoveryTests, ClientReconnectsAfterServerRestart)
16021698
auto status_before = client->check_status("test-job");
16031699
EXPECT_TRUE(status_before.success);
16041700

1605-
server_.stop();
1701+
ASSERT_TRUE(server_.stop());
16061702
EXPECT_FALSE(server_.is_running());
16071703

16081704
auto status_down = client->check_status("test-job");
@@ -1617,6 +1713,8 @@ TEST_F(ErrorRecoveryTests, ClientReconnectsAfterServerRestart)
16171713
TEST_F(ErrorRecoveryTests, ClientHandlesServerCrashDuringSolve)
16181714
{
16191715
ASSERT_TRUE(start_server());
1716+
pid_t server_pgid = server_.verified_process_group_id();
1717+
ASSERT_GT(server_pgid, 0);
16201718
auto client = create_client();
16211719
ASSERT_NE(client, nullptr);
16221720

@@ -1630,7 +1728,8 @@ TEST_F(ErrorRecoveryTests, ClientHandlesServerCrashDuringSolve)
16301728
ASSERT_TRUE(submit_result.success);
16311729

16321730
std::this_thread::sleep_for(std::chrono::milliseconds(500));
1633-
server_.stop();
1731+
EXPECT_TRUE(server_.stop()) << "Active-solve shutdown must remove the server process group";
1732+
EXPECT_TRUE(VerifiedProcessGroupIsGone(server_pgid));
16341733

16351734
auto status_result = client->check_status(submit_result.job_id);
16361735
EXPECT_FALSE(status_result.success);
@@ -1694,7 +1793,7 @@ TEST_F(ErrorRecoveryTests, ChunkedUploadAfterServerRestart)
16941793
auto result1 = client->solve_mip(problem, settings, false);
16951794
EXPECT_TRUE(result1.success) << result1.error_message;
16961795

1697-
server_.stop();
1796+
ASSERT_TRUE(server_.stop());
16981797
ASSERT_TRUE(start_server({"--max-message-mb", "256"}));
16991798

17001799
auto client2 = create_client(config);
@@ -1745,7 +1844,7 @@ class TlsServerTests : public GrpcIntegrationTestBase {
17451844

17461845
static void TearDownTestSuite()
17471846
{
1748-
if (s_server_) s_server_->stop();
1847+
if (s_server_) EXPECT_TRUE(s_server_->stop());
17491848
s_server_.reset();
17501849
}
17511850

@@ -1846,7 +1945,7 @@ class MtlsServerTests : public GrpcIntegrationTestBase {
18461945

18471946
static void TearDownTestSuite()
18481947
{
1849-
if (s_server_) s_server_->stop();
1948+
if (s_server_) EXPECT_TRUE(s_server_->stop());
18501949
s_server_.reset();
18511950
}
18521951

@@ -1917,7 +2016,7 @@ class ChunkValidationTests : public GrpcIntegrationTestBase {
19172016

19182017
static void TearDownTestSuite()
19192018
{
1920-
if (s_server_) s_server_->stop();
2019+
if (s_server_) EXPECT_TRUE(s_server_->stop());
19212020
s_server_.reset();
19222021
}
19232022

0 commit comments

Comments
 (0)