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
7273namespace {
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
7882class 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)
15831641class 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