Skip to content
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

Add SignalHandlerOptions, deprecate start_with_signal_handler #595

Merged
merged 1 commit into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
- Implemented a workaround to resolve false positive warnings from `clang-tidy` on Windows.
- Added a new `create_or_get_logger` overload that accepts a `std::vector<std::shared_ptr<Sink>>`, improving flexibility
by allowing a variable number of sinks to be passed at runtime when creating a logger.
- Introduced `SignalHandlerOptions` to simplify and unify the API. `Backend::start_with_signal_handler` is now
deprecated,
replaced by a new `Backend::start` overload that accepts `SignalHandlerOptions` for enabling signal handling.

## v7.2.2

Expand Down Expand Up @@ -118,7 +121,8 @@
introducing support for custom buffer sizes in file streams and tuning `transit_events_soft_limit`
and `transit_events_hard_limit` default values
- Improved readability of queue allocation notification messages. Capacities are now displayed in KiB,
e.g., `20:59:25 Quill INFO: Allocated a new SPSC queue with a capacity of 1024 KB (previously 512 KB) from thread 31158`.
e.g.,
`20:59:25 Quill INFO: Allocated a new SPSC queue with a capacity of 1024 KB (previously 512 KB) from thread 31158`.

**New Features:**

Expand Down
2 changes: 1 addition & 1 deletion docs/frontend_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ For example, to use a `BoundedDropping` queue with a fixed capacity of `131'072`
{
// Start the backend thread
quill::BackendOptions backend_options;
quill::Backend::start(backend_options); // or quill::Backend::start_with_signal_handler<CustomFrontendOptions>();
quill::Backend::start(backend_options); // or quill::Backend::start<CustomFrontendOptions>(backend_options, signal_handler_options);

// All frontend operations and Logger must utilize the CustomFrontend instead of quill::Frontend
auto console_sink = CustomFrontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
Expand Down
2 changes: 1 addition & 1 deletion docs/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ However, in the event of an application crash, some log messages may be lost.

To prevent message loss during crashes caused by signal interrupts, users should set up a signal handler and invoke :cpp:func:`LoggerImpl::flush_log` within it.

The library provides a built-in signal handler that ensures crash-safe behavior, which can be enabled via :cpp:func:`Backend::start_with_signal_handler`.
The library provides a built-in signal handler that ensures crash-safe behavior, which can be enabled via passing :cpp:struct:`SignalHandlerOptions` to :cpp:func:`Backend::start`.

Log Messages Timestamp Order
----------------------------
Expand Down
6 changes: 6 additions & 0 deletions docs/users-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ BackendTscClock Class
.. doxygenclass:: BackendTscClock
:members:

SignalHandler Options
---------------

.. doxygenstruct:: SignalHandlerOptions
:members:

Frontend Options
----------------

Expand Down
2 changes: 1 addition & 1 deletion examples/custom_frontend_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ int main()
{
// Start the backend thread
quill::BackendOptions backend_options;
quill::Backend::start(backend_options); // or quill::Backend::start_with_signal_handler<CustomFrontendOptions>();
quill::Backend::start(backend_options); // or quill::Backend::start<CustomFrontendOptions>(backend_options, signal_handler_options);

// All frontend operations must utilize CustomFrontend instead of quill::Frontend
auto console_sink = CustomFrontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
Expand Down
2 changes: 1 addition & 1 deletion examples/signal_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ int main()
// Start the logging backend thread with a signal handler
// On Linux/Macos one signal handler is set to handle POSIX style signals
// On Windows an exception handler and a Ctrl-C handler is set.
quill::Backend::start_with_signal_handler<quill::FrontendOptions>();
quill::Backend::start<quill::FrontendOptions>(quill::BackendOptions{}, quill::SignalHandlerOptions{});

auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
quill::Logger* logger = quill::Frontend::create_or_get_logger("root", std::move(console_sink));
Expand Down
60 changes: 31 additions & 29 deletions include/quill/Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,18 @@ class Backend
std::atexit([]() { detail::BackendManager::instance().stop_backend_thread(); });
});
}

/**
* Starts the backend thread and initialises a signal handler
*
* @param options Backend options to configure the backend behavior.
* @param catchable_signals List of signals that the backend should catch if with_signal_handler
* @param backend_options Backend options to configure the backend behavior.
* @param signal_handler_options SignalHandler options to configure the signal handler behavior.
* is enabled.
* @param signal_handler_timeout_seconds This variable defines the timeout duration in seconds for
* the signal handler alarm. It is only available on Linux, as Windows does not support the alarm
* function. The signal handler sets up an alarm to ensure that the process will terminate if it
* does not complete within the specified time frame. This is particularly useful to prevent the
* process from hanging indefinitely in case the signal handler encounters an issue.
* @param signal_handler_logger The logger instance that the signal handler will use to log errors when the application crashes.
* The logger is accessed by the signal handler and must be created by your application using Frontend::create_or_get_logger(...).
* If the specified logger is not found, or if this parameter is left empty, the signal handler will default to using the first valid logger it finds.
*
* @note When using the SignalHandler on Linux/MacOS, ensure that each spawned thread in your
* application has performed one of the following actions:
* i) Logged at least once.
* ii) Called Frontend::preallocate().
* iii) Blocked signals on that thread to prevent the signal handler from running on it.
* or ii) Called Frontend::preallocate().
* or iii) Blocked signals on that thread to prevent the signal handler from running on it.
* This requirement is because the built-in signal handler utilizes a lock-free queue to issue log
* statements and await the log flushing. The queue is constructed on its first use with `new()`.
* Failing to meet any of the above criteria means the queue was never used, and it will be
Expand All @@ -78,17 +69,13 @@ class Backend
* safe.
*/
template <typename TFrontendOptions>
QUILL_ATTRIBUTE_COLD static void start_with_signal_handler(
BackendOptions const& options = BackendOptions{},
QUILL_MAYBE_UNUSED std::initializer_list<int> const& catchable_signals =
std::initializer_list<int>{SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV},
uint32_t signal_handler_timeout_seconds = 20u, std::string const& signal_handler_logger = {})
QUILL_ATTRIBUTE_COLD static void start(BackendOptions const& backend_options,
SignalHandlerOptions const& signal_handler_options)
{
std::call_once(detail::BackendManager::instance().get_start_once_flag(),
[options, catchable_signals, signal_handler_timeout_seconds, signal_handler_logger]()
[backend_options, signal_handler_options]()
{
#if defined(_WIN32)
(void)catchable_signals;
detail::init_exception_handler<TFrontendOptions>();
#else
// We do not want signal handler to run in the backend worker thread
Expand All @@ -97,16 +84,16 @@ class Backend
sigset_t set, oldset;
sigfillset(&set);
sigprocmask(SIG_SETMASK, &set, &oldset);
detail::init_signal_handler<TFrontendOptions>(catchable_signals);
detail::init_signal_handler<TFrontendOptions>(signal_handler_options.catchable_signals);
#endif

// Run the backend worker thread, we wait here until the thread enters the main loop
detail::BackendManager::instance().start_backend_thread(options);
detail::BackendManager::instance().start_backend_thread(backend_options);

detail::SignalHandlerContext::instance().logger_name = signal_handler_logger;
detail::SignalHandlerContext::instance().logger_name = signal_handler_options.logger;

detail::SignalHandlerContext::instance().signal_handler_timeout_seconds.store(
signal_handler_timeout_seconds);
signal_handler_options.timeout_seconds);

// We need to update the signal handler with some backend thread details
detail::SignalHandlerContext::instance().backend_thread_id.store(
Expand All @@ -115,8 +102,8 @@ class Backend
#if defined(_WIN32)
// nothing to do
#else
// Unblock signals in the main thread so subsequent threads do not inherit the blocked mask
sigprocmask(SIG_SETMASK, &oldset, nullptr);
// Unblock signals in the main thread so subsequent threads do not inherit the blocked mask
sigprocmask(SIG_SETMASK, &oldset, nullptr);
#endif

// Set up an exit handler to call stop when the main application exits.
Expand All @@ -126,6 +113,21 @@ class Backend
});
}

/***/
template <typename TFrontendOptions>
[[deprecated(
"This function is deprecated and will be removed in the next version. Use "
"start(BackendOptions, SignalHandlerOptions) instead ")]] QUILL_ATTRIBUTE_COLD static void
start_with_signal_handler(BackendOptions const& options = BackendOptions{},
QUILL_MAYBE_UNUSED std::initializer_list<int> const& catchable_signals =
std::initializer_list<int>{SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV},
uint32_t signal_handler_timeout_seconds = 20u,
std::string const& signal_handler_logger = {})
{
SignalHandlerOptions sho{catchable_signals, signal_handler_timeout_seconds, signal_handler_logger};
start<TFrontendOptions>(options, sho);
}

/**
* Stops the backend thread.
* @note thread-safe
Expand Down Expand Up @@ -193,7 +195,7 @@ class Backend
* multiple threads calling `poll()` simultaneously.
* - The built-in signal handler is not set up with `ManualBackendWorker`. If signal handling is
* required, you must manually set up the signal handler and block signals from reaching the `ManualBackendWorker` thread.
* See the `start_with_signal_handler()` implementation for guidance on how to do this.
* See the `start<FrontendOptions>(BackendOptions, SignalHandlerOptions)` implementation for guidance on how to do this.
* - The following options are not supported when using `ManualBackendWorker`: `cpu_affinity`,
* `thread_name`, `sleep_duration`, and `enable_yield_when_idle`.
* - Avoid performing very heavy tasks in your custom thread. Significant delays in calling `poll()`
Expand Down Expand Up @@ -227,8 +229,8 @@ class Backend
{
QUILL_THROW(
QuillError{"acquire_manual_backend_worker() can only be called once per process. "
"Additionally, it should "
"not be called when start() or start_with_signal_handler() has been invoked"});
"Additionally, it should not be "
"called when start() has already been invoked"});
}

return manual_backend_worker;
Expand Down
33 changes: 31 additions & 2 deletions include/quill/backend/SignalHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@

QUILL_BEGIN_NAMESPACE

/**
* Struct to hold options for the signal handler.
*/
struct SignalHandlerOptions
{
/**
* List of signals that the backend should catch if with_signal_handler is enabled.
*/
std::vector<int> catchable_signals{SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV};

/**
* Defines the timeout duration in seconds for the signal handler alarm.
* It is only available on Linux, as Windows does not support the alarm function.
* The signal handler sets up an alarm to ensure that the process will terminate if it does not
* complete within the specified time frame. This is particularly useful to prevent the
* process from hanging indefinitely in case the signal handler encounters an issue.
*/
uint32_t timeout_seconds = 20u;

/**
* The logger instance that the signal handler will use to log errors when the application crashes.
* The logger is accessed by the signal handler and must be created by your application using
* Frontend::create_or_get_logger(...).
* If the specified logger is not found, or if this parameter is left empty, the signal handler
* will default to using the first valid logger it finds.
*/
std::string logger;
};

namespace detail
{
/***/
Expand Down Expand Up @@ -343,7 +372,7 @@ void init_exception_handler()
* @param catchable_signals the signals we are catching
*/
template <typename TFrontendOptions>
void init_signal_handler(std::initializer_list<int32_t> const& catchable_signals = std::initializer_list<int>{
void init_signal_handler(std::vector<int> const& catchable_signals = std::vector<int>{
SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV})
{
for (auto const& catchable_signal : catchable_signals)
Expand Down Expand Up @@ -373,7 +402,7 @@ inline void on_alarm(int32_t signal_number)
}

template <typename TFrontendOptions>
void init_signal_handler(std::initializer_list<int32_t> const& catchable_signals)
void init_signal_handler(std::vector<int> const& catchable_signals)
{
for (auto const& catchable_signal : catchable_signals)
{
Expand Down
4 changes: 2 additions & 2 deletions test/integration_tests/SignalHandlerLoggerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ TEST_CASE("signal_handler_logger")

// Start the logging backend thread, we expect the signal handler to catch the signal,
// flush the log and raise the signal back
Backend::start_with_signal_handler<FrontendOptions>(
BackendOptions{}, std::initializer_list<int>{SIGABRT}, 40, logger_name_b);
Backend::start<FrontendOptions>(
BackendOptions{}, SignalHandlerOptions{std::vector<int>{SIGABRT}, 40, logger_name_b});

// For testing purposes we want to keep the application running, we do not reraise the signal
detail::SignalHandlerContext::instance().should_reraise_signal.store(false);
Expand Down
6 changes: 4 additions & 2 deletions test/integration_tests/SignalHandlerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ TEST_CASE("signal_handler")

// Start the logging backend thread, we expect the signal handler to catch the signal,
// flush the log and raise the signal back
Backend::start_with_signal_handler<FrontendOptions>(BackendOptions{},
std::initializer_list<int>{SIGABRT}, 40);
SignalHandlerOptions sho{};
sho.catchable_signals = std::vector<int>{SIGABRT};
sho.timeout_seconds = 40;
Backend::start<FrontendOptions>(BackendOptions{}, sho);

// For testing purposes we want to keep the application running, we do not reraise the signal
detail::SignalHandlerContext::instance().should_reraise_signal.store(false);
Expand Down