Skip to content

Make mip_node_t destructor exception-safe (SonarQube CRIT BUG cpp:S1048)#1230

Open
rgsl888prabhu wants to merge 6 commits into
mainfrom
fix/sonar-bug-mip-node-destructor-noexcept
Open

Make mip_node_t destructor exception-safe (SonarQube CRIT BUG cpp:S1048)#1230
rgsl888prabhu wants to merge 6 commits into
mainfrom
fix/sonar-bug-mip-node-destructor-noexcept

Conversation

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

@rgsl888prabhu rgsl888prabhu commented May 15, 2026

Summary

The iterative-teardown destructor in mip_node_t uses std::vector::push_back to walk and detach the subtree. push_back can throw std::bad_alloc, and a destructor that lets an exception propagate calls std::terminate.

Wraps the body in try { ... } catch (...) {} so the destructor stays exception-free. Under OOM the catch absorbs the failure, and any descendants still attached to children are destroyed via the recursive unique_ptr chain as this frame unwinds — which risks stack overflow on a very deep tree under OOM, but is strictly better than std::terminate.

The iterative teardown uses `std::vector::push_back`, which can throw
`std::bad_alloc`. Letting that propagate out of a destructor calls
`std::terminate`. Wrap the body in a try/catch(...) so the destructor
stays exception-free.

Under OOM the catch absorbs the failure, and any descendants still
attached to `children` are destroyed via the recursive unique_ptr
chain as this frame unwinds — which risks stack overflow on a very
deep tree, but only when memory is already exhausted, and is strictly
better than `std::terminate`.

Clears 2 CRITICAL bug findings on `main`:
- cpp/src/branch_and_bound/mip_node.hpp:49
- cpp/src/branch_and_bound/mip_node.hpp:54

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rgsl888prabhu rgsl888prabhu requested a review from a team as a code owner May 15, 2026 17:37
@rgsl888prabhu rgsl888prabhu requested review from kaatish and mlubin May 15, 2026 17:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 34457436-62c0-43df-b73b-5ea0f8284407

📥 Commits

Reviewing files that changed from the base of the PR and between 9dcb39e and c27e8f6.

📒 Files selected for processing (3)
  • cpp/src/branch_and_bound/mip_node.hpp
  • cpp/src/grpc/client/grpc_client.cpp
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu
🚧 Files skipped from review as they are similar to previous changes (3)
  • cpp/src/branch_and_bound/mip_node.hpp
  • cpp/src/grpc/client/grpc_client.cpp
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu

📝 Walkthrough

Walkthrough

Three destructors were made exception-tolerant: mip_node_t’s breadth-first descendant detachment, timing_raii_t’s timing-sample push, and grpc_client_t’s shutdown/join sequence now swallow exceptions so destructors do not throw.

Changes

Destructor exception-safety

Layer / File(s) Summary
mip_node_t destructor wrapper
cpp/src/branch_and_bound/mip_node.hpp
Added logger include and wrapped the breadth-first detached-descendants collection/expansion loop in try { ... } catch(...); std::exception is logged, other exceptions are swallowed; remaining descendants are destroyed via the unique_ptr chain.
timing_raii_t destructor wrapper
cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu
The timing_raii_t destructor now computes duration and calls times_vec_.push_back(...) inside try/catch; std::exception is logged and other exceptions are suppressed so the destructor is non-throwing.
grpc_client_t destructor shutdown
cpp/src/grpc/client/grpc_client.cpp
grpc_client_t::~grpc_client_t() now sets stop_logs_, best-effort cancels active_log_context_ under log_context_mutex_ via TryCancel() (exceptions logged/suppressed), swaps out log_thread_, attempts join() in a try/catch, and falls back to detach() (also try/catch + logging) to avoid termination on join/detach failures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately summarizes the main change: making mip_node_t's destructor exception-safe to address a SonarQube critical bug (cpp:S1048).
Description check ✅ Passed The description clearly explains the problem (push_back throwing std::bad_alloc in destructor causing std::terminate), the solution (wrapping in try/catch), and the tradeoff (possible stack overflow on deep trees under OOM, but better than termination).
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sonar-bug-mip-node-destructor-noexcept

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

@rgsl888prabhu rgsl888prabhu self-assigned this May 15, 2026
@rgsl888prabhu rgsl888prabhu added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels May 15, 2026
@mlubin
Copy link
Copy Markdown
Contributor

mlubin commented May 15, 2026

Do we intent to fail nicely on every potential line that could throw bad_alloc? I doubt this is the only spot that would need to change if that's true.

rgsl888prabhu and others added 2 commits May 15, 2026 13:05
Codebase survey for other destructors that can throw bad_alloc found
the same pattern in fj_cpu.cu's `timing_raii_t::~timing_raii_t`
(push_back into a std::vector<double>). SonarQube missed it because
.cu files aren't covered by its C++ analyzer; addresses the broader
concern from the PR review.

Same try/catch(...) approach. Losing one timing sample under OOM is
acceptable; the alternative is std::terminate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stop_log_streaming() can throw: std::thread::join may throw
std::system_error if the join fails, and std::lock_guard's mutex
acquisition can throw on a mutex error. Wrap the call in
try/catch(...) so the destructor stays exception-free.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

Good question — I shouldn't have framed this as "fail nicely on bad_alloc." The narrower motivation is the destructor contract specifically: since C++11, destructors are implicitly noexcept, so an escaping exception calls std::terminate immediately, with no chance for a handler. In non-destructor code an unhandled bad_alloc can still propagate to a caller that may choose what to do. Rule cpp:S1048 targets only this destructor case.

I also surveyed cpp/{include,src} for other destructors that allocate or otherwise can throw. Results, and what's now in this PR:

File Destructor Status
cpp/src/branch_and_bound/mip_node.hpp mip_node_t Fixed in this PR (push_backbad_alloc)
cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu timing_raii_t Fixed in this PR (same push_back pattern)
cpp/src/grpc/client/grpc_client.cpp grpc_client_t (via stop_log_streaming) Fixed in this PR (thread::joinsystem_error, lock_guard ctor)
cpp/src/utilities/high_res_timer.hpp HighResTimer Body is empty — safe

SonarQube only flagged mip_node_t because .cu files aren't covered by its C++ analyzer, and it doesn't reason about thread::join propagating from the called stop_log_streaming(). That's the gap your question implicitly pointed at.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6e1fa19c-ebee-4801-9e5f-7248286561d0

📥 Commits

Reviewing files that changed from the base of the PR and between b08429f and fd66d5a.

📒 Files selected for processing (1)
  • cpp/src/grpc/client/grpc_client.cpp

Comment thread cpp/src/grpc/client/grpc_client.cpp
@mlubin mlubin requested review from Kh4ster and nguidotti and removed request for mlubin May 15, 2026 18:29
@mlubin
Copy link
Copy Markdown
Contributor

mlubin commented May 15, 2026

I'll defer approval to others who have more familiarity with this code.

rgsl888prabhu and others added 2 commits May 18, 2026 20:59
The previous version wrapped stop_log_streaming() in try/catch in the
destructor, but that does not actually prevent std::terminate:

  1. If lock_guard or TryCancel throw before the swap, log_thread_
     still owns a joinable std::thread; when the surrounding
     grpc_client_t destructor finishes and log_thread_ is destroyed,
     a joinable thread's destructor calls std::terminate — outside the
     catch block.

  2. If t->join() throws, the local unique_ptr<thread> t is destroyed
     during the same unwind. Same problem — joinable thread destructor
     terminates before the catch can fire.

Inline a noexcept shutdown in the destructor instead: cancel under a
try/catch, swap log_thread_ into a local (so the member is now empty),
join the local, and on join failure detach the thread before its
destructor runs. The public stop_log_streaming() keeps its existing
contract for the solve_lp / solve_mip callers.

Thanks to CodeRabbit for the catch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rgsl888prabhu rgsl888prabhu requested review from hlinsen and rg20 and removed request for kaatish May 18, 2026 18:01
@rg20
Copy link
Copy Markdown
Contributor

rg20 commented May 18, 2026

Looks like you are catching the exceptions but not reporting them. You can use cuopt_expects and throw RunTimeError.

…catches

Per review on PR #1230: silently catching in destructors hides real
failures. Switch each catch block to log via CUOPT_LOG_ERROR with the
exception's what(), matching the pattern used in solve.cu. The
catch (...) is kept as a noexcept safety net.

(Cannot literally throw RuntimeError as the review suggested — that
would re-introduce the std::terminate path this PR exists to close.
Logging is the closest we can get while satisfying the destructor
noexcept contract.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

@rg20 Good point — silently swallowing hides real failures. Switched each catch in this PR to log via CUOPT_LOG_ERROR("…: %s", e.what()), matching the pattern used in cpp/src/mip_heuristics/solve.cu (commit c27e8f6a).

One thing I could not literally do: throw RuntimeError from these catch blocks. Re-throwing out of a destructor would re-introduce the exact std::terminate path this PR exists to close (destructors are implicitly noexcept since C++11, so any escaping exception aborts the process). Logging is the closest we can get while keeping the destructor exception-free. If you’d rather we also surface these via cuopt_expects somewhere upstream of the destructor — e.g., add a noexcept teardown helper that callers like solve_lp/solve_mip use, and have that throw — happy to do that as a follow-up.

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

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants