-
Notifications
You must be signed in to change notification settings - Fork 853
Dedicated crash handler thread. #21826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Conversation
/// @brief dedicated crash handler thread | ||
std::unique_ptr<std::thread> crashHandlerThread = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use a std::jthread
instead?
/// @brief atomic variable for coordinating between signal handler and crash | ||
/// handler thread | ||
std::atomic<arangodb::CrashHandlerState> crashHandlerState( | ||
arangodb::CrashHandlerState::IDLE); | ||
|
||
/// @brief dedicated crash handler thread | ||
std::unique_ptr<std::thread> crashHandlerThread = nullptr; | ||
|
||
/// @brief mutex to protect thread creation/destruction | ||
std::mutex crashHandlerThreadMutex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these variables should be tied together, with crashHandlerThreadFunction()
, either into a common struct or a namespace.
lib/CrashHandler/CrashHandler.cpp
Outdated
while (true) { | ||
arangodb::CrashHandlerState state = | ||
crashHandlerState.load(std::memory_order_acquire); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that, since C++20, a std::atomic
has methods wait(T value)
, notify_one()
and notify_all()
. And if I read https://en.cppreference.com/w/cpp/utility/program/signal correctly, calling these from the signal handler should be OK, as long as the atomic is lock free (which it has to be anyway). I think it's covered by "f is a non-static member function invoked on an object obj, such that obj.is_lock_free() yields true". But please double-check if you agree.
That would simplify this code and the wait function in the crash handler, and make it more polite/efficient.
In addition, if we use std::jthread
(see my other comment), we can use stop token handling to address the thread shutdown instead of the atomic. This might be nicer to use (also see my comment on shutdown) and separate the concerns a little.
lib/CrashHandler/CrashHandler.cpp
Outdated
// Signal the dedicated crash handler thread (force trigger even if not | ||
// idle) | ||
::crashHandlerState.store(arangodb::CrashHandlerState::CRASH_DETECTED, | ||
std::memory_order_release); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would vote for both commenting and asserting for everything we do here that we're allowed to do it in the signal handler (or that it's not allowed and we're doing it anyway). So here I'd add
static_assert(decltype(::crashHandlerState)::is_always_lock_free);
or so, and a comment that the store is allowed under this condition.
lib/CrashHandler/CrashHandler.cpp
Outdated
// Log signal-specific information in the signal handler | ||
logCrashInfo("signal handler invoked", signal, info, ucontext); | ||
// Morally, we should do this after the work of the crash handler thread, | ||
// however, for backwards compatibility, we keep it here as first thing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it actually preferable to do it in the crash handler thread as much as possible, to reduce or eliminate the amount of UB we have in the crash handler, at least the actual logging.
// Now log the backtrace from the crashed thread (only the crashed thread | ||
// can do this) | ||
logBacktrace(); | ||
logProcessInfo(); | ||
|
||
// Final cleanup | ||
arangodb::Logger::flush(); | ||
arangodb::Logger::shutdown(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, I think it would be preferable to do this in the crash handler thread as much as possible (would need some refactoring/splitting of logBacktrace()
).
lib/CrashHandler/CrashHandler.h
Outdated
/// @brief shuts down the crash handler thread | ||
static void shutdownCrashHandler(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think its workable to have a function that must be called before each call to exit
or similar. As of now, you've addressed an exit call in arangod.cpp
(there is even a second one there) - and how does this affect the client-tools? And you've addressed to exit()s in the VersionFeature, but it seems we have at least a dozen other places calling exit.
I haven't thought it through, so there may be problems, but some ideas how we might address this:
- If we move the
crashHandlerThread
variable in a struct, we might be able to stop the thread in that thread's destructor - We could install handlers with
std::atexit
andstd::at_quick_exit
(I don't thinkstd::set_terminate
is needed?) - Don't install the thread globally, but with a scoped variable that we put in
runServer
, probably where we callCrashHandler::installCrashHandler()
. It might then make sense to moveinstallCrashHandler()
into the constructor of the object, and maybe even remove the handlers in the destructor - that would clarify exactly in which scope the CrashHandler, including its handlers and thread, is active.
Intuitively I like the third option best, and the second the least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will try to do this as much as possible. Something with static lifetime will be necessary, or else the signal handler (which can be executed any time from any thread) and the std::terminate
handler (which is executed on assertion failure or other catastrophic events, again essentially any time in any thread) do not have access to it. So making it into a totally scoped variable will not be possible.
I will try to avoid the explicit shutdown by additionally creating a scoped CrashHandler
object (which can do the normal initialization and shutdown). But an atexit
handler will be needed anyway, since we have these places which answer to --version
, which use exit
to stop the process right away. In this case the scoped variable is no longer cleaned up properly. Let's see what I can come up with.
lib/CrashHandler/CrashHandler.h
Outdated
/// @brief States for the crash handler thread coordination | ||
enum class CrashHandlerState : int { | ||
IDLE = 0, ///< idle state, waiting for crashes | ||
CRASH_DETECTED = 1, ///< crash detected, handling in progress | ||
HANDLING_COMPLETE = 2, ///< crash handling complete | ||
SHUTDOWN = 3 ///< shutdown requested | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this follows (only) specific state transitions, these should be noted in a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Scope & Purpose
This PR implements a dedicated crash handler thread.
The read signal handler triggers the dedicated thread to
write out useful information before it continues with the crash.
This is useful since a dedicated thread has none of the restrictions
of a signal handler and can for example use system calls.
This allows us to dump a lot more information out at crash time.
Checklist