Make mip_node_t destructor exception-safe (SonarQube CRIT BUG cpp:S1048)#1230
Make mip_node_t destructor exception-safe (SonarQube CRIT BUG cpp:S1048)#1230rgsl888prabhu wants to merge 3 commits into
Conversation
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>
📝 WalkthroughWalkthroughThree 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 stop_log_streaming call are now wrapped in catch-all handlers so exceptions (e.g., OOM or join/lock failures) won’t propagate from destructors. ChangesDestructor exception-safety
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
Do we intent to fail nicely on every potential line that could throw |
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>
|
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 I also surveyed
SonarQube only flagged |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
cpp/src/grpc/client/grpc_client.cpp
| grpc_client_t::~grpc_client_t() | ||
| { | ||
| // stop_log_streaming() can throw via std::thread::join (std::system_error) | ||
| // and std::lock_guard's mutex acquisition. Keep the destructor exception-free. | ||
| try { | ||
| stop_log_streaming(); | ||
| } catch (...) { | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="cpp/src/grpc/client/grpc_client.cpp"
echo "=== Destructor wrapper ==="
sed -n '130,140p' "$FILE"
echo
echo "=== stop_log_streaming implementation ==="
sed -n '238,257p' "$FILE"Repository: NVIDIA/cuopt
Length of output: 1157
Destructor catch-all cannot prevent std::terminate() on exception paths.
The try-catch wrapper in the destructor (lines 130–138) does not fully protect against process termination. Two exception scenarios remain unsafe:
-
Exception before
std::swap(t, log_thread_)(e.g., atstd::lock_guard<std::mutex>orTryCancel()): The memberlog_thread_still owns the thread. When the destructor exits andlog_thread_is destroyed, if it holds a joinable thread, its destructor callsstd::terminate()— occurring outside the catch block. -
Exception at
t->join(): During stack unwinding, the localstd::unique_ptr<std::thread> tis destroyed before the exception exitsstop_log_streaming(). If the thread is joinable,std::terminate()is invoked during unwinding, before the catch block can suppress it.
The suggested fix—a stop_log_streaming_noexcept() method that guarantees no exceptions and no joinable thread ownership on any exit path—is the correct approach to satisfy the exception-safe destructor requirement.
|
I'll defer approval to others who have more familiarity with this code. |
Summary
The iterative-teardown destructor in
mip_node_tusesstd::vector::push_backto walk and detach the subtree.push_backcan throwstd::bad_alloc, and a destructor that lets an exception propagate callsstd::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 tochildrenare destroyed via the recursiveunique_ptrchain as this frame unwinds — which risks stack overflow on a very deep tree under OOM, but is strictly better thanstd::terminate.