-
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
lib/CrashHandler/CrashHandler.cpp
Outdated
/// @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?
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, but it does not really make a difference, as far as I see.
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's a better default, and there's generally very little reason to use std::thread
.
Mostly it will be automatically joined on destruction instead of terminating the process, if it hasn't been joined or detached, and it also allows the use of stop tokens/sources.
lib/CrashHandler/CrashHandler.cpp
Outdated
/// @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.
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.
They are collected in a common anonymous 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.
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.
lib/CrashHandler/CrashHandler.cpp
Outdated
// 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()
).
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.
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.
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 as discussed.
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.
std::unique_ptr<char[]> backtraceBuffer; | ||
constexpr size_t backtraceBufferSize = 1024 * 1024; // 1MB |
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.
You could use a std::array
instead of a char[]
. That would encode the length in the type, and thus make it slightly easier to access. Not a necessity though.
And not sure if there's a noteworthy tradeoff, but we could also omit the heap allocation and use static memory instead, e.g.
std::unique_ptr<char[]> backtraceBuffer; | |
constexpr size_t backtraceBufferSize = 1024 * 1024; // 1MB | |
constexpr size_t backtraceBufferSize = 1024 * 1024; // 1MB | |
std::array<char, backtraceBufferSize> backtraceBuffer; |
Do you think it matters? Don't change it unless you prefer it, it's just a thought, I have no preference.
|
||
/// @brief mutex to protect thread creation/destruction | ||
std::mutex crashHandlerThreadMutex; | ||
|
||
// Static variables to store crash data for the dedicated crash handler thread | ||
/// @brief stores the crash context/reason | ||
static char const* crashContext = 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.
Maybe use a std::string_view
instead of a char*
? The callers have one available anyway, as far as I can see.
std::atomic<bool> _threadRunning{false}; | ||
static std::atomic<CrashHandler*> _theCrashHandler; |
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.
Can we omit _threadRunning
, and do a CAS on _theCrashHandler
instead?
// We intentionally do not protect against two signal handlers running | ||
// concurrently. We could do this using atomics, but choose not to | ||
// for simplicity. |
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 would be simple enough to protect against this:
E.g., change crashSignal
to be an atomic, set it first but with a CAS, and set the rest only if that succeeded.
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.
Actually, I've just seen that this is handled by ::crashHandlerInvoked.exchange(true)
in crashHandlerSignalHandler
- so maybe just clarify that in the comment?
SmallString headerBuffer; | ||
headerBuffer.append("Backtrace of thread "); | ||
headerBuffer.appendUInt64(arangodb::Thread::currentThreadNumber()); | ||
headerBuffer.append(" [").append(currentThreadName).append("]\n"); | ||
|
||
LOG_TOPIC("c962b", INFO, arangodb::Logger::CRASH) << buffer.view(); | ||
if (!safeAppend(headerBuffer.view())) { | ||
return totalWritten; | ||
} |
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.
Maybe wrap it in a {} block, to make it clear that we don't add up too many of those 4kb buffers on the stack. Though the optimizer might catch that anyway...
/// in a signal handler. Use `acquireBacktrace` above to get the backtrace | ||
/// in the signal handler and then this function in the dedicated | ||
/// CrashHandler thread to do the actual logging! | ||
void logAcquiredBacktrace(char const* buffer, size_t bufferSize) try { |
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.
Maybe pass a std::string_view
instead of char*
+ size_t
?
size_t pos = 0; | ||
size_t lineStart = 0; | ||
|
||
while (pos <= bufferSize) { | ||
if (pos == bufferSize || buffer[pos] == '\n') { | ||
if (pos > lineStart) { | ||
std::string_view line(buffer + lineStart, pos - lineStart); |
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 you change buffer
into a std::string_view
, you can just do
size_t pos = 0; | |
size_t lineStart = 0; | |
while (pos <= bufferSize) { | |
if (pos == bufferSize || buffer[pos] == '\n') { | |
if (pos > lineStart) { | |
std::string_view line(buffer + lineStart, pos - lineStart); | |
for (auto const line : std::views::split(buffer, "\n")) { |
instead, if you include <ranges>
. :)
} catch (...) { | ||
// could not allocate memory for backtrace buffer. | ||
// this is not critical - we can still work without it | ||
::backtraceBuffer.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.
Maybe add a log message? Seems unlikely, but may confuse us very much without one if it does happen.
@@ -685,6 +840,20 @@ void CrashHandler::installCrashHandler() { | |||
|
|||
CrashHandler::crash(buffer.view()); | |||
}); | |||
|
|||
std::atexit([]() { | |||
CrashHandler* ch = CrashHandler::_theCrashHandler; |
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.
Maybe do a
CrashHandler* ch = CrashHandler::_theCrashHandler; | |
auto* ch = CrashHandler::_theCrashHandler.exchange(nullptr); |
instead? And similarly in std::at_quick_exit
and ~CrashHandler()
. That could avoid races between ~CrashHandler()
and the exit handlers.
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