Skip to content

add a Python interface to the C++ grpc asyc client#1430

Open
tmckayus wants to merge 6 commits into
NVIDIA:mainfrom
tmckayus:feature/cuopt-grpc-async-client
Open

add a Python interface to the C++ grpc asyc client#1430
tmckayus wants to merge 6 commits into
NVIDIA:mainfrom
tmckayus:feature/cuopt-grpc-async-client

Conversation

@tmckayus

Copy link
Copy Markdown
Contributor

This lets Python use an async grpc client with Problem type in additon to existing blocking Problem.solve() with remote execution env vars set (zero-code-change remote solve)

For some scenarios, an async client with polling, or results fetched by a different process/thread, or job cancellation, etc is desirable. It also helps with agents that formulate problems and then want to cancel the job and modify the model.

This lets Python use an async grpc client with Problem type in
additon to existing blocking Problem.solve() with remote execution env
vars set (zero-code-change remote solve)
@tmckayus tmckayus requested review from a team as code owners June 12, 2026 15:40
@tmckayus

Copy link
Copy Markdown
Contributor Author

Check test_grpc_client.py to see usage. Adding user-facing documentation/example as a follow up on the PR

@tmckayus tmckayus added non-breaking Introduces a non-breaking change Feature feature request New feature or request labels Jun 12, 2026
@tmckayus

Copy link
Copy Markdown
Contributor Author

Also, some of the name-spacing around linear_programming should change in a future PR. The grpc client is not necessarily specific to linear programming, although it started there. We should move it later. For the moment, left it alone to reduce churn.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e97822b8-41b3-48c0-997c-2fd8bba9ca06

📥 Commits

Reviewing files that changed from the base of the PR and between 081e193 and 351e194.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/grpc_server_fixtures.py
  • python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cuopt/cuopt/grpc_server_fixtures.py

📝 Walkthrough

Walkthrough

Adds a complete gRPC Python client for cuOpt by introducing C++ environment-driven configuration, a C++ wrapper exposing client operations, Cython bindings, Python Client class with job submission/monitoring/log-streaming/incumbent-streaming support, refactored solution building, integration tests, and test server fixtures.

Changes

gRPC Python Client Implementation

Layer / File(s) Summary
C++ Environment-Driven gRPC Configuration
cpp/include/cuopt/grpc/grpc_client_env.hpp, cpp/src/grpc/client/grpc_client_env.cpp
Extracts CUOPT_* environment-variable parsing and TLS/chunk-size configuration into reusable functions (apply_grpc_client_env_overrides, make_grpc_client_config) with validation for host, port, and mTLS certificate requirements.
C++ gRPC Client Wrapper for Cython
cpp/include/cuopt/grpc/cython_grpc_client.hpp, cpp/src/grpc/client/cython_grpc_client.cpp
Implements grpc_python_client_t C++ class with enum/struct types for job status, results, logs, and incumbents; provides methods for connect, submit (MIP/LP auto-detection), status polling with timeout, result retrieval with solution conversion, log streaming/fetching, incumbent fetching with pagination, and error reporting.
solve_remote.cpp Refactoring
cpp/src/grpc/client/solve_remote.cpp
Removes local environment and TLS configuration helpers; replaces internal apply_env_overrides calls with apply_grpc_client_env_overrides in both solve_lp_remote and solve_mip_remote functions.
Python Solution Building Helpers
python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pxd, python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
Centralizes variable-mapping logic via _vars_dict helper, introduces create_solution_with_names unified entry point, and exports build_solution_from_unique_ptr and prepare_solver_settings for use by gRPC client result conversion.
Cython Bindings and Python Client Implementation
python/cuopt/cuopt/grpc/grpc_client.pxd, python/cuopt/cuopt/grpc/grpc_client.pyx
Declares Cython extern C++ types and grpc_python_client_t interface; implements Python Client class with job submission, status/wait polling, result fetching, log streaming/fetching with background threads and backfill retry on incomplete streams, incumbent streaming with direct callbacks or forwarding to SolverSettings MIP callbacks, and helpers for MIP detection and data model conversion.
Python Package Build and Module Integration
cpp/CMakeLists.txt, python/cuopt/cuopt/CMakeLists.txt, python/cuopt/cuopt/__init__.py, python/cuopt/cuopt/grpc/CMakeLists.txt, python/cuopt/cuopt/grpc/__init__.py
Adds gRPC client source files to C++ build, configures Cython module generation via rapids-cython, enables lazy loading of grpc module in cuopt package, and re-exports Client/GrpcError/JobStatus/JobNotReadyError from module init.
gRPC Test Server Fixtures and Lifecycle
python/cuopt/cuopt/grpc_server_fixtures.py, python/cuopt/conftest.py
Provides utilities to locate gRPC server executable, wait for port availability, construct CPU-only test environments, and start/stop server processes with signal handling; defines class-scoped pytest fixture with per-class server isolation and configurable yield (env or port).
gRPC Client Integration Tests
python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py
Comprehensive test suite validating LP/MIP submission, status transitions, result retrieval, log streaming with callback collection and bulk fetching verification, job-not-ready error handling, and incumbent streaming with GetSolutionCallback integration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

improvement

Suggested reviewers

  • hlinsen
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'add a Python interface to the C++ grpc asyc client' is directly related to the main change: extending the codebase with Python bindings for a C++ gRPC async client. Note: 'asyc' appears to be a typo for 'async'.
Description check ✅ Passed The description is relevant to the changeset, explaining the purpose of the async gRPC client interface and its use cases (polling, cancellation, multi-process/thread result fetching).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (5)
cpp/include/cuopt/grpc/cython_grpc_client.hpp (1)

138-143: ⚡ Quick win

Fix the fetch_incumbents contract comment.

This API is shaped as a single paginated fetch (from_index, max_count, next_index, job_complete), but the comment says it polls until completion. The implementation in cpp/src/grpc/client/cython_grpc_client.cpp also performs just one get_incumbents call. Please document the actual one-page behavior so the Cython layer does not assume it blocks.

🤖 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/include/cuopt/grpc/cython_grpc_client.hpp` around lines 138 - 143, The
doc comment for fetch_incumbents is incorrect: it does not poll until job
completion but performs a single paginated fetch using the supplied from_index
and max_count and returns a grpc_incumbents_result_t that includes next_index
and job_complete flags; update the comment on fetch_incumbents to state it
issues one GetIncumbents RPC call that returns up to max_count incumbents
starting at from_index and indicates the next_index to continue pagination and
whether the job is complete so callers (e.g., the Cython layer) must loop if
they want to poll to completion.
python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py (4)

27-38: ⚖️ Poor tradeoff

Polling logic may cause non-deterministic test behavior.

Lines 35-36 check if results are available during polling and return COMPLETED even if status() hasn't transitioned yet. This dual-check pattern can cause race conditions:

  • Result availability and status transitions may not be atomic
  • Different timing on CI vs local runs could produce different test paths
  • The C++ wait() (context snippet 2) polls status only until !is_in_flight

For deterministic tests, rely on client.wait() or poll status() alone, not result availability.

Simplify polling to use only status checks
 def _poll_until_complete(
     client, job_id, names, timeout=120, poll_interval=0.05
 ):
     deadline = time.monotonic() + timeout
     while time.monotonic() < deadline:
         status = client.status(job_id)
         if status not in (JobStatus.QUEUED, JobStatus.PROCESSING):
             return status
-        if client.result(job_id, names) is not None:
-            return JobStatus.COMPLETED
         time.sleep(poll_interval)
     return client.status(job_id)

Or use client.wait() directly instead of a custom helper.

🤖 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_grpc_client.py` around lines
27 - 38, The _poll_until_complete helper mixes status checks and result()
availability which can lead to racey, non-deterministic tests; update it to rely
solely on client.status (or replace its usage with client.wait) so state
transitions are the single source of truth. Specifically, change the logic in
_poll_until_complete to remove the client.result(job_id, names) check and only
return based on client.status(job_id) and JobStatus values (or call
client.wait(job_id, timeout) from tests instead of _poll_until_complete) to
match the C++ wait() behavior and avoid race conditions.

109-124: ⚡ Quick win

Validate that MIP solution variables are actually integers.

Line 121-123 fetch and validate the objective but don't check that x and y are integer-valued, which is the defining characteristic of a MIP solution.

Add integer validation
         solution = client.result(job_id, _MIP_NAMES)
         assert solution is not None
         assert solution.get_primal_objective() == pytest.approx(15.0, rel=1e-3)
+        vars_ = solution.get_vars()
+        assert vars_["x"] % 1 == 0, f"x={vars_['x']} is not integer"
+        assert vars_["y"] % 1 == 0, f"y={vars_['y']} is not integer"
         client.delete(job_id)
🤖 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_grpc_client.py` around lines
109 - 124, In test_mip_submit_and_result, after obtaining solution =
client.result(job_id, _MIP_NAMES) and asserting the objective, also validate
that the decision variables x and y are integer-valued: extract the primal
values for "x" and "y" from the solution (use the existing solution API such as
solution.get_primal_value("x") / solution.get_primal_value("y") or the
equivalent accessor used elsewhere), and assert each is integer within a small
tolerance (e.g., assert abs(val - round(val)) < 1e-6 or compare to
pytest.approx(round(val))). Keep these checks immediately after the objective
assertion and before client.delete(job_id).

70-92: ⚡ Quick win

Verify whether log backfill was needed.

Line 90 asserts length equality but doesn't check if streaming worked or if backfill was required. According to context snippet 4, join_log_stream() returns a state dict with "backfilled" and "live_lines" keys. To catch regressions where streaming silently fails and only backfill works, assert that live streaming actually received lines:

Add assertion on streaming success
         terminal = _poll_until_complete(client, job_id, _DEMO_LP_NAMES)
         assert terminal == JobStatus.COMPLETED
-        client.join_log_stream(job_id)
+        state = client.join_log_stream(job_id)
+        assert state["live_lines"] > 0, "Log streaming failed; only backfill worked"
 
         solution = client.result(job_id, _DEMO_LP_NAMES)
🤖 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_grpc_client.py` around lines
70 - 92, The test currently only compares received vs bulk logs length and
doesn't verify whether live streaming supplied any lines; after calling
client.join_log_stream(job_id) capture its return value (the state dict returned
by join_log_stream) and assert that the "live_lines" count is > 0 (and
optionally that "backfilled" is False if you want to ensure no backfill was
required) to ensure start_log_stream actually streamed messages; modify
test_submit_with_log_stream to store the result of join_log_stream and add an
assertion on state["live_lines"] > 0 (referencing test_submit_with_log_stream,
client.start_log_stream, client.join_log_stream and the
"live_lines"/"backfilled" keys).

126-168: ⚡ Quick win

Validate incumbent solution quality and progression.

Lines 162-164 assert that incumbents exist but don't validate their content or progression:

  • No check that incumbent objectives improve monotonically
  • No validation that the final solution's objective matches the best incumbent
  • No verification of incumbent variable values

Tests should validate numerical correctness, not just that callbacks fired.

Add incumbent validation
         assert collector.entries
+        # Verify objectives are non-decreasing (MAXIMIZE problem)
+        objectives = [e["cost"] for e in collector.entries]
+        for i in range(1, len(objectives)):
+            assert objectives[i] >= objectives[i-1], \
+                f"Incumbent regressed: {objectives[i-1]} -> {objectives[i]}"
+        
         bulk = client.incumbents(job_id)
         assert bulk
+        assert len(bulk) == len(collector.entries)
 
         solution = client.result(job_id, _MIP_NAMES)
         assert solution is not None
+        final_obj = solution.get_primal_objective()
+        best_incumbent = max(e["cost"] for e in collector.entries)
+        assert final_obj == pytest.approx(best_incumbent, rel=1e-9)
         client.delete(job_id)
🤖 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_grpc_client.py` around lines
126 - 168, The test_mip_incumbent_stream currently only asserts that incumbents
were received; update it to validate incumbent quality by (1) inspecting
collector.entries (populated by IncumbentCollector.get_solution) to ensure the
reported costs form a non-increasing sequence (monotonic improvement for
MAXIMIZE or non-decreasing for MINIMIZE), (2) compare the best incumbent cost
from collector.entries to the final solution objective returned by
client.result(job_id, _MIP_NAMES) to ensure they match (within a small numeric
tolerance), and (3) verify that the variable assignments in the final solution
equal the variable values in the best incumbent entry (or are consistent within
tolerance). Use client.incumbents(job_id) and collector.entries to find the best
incumbent and perform numeric comparisons rather than just asserting presence.
🤖 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/cython_grpc_client.cpp`:
- Around line 124-126: The wait() method (grpc_python_client_t::wait)
incorrectly treats timeout_seconds <= 0 as the blocking sentinel and forwards
negative values into impl_->client.wait_for_completion(job_id); change the logic
to only treat 0 as the blocking sentinel (per cython_grpc_client.hpp) and
validate negative values: if timeout_seconds < 0 return an appropriate error
status via map_status_result (or a mapped grpc_status_result_t indicating
invalid argument) instead of calling wait_for_completion; if timeout_seconds ==
0 call wait_for_completion(job_id) and otherwise call the timed API.

In `@cpp/src/grpc/client/grpc_client_env.cpp`:
- Around line 79-83: The make_grpc_client_config function must validate inputs
before composing server_address: check that host is non-empty and port is within
the valid TCP range (1..65535) and, if invalid, throw a std::invalid_argument
with a clear message (e.g., "invalid host" or "invalid port"). Move the
validation to the top of make_grpc_client_config (before building
config.server_address), then compose server_address as host + ":" +
std::to_string(port) and continue calling
apply_grpc_client_env_overrides(config); reference: make_grpc_client_config,
grpc_client_config_t, server_address, apply_grpc_client_env_overrides.
- Around line 70-75: The code silently skips client cert/key when only one env
var is set; update the logic in the grpc client env initialization (look around
get_env("CUOPT_TLS_CLIENT_CERT") / get_env("CUOPT_TLS_CLIENT_KEY") and the
branch that assigns config.tls_client_cert/config.tls_client_key using
read_pem_file) to validate mTLS input: if exactly one of CUOPT_TLS_CLIENT_CERT
or CUOPT_TLS_CLIENT_KEY is present, return an error (or abort initialization)
indicating the missing counterpart; only call read_pem_file and set
config.tls_client_cert and config.tls_client_key when both are present, and
leave both unset when both are absent.

In `@python/cuopt/cuopt/grpc/grpc_client.pyx`:
- Around line 282-298: The join_log_stream implementation currently pops the
thread handle before joining, losing the reference if join(timeout) times out;
change it to look up (but not pop) the thread from self._log_threads, call
thread.join(timeout), and only remove (pop) the thread from self._log_threads
when join returned (thread is not alive) so the thread can be joined/cleaned
later if the timeout expired; similarly only pop self._log_thread_errors and
self._log_stream_state after confirming the thread has stopped (or propagate
errors immediately if thread is known stopped), using the existing symbols
join_log_stream, _log_threads, _log_thread_errors, and _log_stream_state to
guide where to adjust the lookup/pop ordering and lifecycle checks.
- Around line 62-78: The C callback _invoke_log_callback currently swallows
exceptions; change it to capture the exception object and traceback and save
them into thread-shared state on userdata (e.g., set attributes like
userdata._log_error and userdata._log_traceback or push into a thread-safe
container referenced by userdata) instead of just returning 0 silently; still
stop the stream by returning 0 after storing the error. Then update
join_log_stream() (or the function that waits for the stream) to inspect that
shared userdata state after the stream ends and re-raise the original exception
(or raise a new exception that includes the stored traceback/message) so callers
see the real failure. Ensure the storage and inspection are done while holding
the GIL and use consistent keys/symbols (userdata, _invoke_log_callback,
_call_log_callback, join_log_stream) so the bridge and join code can find and
propagate the error.
- Around line 265-268: The wrapped() helper currently ignores the return value
of _call_log_callback so the user's stop signal is lost; modify wrapped (used by
start_log_stream) to capture the callback result (e.g., should_continue =
_call_log_callback(callback, line, job_complete)) and return that boolean (or
False on explicit stop) instead of discarding it so start_log_stream can honor
"False stops early" and stop the stream promptly.

In `@python/cuopt/cuopt/tests/linear_programming/grpc_server_fixtures.py`:
- Around line 115-122: Update stop_grpc_server to avoid signaling a process that
already exited: check proc.poll() first and if it is not None just call
proc.wait() (or reap exit code) instead of proc.send_signal(signal.SIGTERM);
only call proc.send_signal(signal.SIGTERM) and the subsequent
proc.wait(timeout=5)/proc.kill() logic when proc.poll() returns None (process
still running). Ensure the function references proc.poll, proc.send_signal,
proc.wait, and proc.kill as described so teardown won't convert a test crash
into a teardown error.
- Around line 82-100: The start_grpc_server function launches cuopt_grpc_server
with the inherited environment; call the test helper cpu_only_env() to get the
sanitized environment, pass it into subprocess.Popen via the env=... parameter,
and return the proc and the env tuple as the docstring promises; update the
Popen call in start_grpc_server (which locates the binary via find_grpc_server)
to include env=cpu_only_env() (store it in a variable like env) and return
(proc, env).

In `@python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py`:
- Around line 41-168: The tests only cover happy paths; add non-happy-path unit
tests in python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py that
exercise client.delete, client.result, and client.logs for error scenarios: (1)
submit jobs that produce infeasible/unbounded/empty outcomes (use or create
small problems analogous to _demo_lp_problem/_MIP_NAMES) and assert
result()/logs() return appropriate errors or None and JobStatus reflects
INFEASIBLE/UNBOUNDED/etc, (2) test deleting a job while it is JobStatus.QUEUED
and while JobStatus.PROCESSING then assert subsequent result(job_id, ...) and
logs(job_id) raise the expected errors or return None, (3) call
client.result/client.logs after client.delete and assert proper error behavior,
and (4) add tests for invalid job_id and unreachable server (e.g., use a bad
port or mock Client to simulate connection failure) asserting the client raises
the expected exceptions; reference existing symbols Client, job_id,
client.submit, client.delete, client.result, client.logs,
client.start_log_stream, client.start_incumbent_stream, JobStatus, and
JobNotReadyError to locate where to add these cases.

---

Nitpick comments:
In `@cpp/include/cuopt/grpc/cython_grpc_client.hpp`:
- Around line 138-143: The doc comment for fetch_incumbents is incorrect: it
does not poll until job completion but performs a single paginated fetch using
the supplied from_index and max_count and returns a grpc_incumbents_result_t
that includes next_index and job_complete flags; update the comment on
fetch_incumbents to state it issues one GetIncumbents RPC call that returns up
to max_count incumbents starting at from_index and indicates the next_index to
continue pagination and whether the job is complete so callers (e.g., the Cython
layer) must loop if they want to poll to completion.

In `@python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py`:
- Around line 27-38: The _poll_until_complete helper mixes status checks and
result() availability which can lead to racey, non-deterministic tests; update
it to rely solely on client.status (or replace its usage with client.wait) so
state transitions are the single source of truth. Specifically, change the logic
in _poll_until_complete to remove the client.result(job_id, names) check and
only return based on client.status(job_id) and JobStatus values (or call
client.wait(job_id, timeout) from tests instead of _poll_until_complete) to
match the C++ wait() behavior and avoid race conditions.
- Around line 109-124: In test_mip_submit_and_result, after obtaining solution =
client.result(job_id, _MIP_NAMES) and asserting the objective, also validate
that the decision variables x and y are integer-valued: extract the primal
values for "x" and "y" from the solution (use the existing solution API such as
solution.get_primal_value("x") / solution.get_primal_value("y") or the
equivalent accessor used elsewhere), and assert each is integer within a small
tolerance (e.g., assert abs(val - round(val)) < 1e-6 or compare to
pytest.approx(round(val))). Keep these checks immediately after the objective
assertion and before client.delete(job_id).
- Around line 70-92: The test currently only compares received vs bulk logs
length and doesn't verify whether live streaming supplied any lines; after
calling client.join_log_stream(job_id) capture its return value (the state dict
returned by join_log_stream) and assert that the "live_lines" count is > 0 (and
optionally that "backfilled" is False if you want to ensure no backfill was
required) to ensure start_log_stream actually streamed messages; modify
test_submit_with_log_stream to store the result of join_log_stream and add an
assertion on state["live_lines"] > 0 (referencing test_submit_with_log_stream,
client.start_log_stream, client.join_log_stream and the
"live_lines"/"backfilled" keys).
- Around line 126-168: The test_mip_incumbent_stream currently only asserts that
incumbents were received; update it to validate incumbent quality by (1)
inspecting collector.entries (populated by IncumbentCollector.get_solution) to
ensure the reported costs form a non-increasing sequence (monotonic improvement
for MAXIMIZE or non-decreasing for MINIMIZE), (2) compare the best incumbent
cost from collector.entries to the final solution objective returned by
client.result(job_id, _MIP_NAMES) to ensure they match (within a small numeric
tolerance), and (3) verify that the variable assignments in the final solution
equal the variable values in the best incumbent entry (or are consistent within
tolerance). Use client.incumbents(job_id) and collector.entries to find the best
incumbent and perform numeric comparisons rather than just asserting presence.
🪄 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: 2d3732be-cc10-45f1-96b4-5aba3dad083c

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec39b3 and 22d7297.

📒 Files selected for processing (17)
  • cpp/CMakeLists.txt
  • cpp/include/cuopt/grpc/cython_grpc_client.hpp
  • cpp/include/cuopt/grpc/grpc_client_env.hpp
  • cpp/src/grpc/client/cython_grpc_client.cpp
  • cpp/src/grpc/client/grpc_client_env.cpp
  • cpp/src/grpc/client/solve_remote.cpp
  • python/cuopt/cuopt/CMakeLists.txt
  • python/cuopt/cuopt/__init__.py
  • python/cuopt/cuopt/grpc/CMakeLists.txt
  • python/cuopt/cuopt/grpc/__init__.py
  • python/cuopt/cuopt/grpc/grpc_client.pxd
  • python/cuopt/cuopt/grpc/grpc_client.pyx
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pxd
  • python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx
  • python/cuopt/cuopt/tests/linear_programming/conftest.py
  • python/cuopt/cuopt/tests/linear_programming/grpc_server_fixtures.py
  • python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py

Comment thread cpp/src/grpc/client/cython_grpc_client.cpp Outdated
Comment thread cpp/src/grpc/client/grpc_client_env.cpp
Comment thread cpp/src/grpc/client/grpc_client_env.cpp
Comment thread python/cuopt/cuopt/grpc/grpc_client.pyx
Comment thread python/cuopt/cuopt/grpc/grpc_client.pyx Outdated
Comment thread python/cuopt/cuopt/grpc/grpc_client.pyx
Comment thread python/cuopt/cuopt/grpc_server_fixtures.py
Comment thread python/cuopt/cuopt/grpc_server_fixtures.py
Comment thread python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cuopt/cuopt/grpc_server_fixtures.py (1)

35-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use unique port offsets to avoid cross-class server collisions.

Line 35 and Line 36 both resolve to offset 800, which can cause nondeterministic server bind failures when those classes execute concurrently.

Suggested minimal fix
 GRPC_PORT_OFFSET_CLIENT = 800
-GRPC_PORT_OFFSET_TLS = 800
+GRPC_PORT_OFFSET_TLS = 850
 GRPC_PORT_OFFSET_MTLS = 900

As per coding guidelines, **/*test*.{cpp,py} requires preventing flaky tests from race/timing-related behavior; shared fixed ports across classes make this fixture flaky under parallel execution.

🤖 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/grpc_server_fixtures.py` around lines 35 - 37, The
constants GRPC_PORT_OFFSET_CLIENT and GRPC_PORT_OFFSET_TLS currently share the
same value (800), causing port collisions when fixtures run in parallel; change
GRPC_PORT_OFFSET_TLS to a unique, non-overlapping offset (for example 810 or
another unused value distinct from GRPC_PORT_OFFSET_CLIENT=800 and
GRPC_PORT_OFFSET_MTLS=900) and update any references to GRPC_PORT_OFFSET_TLS so
tests and server fixtures use the new constant.

Source: Coding guidelines

🧹 Nitpick comments (1)
python/cuopt/cuopt/grpc_server_fixtures.py (1)

40-167: ⚡ Quick win

Add type hints and complete API docstrings for the new public helpers.

The newly introduced public functions/fixture are missing type annotations, and current docstrings do not consistently document params/returns/raises. Please annotate signatures and expand docstring content accordingly.

As per coding guidelines, python/**/*.py requires type hints on new public functions/classes and docstring content covering params, returns, and raises.

🤖 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/grpc_server_fixtures.py` around lines 40 - 167, The public
helpers find_grpc_server, wait_for_port, cpu_only_env, start_grpc_server,
stop_grpc_server and the grpc_server fixture need Python type annotations and
expanded docstrings that document parameters, return values and raised
exceptions; update each function signature to include appropriate types (e.g. ->
Optional[str] for find_grpc_server, port: int, timeout: float for wait_for_port
-> bool, cpu_only_env(port: int) -> Dict[str,str],
start_grpc_server(port_offset: int) -> Tuple[subprocess.Popen, Dict[str,str]],
stop_grpc_server(proc: subprocess.Popen) -> None, and annotate the fixture
return type in its typing comment or use pytest.GeneratorFixture/Iterator
typing), add necessary typing imports (Optional, Dict, Tuple) and expand each
docstring to list :param:, :return:, and :raises: entries for the conditions
already handled (e.g. pytest.skip, pytest.fail, subprocess.TimeoutExpired) so
the API is fully documented per project guidelines.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@python/cuopt/cuopt/grpc_server_fixtures.py`:
- Around line 35-37: The constants GRPC_PORT_OFFSET_CLIENT and
GRPC_PORT_OFFSET_TLS currently share the same value (800), causing port
collisions when fixtures run in parallel; change GRPC_PORT_OFFSET_TLS to a
unique, non-overlapping offset (for example 810 or another unused value distinct
from GRPC_PORT_OFFSET_CLIENT=800 and GRPC_PORT_OFFSET_MTLS=900) and update any
references to GRPC_PORT_OFFSET_TLS so tests and server fixtures use the new
constant.

---

Nitpick comments:
In `@python/cuopt/cuopt/grpc_server_fixtures.py`:
- Around line 40-167: The public helpers find_grpc_server, wait_for_port,
cpu_only_env, start_grpc_server, stop_grpc_server and the grpc_server fixture
need Python type annotations and expanded docstrings that document parameters,
return values and raised exceptions; update each function signature to include
appropriate types (e.g. -> Optional[str] for find_grpc_server, port: int,
timeout: float for wait_for_port -> bool, cpu_only_env(port: int) ->
Dict[str,str], start_grpc_server(port_offset: int) -> Tuple[subprocess.Popen,
Dict[str,str]], stop_grpc_server(proc: subprocess.Popen) -> None, and annotate
the fixture return type in its typing comment or use
pytest.GeneratorFixture/Iterator typing), add necessary typing imports
(Optional, Dict, Tuple) and expand each docstring to list :param:, :return:, and
:raises: entries for the conditions already handled (e.g. pytest.skip,
pytest.fail, subprocess.TimeoutExpired) so the API is fully documented per
project guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 023d9ed7-2291-4319-be01-e85bec5e7230

📥 Commits

Reviewing files that changed from the base of the PR and between 22d7297 and a023c81.

📒 Files selected for processing (6)
  • cpp/src/grpc/client/cython_grpc_client.cpp
  • cpp/src/grpc/client/grpc_client_env.cpp
  • python/cuopt/conftest.py
  • python/cuopt/cuopt/grpc/grpc_client.pyx
  • python/cuopt/cuopt/grpc_server_fixtures.py
  • python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • python/cuopt/cuopt/tests/linear_programming/test_grpc_client.py
  • cpp/src/grpc/client/grpc_client_env.cpp
  • cpp/src/grpc/client/cython_grpc_client.cpp
  • python/cuopt/cuopt/grpc/grpc_client.pyx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/cuopt/cuopt/grpc_server_fixtures.py (1)

35-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use unique port offsets for each test class configuration.

Line 35 and Line 36 both use 800, which can make grpc_server classes contend for the same port and cause intermittent startup failures under parallel execution.

Suggested fix
 GRPC_PORT_OFFSET_CLIENT = 800
-GRPC_PORT_OFFSET_TLS = 800
+GRPC_PORT_OFFSET_TLS = 850
 GRPC_PORT_OFFSET_MTLS = 900
🤖 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/grpc_server_fixtures.py` around lines 35 - 36,
GRPC_PORT_OFFSET_CLIENT and GRPC_PORT_OFFSET_TLS are both set to 800 causing
port collisions when grpc_server fixtures run in parallel; change one constant
(e.g., GRPC_PORT_OFFSET_TLS) to a different unique offset value (or pick
distinct offsets for both) and update any usages so each test class
configuration uses a distinct port range to avoid startup contention.
🤖 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.

Outside diff comments:
In `@python/cuopt/cuopt/grpc_server_fixtures.py`:
- Around line 35-36: GRPC_PORT_OFFSET_CLIENT and GRPC_PORT_OFFSET_TLS are both
set to 800 causing port collisions when grpc_server fixtures run in parallel;
change one constant (e.g., GRPC_PORT_OFFSET_TLS) to a different unique offset
value (or pick distinct offsets for both) and update any usages so each test
class configuration uses a distinct port range to avoid startup contention.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 89213f16-bf9e-4b36-aab3-98f7a98d35dd

📥 Commits

Reviewing files that changed from the base of the PR and between a023c81 and 081e193.

📒 Files selected for processing (2)
  • python/cuopt/cuopt/grpc_server_fixtures.py
  • python/cuopt/pyproject.toml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request Feature non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant