diff --git a/core/include/userver/utils/periodic_task.hpp b/core/include/userver/utils/periodic_task.hpp index 22ba3147ae93..701d81d0c25a 100644 --- a/core/include/userver/utils/periodic_task.hpp +++ b/core/include/userver/utils/periodic_task.hpp @@ -62,6 +62,15 @@ class PeriodicTask final { /// Configuration parameters for PeriodicTask. struct Settings final { + struct TimePoint { + std::uint8_t hour; + std::uint8_t minute; + + bool operator==(const TimePoint& other) const { + return std::tie(hour, minute) == std::tie(other.hour, other.minute); + } + }; + static constexpr uint8_t kDistributionPercent = 25; constexpr /*implicit*/ Settings( @@ -91,6 +100,12 @@ class PeriodicTask final { UASSERT(distribution_percent <= 100); } + constexpr Settings( + TimePoint time_point, + logging::Level span_level = logging::Level::kInfo + ) + : strong_mode_anchor(time_point), span_level(span_level) {} + template constexpr /*implicit*/ Settings(std::chrono::duration period) : Settings(period, kDistributionPercent, {}, logging::Level::kInfo) {} @@ -110,6 +125,10 @@ class PeriodicTask final { /// `(period +/- distribution) - time of previous execution` std::chrono::milliseconds distribution{}; + /// @brief Time point for the task execution. Task is repeated every + /// hour:minute UTC of the current/next day + std::optional strong_mode_anchor; + /// @brief Used instead of `period` in case of exception, if set. std::optional exception_period; diff --git a/core/src/utils/periodic_task.cpp b/core/src/utils/periodic_task.cpp index da947d48e3f8..28eb3c3c0fa7 100644 --- a/core/src/utils/periodic_task.cpp +++ b/core/src/utils/periodic_task.cpp @@ -22,8 +22,8 @@ namespace { auto TieSettings(const PeriodicTask::Settings& settings) { // Can't use Boost.Pfr, because Settings has custom constructors. - const auto& [f1, f2, f3, f4, f5, f6] = settings; - return std::tie(f1, f2, f3, f4, f5, f6); + const auto& [f1, f2, f3, f4, f5, f6, f7] = settings; + return std::tie(f1, f2, f3, f4, f5, f6, f7); } } // namespace @@ -156,7 +156,21 @@ void PeriodicTask::Run() { if (!no_exception) period = exception_period; std::chrono::steady_clock::time_point start; - if (settings->flags & Flags::kStrong) { + + if (settings->strong_mode_anchor) { + const auto now = utils::datetime::WallCoarseClock::now(); + const auto today = std::chrono::time_point_cast>>(now); + + const auto [hours, minutes] = settings->strong_mode_anchor.value(); + auto anchor_time_point = today + std::chrono::hours(hours) + std::chrono::minutes(minutes); + if (now > anchor_time_point) { + anchor_time_point += std::chrono::hours(24); + } + + start = std::chrono::steady_clock::now() + + std::chrono::duration_cast(anchor_time_point - now); + } + else if (settings->flags & Flags::kStrong) { start = before; } else { start = std::chrono::steady_clock::now(); diff --git a/core/src/utils/periodic_task_test.cpp b/core/src/utils/periodic_task_test.cpp index fba863a8fd20..4b86e482f462 100644 --- a/core/src/utils/periodic_task_test.cpp +++ b/core/src/utils/periodic_task_test.cpp @@ -157,6 +157,47 @@ UTEST(PeriodicTask, Strong) { task.Stop(); } +UTEST(PeriodicTask, StrongModeAnchor) { + SimpleTaskData simple; + + simple.sleep = 6s; + + const std::chrono::system_clock::time_point tp = std::chrono::system_clock::now(); + + auto tp_days = std::chrono::time_point_cast>>(tp); + + auto tod = tp - tp_days; + + auto h = std::chrono::duration_cast(tod); + tod -= h; + auto m = std::chrono::duration_cast(tod); + + auto hour = static_cast(h.count()); + auto minute = static_cast(m.count()); + + minute++; + if (minute >= 60) { + minute = 0; + hour++; + if (hour >= 24) { + hour = 0; + } + } + + utils::PeriodicTask::Settings::TimePoint mode_anchor{ + .hour = hour, + .minute = minute + }; + utils::PeriodicTask task( + "task", + utils::PeriodicTask::Settings(mode_anchor), + simple.GetTaskFunction() + ); + EXPECT_TRUE(simple.WaitFor(simple.sleep * kSlowRatio, [&] { return simple.GetCount() == 1; })); + + task.Stop(); +} + UTEST(PeriodicTask, Now) { SimpleTaskData simple;