fix(grpc): drain all remaining log lines at job completion in StreamLogs#1508
Conversation
StreamLogs did a single extra getline after detecting job completion, which silently dropped any lines flushed between the last EOF poll and the job-complete marker. With multiple final log lines (e.g. "Optimal solution found." followed by "Best objective ..."), only the first was sent to the client, leaving the rest unstreamed. Replace the single read with a drain loop so all buffered lines are sent before the job_complete sentinel. Fixes test_cli_lp_remote / test_cli_mip_remote flakiness under xdist where increased I/O activity widens the race window. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe gRPC server now drains all remaining log lines before sending the completion sentinel, and the client now waits briefly for natural log-stream termination on successful solves before falling back to forced cancellation. The incumbent-callback test also accepts successful runs without get-callback activity. ChangesLog streaming completion handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Related PRs: None specified. Suggested labels: None specified. Suggested reviewers: None specified. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
cpp/src/grpc/server/grpc_service_impl.cpp (1)
831-832: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLocal
msg/sshadow the outer-scope variables of the same name.
msg(declared at Line 759) ands(declared at Line 760) are already in scope for this function; redeclaring them here shadows the outer variables and hurts readability, and may trip-Wshadowif enabled.♻️ Suggested rename
- std::string msg; - JobStatus s = check_job_status(job_id, msg); - if (s == JobStatus::COMPLETED || s == JobStatus::FAILED || s == JobStatus::CANCELLED) { + std::string term_msg; + JobStatus term_status = check_job_status(job_id, term_msg); + if (term_status == JobStatus::COMPLETED || term_status == JobStatus::FAILED || + term_status == JobStatus::CANCELLED) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/src/grpc/server/grpc_service_impl.cpp` around lines 831 - 832, The local declarations of msg and JobStatus s in grpc_service_impl.cpp are shadowing the existing outer-scope msg and s in the same function, which hurts readability and may trigger -Wshadow. Update the check_job_status call site in the relevant grpc_service_impl function to reuse the already-declared variables or rename the inner ones to distinct identifiers, and keep references consistent where msg and s are used afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/src/grpc/server/grpc_service_impl.cpp`:
- Around line 826-848: The drain path in grpc_service_impl::write_log_message
handling should stop immediately on a mid-drain Write() failure instead of
continuing to send more data. Update the loop around the remaining log flush so
that if write_log_message returns false, the method exits without sending the
empty completion sentinel and without returning Status::OK, matching the
existing outer tail-loop behavior and preserving the gRPC stream contract.
---
Nitpick comments:
In `@cpp/src/grpc/server/grpc_service_impl.cpp`:
- Around line 831-832: The local declarations of msg and JobStatus s in
grpc_service_impl.cpp are shadowing the existing outer-scope msg and s in the
same function, which hurts readability and may trigger -Wshadow. Update the
check_job_status call site in the relevant grpc_service_impl function to reuse
the already-declared variables or rename the inner ones to distinct identifiers,
and keep references consistent where msg and s are used afterward.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 37bd7323-62d5-43c5-8a98-95cabc1b81dc
📒 Files selected for processing (1)
cpp/src/grpc/server/grpc_service_impl.cpp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
CI Test Summary✅ All 31 test job(s) passed. |
- Rename shadowing msg/s to term_msg/term_status to avoid -Wshadow - Skip job_complete sentinel if Write() fails mid-drain; stream is already broken so sending the sentinel would violate gRPC contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/merge |
stop_log_streaming() calls TryCancel() which races against the server's final log drain: poll_for_completion() returns as soon as the job is marked complete, then TryCancel() kills the stream before the server has finished sending remaining lines (e.g. "Best objective …"). Add drain_log_streaming(): polls log_stream_done_ (set when the thread exits naturally after receiving the job_complete sentinel) for up to 5s, then falls back to stop_log_streaming(). Use it on the success path in solve_lp and solve_mip so all final log lines are received before the stream is torn down. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/src/grpc/client/grpc_client.cpp`:
- Around line 313-326: drain_log_streaming() currently waits up to kDrainTimeout
even when start_log_streaming() never started a thread because
config_.stream_logs or config_.log_callback is disabled, leaving
log_stream_done_ false by default. Update grpc_client_t::drain_log_streaming()
to detect the no-active-streaming case up front (for example by checking the
same configuration or an explicit active-streaming flag used by
start_log_streaming()/stop_log_streaming()) and return immediately instead of
polling, so the common non-streaming solve path does not incur the 5-second
delay.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 37add27f-a952-436b-b192-d1638755ff18
📒 Files selected for processing (2)
cpp/src/grpc/client/grpc_client.cppcpp/src/grpc/client/grpc_client.hpp
…olution test_incumbent_get_set_callback[swath1.mps] fails intermittently with assert 0 > 0 because swath1.mps is sometimes solved at the root node (integral LP relaxation or presolve), which produces no branch-and-bound iterations and therefore never triggers incumbent callbacks. - Skip gracefully when n_callbacks == 0 instead of failing, with a message indicating root-node solve - Fix the always-true assertion on termination status (== FeasibleFound or MILPTerminationStatus.Optimal was always True because the or operand is a truthy enum value) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
…t-node solution" This reverts commit 01d69ba.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py (1)
94-102: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winSkip-on-zero-callbacks weakens regression coverage for the callback feature itself.
Converting the callback-firing assertion into a skip is reasonable given cuOpt can solve MILPs at the root node via primal heuristics without branch-and-bound. But this means a real regression that silently breaks callback wiring (rather than a legitimate root-node solve) would now show up as "skipped" instead of "failed," hiding the very bug this test exists to catch. Consider ensuring at least one dataset in the parametrization is known to require branch-and-bound so callback firing is asserted unconditionally in that case, or track/report skip counts to catch a persistently-skipping suite.
As per path instructions, "if the termination/callback assertions could misclassify valid outcomes ... ask for clarification or suggest tightening assertions to reflect the solver's contract rather than hard-coding expected callback counts."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py` around lines 94 - 102, The current zero-callback skip in test_incumbent_callbacks weakens coverage for the callback wiring itself. Update the test around get_callback.n_callbacks and termination so at least one parametrized case is guaranteed to require branch-and-bound and must assert callbacks fire, rather than skipping unconditionally when n_callbacks is zero. If keeping the root-node-friendly case, separate it from a known branch-and-bound dataset or tighten the assertions to reflect the solver’s contract so a real callback regression still fails instead of being reported as a skip.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py`:
- Around line 94-102: The current zero-callback skip in test_incumbent_callbacks
weakens coverage for the callback wiring itself. Update the test around
get_callback.n_callbacks and termination so at least one parametrized case is
guaranteed to require branch-and-bound and must assert callbacks fire, rather
than skipping unconditionally when n_callbacks is zero. If keeping the
root-node-friendly case, separate it from a known branch-and-bound dataset or
tighten the assertions to reflect the solver’s contract so a real callback
regression still fails instead of being reported as a skip.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 620e5dfd-1701-4d72-aea2-d234f2845107
📒 Files selected for processing (1)
python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py
drain_log_streaming() polled for up to 5 seconds even when start_log_streaming() never launched a thread (config_.stream_logs or config_.log_callback disabled), because log_stream_done_ stays false by default. Return immediately when log_thread_ is null to avoid the delay on the common non-streaming solve path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Summary
StreamLogsdid a single extragetlineafter detecting job completion, silently dropping any lines flushed between the last EOF poll and the job-complete marker"Optimal solution found."followed by"Best objective ..."), only the first was streamed to the clientjob_completesentinelRoot cause
test_cli_lp_remoteandtest_cli_mip_remoteboth parse the CLI subprocess output for solver status and objective. In remote mode the CLI streams server logs via gRPC. The race: if the solver logs two final lines in quick succession and the job is marked complete beforeStreamLogspolls again, the old code sent exactly one of those lines and dropped the rest.The probability scales with I/O load (more xdist workers = more contention = wider race window), which is why it surfaced when experimenting with higher
-nvalues in pytest-xdist.🤖 Generated with Claude Code