libdisplaydevice master
C++ library to modify display devices.
retry_scheduler.h
Go to the documentation of this file.
1
5#pragma once
6
7// system includes
8#include <algorithm>
9#include <condition_variable>
10#include <exception>
11#include <functional>
12#include <memory>
13#include <mutex>
14#include <stdexcept>
15#include <thread>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
20// local includes
21#include "logging.h"
22
23namespace display_device {
31 class SchedulerStopToken final {
32 public:
37 explicit SchedulerStopToken(std::function<void()> cleanup);
38
43
48
52 ~SchedulerStopToken() noexcept;
53
57 void requestStop();
58
63 [[nodiscard]] bool stopRequested() const;
64
65 private:
66 bool m_stop_requested {false};
67 std::function<void()> m_cleanup;
68 };
69
70 namespace detail {
76 inline void logSchedulerException(const std::exception &exception, const char *message) {
77 DD_LOG(error) << message << " Error:\n"
78 << exception.what();
79 }
80
85 template<class FunctionT>
86 concept OptionalFunction = requires(FunctionT exec_fn) {
87 static_cast<bool>(exec_fn);
88 };
89
95 template<class T, bool AddConst>
96 struct AutoConst;
97
102 template<class T>
103 struct AutoConst<T, false> {
107 using type = T;
108 };
109
114 template<class T>
115 struct AutoConst<T, true> {
119 using type = std::add_const_t<T>;
120 };
121
127 template<class T, bool AddConst>
129
134 template<class T, class FunctionT>
135 concept ExecuteWithoutStopToken = requires(FunctionT exec_fn, T &value) { exec_fn(value); };
136
141 template<class T, class FunctionT>
142 concept ExecuteWithStopToken = requires(FunctionT exec_fn, T &value, SchedulerStopToken &token) {
143 exec_fn(value, token);
144 };
145
149 template<class T, class FunctionT>
151 } // namespace detail
152
160 enum class Execution {
161 Immediate,
164 };
165
166 std::vector<std::chrono::milliseconds> m_sleep_durations;
168 };
169
176 template<class T>
177 class RetryScheduler final {
178 public:
183 explicit RetryScheduler(std::unique_ptr<T> iface):
184 m_iface {iface ? std::move(iface) : throw std::invalid_argument {"Nullptr interface provided in RetryScheduler!"}},
185 m_thread {[this]() {
186 runThreadLoop();
187 }} {
188 }
189
194 {
195 std::lock_guard lock {m_mutex};
196 m_keep_alive = false;
197 syncThreadUnlocked();
198 }
199
200 if (m_thread.joinable()) {
201 m_thread.join();
202 }
203 }
204
223 void schedule(std::function<void(T &, SchedulerStopToken &stop_token)> exec_fn, const SchedulerOptions &options) {
224 if (!exec_fn) {
225 throw std::invalid_argument {"Empty callback function provided in RetryScheduler::schedule!"};
226 }
227
228 if (options.m_sleep_durations.empty()) {
229 throw std::invalid_argument {"At least 1 sleep duration must be specified in RetryScheduler::schedule!"};
230 }
231
232 if (std::ranges::any_of(options.m_sleep_durations, [&](const auto &duration) {
233 return duration == std::chrono::milliseconds::zero();
234 })) {
235 throw std::invalid_argument {"All of the durations specified in RetryScheduler::schedule must be larger than a 0!"};
236 }
237
238 std::lock_guard lock {m_mutex};
239 SchedulerStopToken stop_token {[this]() {
240 stopUnlocked();
241 }};
242
243 // We are catching the exception here instead of propagating to have
244 // similar try...catch login as in the scheduler thread.
245 try {
246 auto sleep_durations = options.m_sleep_durations;
249 std::this_thread::sleep_for(takeNextDuration(sleep_durations));
250 }
251
252 exec_fn(*m_iface, stop_token);
253 }
254
255 if (!stop_token.stopRequested()) {
256 m_retry_function = std::move(exec_fn);
257 m_sleep_durations = std::move(sleep_durations);
258 syncThreadUnlocked();
259 }
260 } catch (const std::exception &error) { // NOSONAR(cpp:S1181): Scheduler callback boundary must catch standard callback failures.
261 stop_token.requestStop();
262 detail::logSchedulerException(error, "Exception thrown in the RetryScheduler::schedule. Stopping scheduler.");
263 }
264 }
265
272 template<class FunctionT>
273 auto execute(FunctionT &&exec_fn) {
274 return executeImpl(*this, std::forward<FunctionT>(exec_fn));
275 }
276
283 template<class FunctionT>
284 auto execute(FunctionT &&exec_fn) const {
285 return executeImpl(*this, std::forward<FunctionT>(exec_fn));
286 }
287
292 [[nodiscard]] bool isScheduled() const {
293 return static_cast<bool>(m_retry_function);
294 }
295
299 void stop() {
300 std::lock_guard lock {m_mutex};
301 stopUnlocked();
302 }
303
304 private:
305 static std::chrono::milliseconds takeNextDuration(std::vector<std::chrono::milliseconds> &durations) {
306 if (durations.size() > 1) {
307 const auto front_it {std::begin(durations)};
308 const auto front_value {*front_it};
309 durations.erase(front_it);
310 return front_value;
311 }
312
313 return durations.empty() ? std::chrono::milliseconds::zero() : durations.back();
314 }
315
352 template<class SelfT, class FunctionT>
353 requires detail::ExecuteCallbackLike<T, FunctionT>
354 static auto executeImpl(SelfT &self, FunctionT &&exec_fn) {
355 constexpr bool IsConst = std::is_const_v<std::remove_reference_t<SelfT>>;
356
357 if constexpr (detail::OptionalFunction<FunctionT>) {
358 if (!exec_fn) {
359 throw std::invalid_argument {"Empty callback function provided in RetryScheduler::execute!"};
360 }
361 }
362
363 std::lock_guard lock {self.m_mutex};
364 detail::auto_const_t<std::decay_t<T>, IsConst> &iface_ref {*self.m_iface};
365 if constexpr (detail::ExecuteWithStopToken<T, FunctionT>) {
366 detail::auto_const_t<SchedulerStopToken, IsConst> stop_token {makeStopCallback(self)};
367 return std::forward<FunctionT>(exec_fn)(iface_ref, stop_token);
368 } else {
369 return std::forward<FunctionT>(exec_fn)(iface_ref);
370 }
371 }
372
373 template<class SelfT>
374 static auto makeStopCallback(SelfT &self) {
375 constexpr bool IsConst = std::is_const_v<std::remove_reference_t<SelfT>>;
376 if constexpr (IsConst) {
377 return []() {
378 // Const execute calls cannot stop the scheduler.
379 };
380 } else {
381 return [&self]() {
382 self.stopUnlocked();
383 };
384 }
385 }
386
387 void runThreadLoop() {
388 std::unique_lock lock {m_mutex};
389 while (m_keep_alive) {
390 m_syncing_thread = false;
391 waitForNextRetry(lock);
392
393 if (m_syncing_thread) {
394 // Thread was waken up to sync sleep time or to be stopped.
395 continue;
396 }
397
398 try {
399 SchedulerStopToken scheduler_stop_token {[this]() {
400 clearThreadLoopUnlocked();
401 }};
402 m_retry_function(*m_iface, scheduler_stop_token);
403 continue;
404 } catch (const std::exception &error) { // NOSONAR(cpp:S1181): Scheduler callback boundary must catch standard callback failures.
405 detail::logSchedulerException(error, "Exception thrown in the RetryScheduler thread. Stopping scheduler.");
406 }
407
408 clearThreadLoopUnlocked();
409 }
410 }
411
412 void waitForNextRetry(std::unique_lock<std::mutex> &lock) {
413 if (const auto duration {takeNextDuration(m_sleep_durations)}; duration > std::chrono::milliseconds::zero()) {
414 // We're going to sleep until manually woken up or the time elapses.
415 m_sleep_cv.wait_for(lock, duration, [this]() {
416 return m_syncing_thread;
417 });
418 return;
419 }
420
421 // We're going to sleep until manually woken up.
422 m_sleep_cv.wait(lock, [this]() {
423 return m_syncing_thread;
424 });
425 }
426
430 void clearThreadLoopUnlocked() {
431 m_sleep_durations = {};
432 m_retry_function = nullptr;
433 }
434
438 void syncThreadUnlocked() {
439 m_syncing_thread = true;
440 m_sleep_cv.notify_one();
441 }
442
446 void stopUnlocked() {
447 if (isScheduled()) {
448 clearThreadLoopUnlocked();
449 syncThreadUnlocked();
450 }
451 }
452
453 std::unique_ptr<T> m_iface;
454 std::vector<std::chrono::milliseconds> m_sleep_durations;
455 std::function<void(T &, SchedulerStopToken &)> m_retry_function {nullptr};
456
457 mutable std::mutex m_mutex {};
458 std::condition_variable m_sleep_cv {};
459 bool m_syncing_thread {false};
460 bool m_keep_alive {true};
461
462 // Always the last in the list so that all the members are already initialized!
463 std::thread m_thread; /* NOSONAR(cpp:S6168): std::jthread is unavailable on the macOS libc++ used by CI. */
464 };
465} // namespace display_device
A wrapper class around an interface that provides a thread-safe access to the interface and allows to...
Definition retry_scheduler.h:177
auto execute(FunctionT &&exec_fn)
A non-const variant of the executeImpl method. See it for details.
Definition retry_scheduler.h:273
RetryScheduler(std::unique_ptr< T > iface)
Default constructor.
Definition retry_scheduler.h:183
void schedule(std::function< void(T &, SchedulerStopToken &stop_token)> exec_fn, const SchedulerOptions &options)
Schedule an interface executor function to be executed at specified intervals.
Definition retry_scheduler.h:223
bool isScheduled() const
Check whether anything is scheduled for execution.
Definition retry_scheduler.h:292
~RetryScheduler()
A destructor that gracefully shuts down the thread.
Definition retry_scheduler.h:193
auto execute(FunctionT &&exec_fn) const
A const variant of the executeImpl method. See it for details.
Definition retry_scheduler.h:284
void stop()
Stop the scheduled function - will no longer be execute once THIS method returns.
Definition retry_scheduler.h:299
A convenience class for stopping the RetryScheduler.
Definition retry_scheduler.h:31
~SchedulerStopToken() noexcept
Executes cleanup logic if scheduler stop was requested.
Definition retry_scheduler.cpp:24
SchedulerStopToken(std::function< void()> cleanup)
Default constructor.
Definition retry_scheduler.cpp:20
SchedulerStopToken & operator=(const SchedulerStopToken &)=delete
Deleted copy operator.
SchedulerStopToken(const SchedulerStopToken &)=delete
Deleted copy constructor.
bool stopRequested() const
Check if stop was requested.
Definition retry_scheduler.cpp:38
void requestStop()
Request the scheduler to be stopped.
Definition retry_scheduler.cpp:34
Check if the function signature matches the acceptable signature for RetryScheduler::execute.
Definition retry_scheduler.h:150
Check if the function signature matches the acceptable signature for RetryScheduler::execute with a s...
Definition retry_scheduler.h:142
Check if the function signature matches the acceptable signature for RetryScheduler::execute without ...
Definition retry_scheduler.h:135
Given that we know that we are dealing with a function, check if it is an optional function (like std...
Definition retry_scheduler.h:86
Declarations for the logging utility.
#define DD_LOG(level)
Helper MACRO that disables output string computation if log level is not enabled.
Definition logging.h:153
typename AutoConst< T, AddConst >::type auto_const_t
A convenience template helper for adding const to the type.
Definition retry_scheduler.h:128
void logSchedulerException(const std::exception &exception, const char *message)
Log an exception thrown by the scheduler or scheduler callback.
Definition retry_scheduler.h:76
Scheduler options to be used when scheduling executor function.
Definition retry_scheduler.h:156
Execution m_execution
Executor's execution logic.
Definition retry_scheduler.h:167
std::vector< std::chrono::milliseconds > m_sleep_durations
Specifies for long the scheduled thread sleeps before invoking executor. Last duration is reused inde...
Definition retry_scheduler.h:166
Execution
Defines the executor's execution logic when it is scheduled.
Definition retry_scheduler.h:160
@ ImmediateWithSleep
The first sleep duration is TAKEN from m_sleep_durations and the calling thread is put to sleep....
@ Immediate
Executor is executed in the calling thread immediately and scheduled afterward.
@ ScheduledOnly
Executor is executed in the thread only.
T type
Original type.
Definition retry_scheduler.h:107
std::add_const_t< T > type
Const-qualified type.
Definition retry_scheduler.h:119
A convenience template struct helper for adding const to the type.
Definition retry_scheduler.h:96