diff --git a/include/common/EasyAssert.h b/include/common/EasyAssert.h index bf489d2..2f96dc4 100644 --- a/include/common/EasyAssert.h +++ b/include/common/EasyAssert.h @@ -130,11 +130,32 @@ FailureCStatus(int code, const std::string& msg) { return CStatus{code, strdup(msg.data())}; } +// Observability hook for the cgo boundary: fired whenever FailureCStatus +// receives an exception that is NOT a SegcoreError (so its typed error code is +// lost and the failure collapses to UnexpectedError). The consumer (milvus) +// registers an observer that bumps a metric / logs; a shrinking hit rate means +// explicit error classification coverage is improving. This header stays free +// of any metrics dependency -- same observer pattern as the Go-side +// RegisterUnmappedSegcoreCodeObserver. +using UntypedCgoExceptionObserver = void (*)(const char* what); + +void +RegisterUntypedCgoExceptionObserver(UntypedCgoExceptionObserver observer); + +namespace impl { +// Best-effort, noexcept: runs inside FailureCStatus's conversion path, so a +// throwing observer must never replace the original failure or escape the +// cgo boundary. +void +NotifyUntypedCgoException(const char* what) noexcept; +} // namespace impl + inline CStatus FailureCStatus(const std::exception* ex) { if (auto segcore_err = dynamic_cast(ex)) { return CStatus{static_cast(segcore_err->get_error_code()), strdup(segcore_err->what())}; } + impl::NotifyUntypedCgoException(ex->what()); return CStatus{static_cast(UnexpectedError), strdup(ex->what())}; } diff --git a/src/cachinglayer/lrucache/ListNode.cpp b/src/cachinglayer/lrucache/ListNode.cpp index 033979b..4ce5b0e 100644 --- a/src/cachinglayer/lrucache/ListNode.cpp +++ b/src/cachinglayer/lrucache/ListNode.cpp @@ -261,8 +261,11 @@ ListNode::mark_unload(std::function cb) { // NOTE: it should not happen, but if it does, we should not make the program deadlock. auto promise = std::move(load_promise_); lock.unlock(); - promise->setException(folly::exception_wrapper( - std::runtime_error("ListNode destroyed while loading, this should not happen"))); + // Wrap a typed SegcoreError (internal invariant, not a + // cancellation): a bare runtime_error rethrown from this + // promise cannot be recognized at the cgo boundary. + promise->setException(folly::exception_wrapper(milvus::SegcoreError( + milvus::ErrorCode::UnexpectedError, "ListNode destroyed while loading, this should not happen"))); } break; } diff --git a/src/common/EasyAssert.cpp b/src/common/EasyAssert.cpp index 744b49f..1ff7a04 100644 --- a/src/common/EasyAssert.cpp +++ b/src/common/EasyAssert.cpp @@ -16,12 +16,43 @@ #include "common/EasyAssert.h" +#include #include #include #include #include "fmt/format.h" +namespace milvus { + +namespace { +std::atomic untyped_cgo_exception_observer{nullptr}; +} // namespace + +void +RegisterUntypedCgoExceptionObserver(UntypedCgoExceptionObserver observer) { + untyped_cgo_exception_observer.store(observer, std::memory_order_release); +} + +namespace impl { +void +NotifyUntypedCgoException(const char* what) noexcept { + if (auto observer = untyped_cgo_exception_observer.load(std::memory_order_acquire)) { + try { + observer(what); + } catch (...) { + // The observer is metrics/logging only. It runs inside + // FailureCStatus's exception-to-CStatus conversion, so a throwing + // observer must never replace the original failure or let an + // exception escape the cgo boundary (which would terminate the + // process). Swallow and carry on with the conversion. + } + } +} +} // namespace impl + +} // namespace milvus + namespace milvus::impl { std::string