From e99b049d2b1726cbbe025ea29648f772f0d9d4a6 Mon Sep 17 00:00:00 2001 From: Per Malmberg Date: Tue, 13 Mar 2018 00:46:03 +0100 Subject: [PATCH] Work on DST handling. --- CMakeLists.txt | 5 +- libcron/CMakeLists.txt | 6 +- libcron/Cron.cpp | 87 --------- libcron/Cron.h | 152 +++++++++++++-- libcron/CronClock.h | 12 +- libcron/Task.cpp | 2 +- test/CMakeLists.txt | 7 +- test/CronTest.cpp | 430 ++++++++++++++++++++++++----------------- 8 files changed, 411 insertions(+), 290 deletions(-) delete mode 100644 libcron/Cron.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d0bb848..1c6e4df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.6) +set(OUTPUT_LOCATION ${CMAKE_CURRENT_LIST_DIR}/out/) + add_subdirectory(libcron) add_subdirectory(test) -add_dependencies(cron_test libcron) \ No newline at end of file +add_dependencies(cron_test libcron) + diff --git a/libcron/CMakeLists.txt b/libcron/CMakeLists.txt index 84fd93f..3bc82cc 100644 --- a/libcron/CMakeLists.txt +++ b/libcron/CMakeLists.txt @@ -8,7 +8,6 @@ include_directories(${CMAKE_CURRENT_LIST_DIR}/externals/date/include) add_library(${PROJECT_NAME} Cron.h - Cron.cpp Task.h CronData.h TimeTypes.h @@ -18,3 +17,8 @@ add_library(${PROJECT_NAME} DateTime.h Task.cpp CronClock.h) + +set_target_properties(${PROJECT_NAME} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" + LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" + RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}") \ No newline at end of file diff --git a/libcron/Cron.cpp b/libcron/Cron.cpp deleted file mode 100644 index e007bb6..0000000 --- a/libcron/Cron.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include "Cron.h" - -using namespace std::chrono; - -namespace libcron -{ - bool libcron::Cron::add_schedule( std::string name, const std::string& schedule, std::function work) - { - auto cron = CronData::create(schedule); - bool res = cron.is_valid(); - if (res) - { - - Task t{std::move(name), CronSchedule{cron}, std::move(work)}; - if (t.calculate_next(clock->now())) - { - tasks.push(t); - } - } - - return res; - } - - std::chrono::system_clock::duration Cron::time_until_next() const - { - system_clock::duration d{}; - if (tasks.empty()) - { - d = std::numeric_limits::max(); - } - else - { - d = tasks.top().time_until_expiry(clock->now()); - } - - return d; - } - - size_t Cron::execute_expired_tasks(system_clock::time_point now) - { - std::vector executed{}; - - while(!tasks.empty() - && tasks.top().is_expired(now)) - { - executed.push_back(tasks.top()); - tasks.pop(); - auto& t = executed[executed.size()-1]; - t.execute(); - } - - auto res = executed.size(); - - // Place executed tasks back onto the priority queue. - std::for_each(executed.begin(), executed.end(), [this, &now](Task& task) - { - // Must calculate new schedules using second after 'now', otherwise - // we'll run the same task over and over if it takes less than 1s to execute. - if(task.calculate_next(now + 1s)) - { - tasks.push(task); - } - }); - - print_queue(tasks); - - return res; - } - - void Cron::print_queue(std::priority_queue, std::greater<>> queue) - { - std::vector v{}; - - while( !queue.empty()) - { - auto t = queue.top(); - queue.pop(); - v.push_back(t); - } - - std::for_each(v.begin(), v.end(), [&queue](auto& task){ - queue.push(task); - }); - } - -} \ No newline at end of file diff --git a/libcron/Cron.h b/libcron/Cron.h index 2efbd1a..9cbb953 100644 --- a/libcron/Cron.h +++ b/libcron/Cron.h @@ -9,15 +9,17 @@ namespace libcron { + template + class Cron; + + template + std::ostream& operator<<(std::ostream& stream, const Cron& c); + + template class Cron { public: - explicit Cron(std::unique_ptr clock = std::make_unique()) - : clock(std::move(clock)) - { - } - bool add_schedule(std::string name, const std::string& schedule, std::function work); size_t count() const @@ -25,24 +27,148 @@ namespace libcron return tasks.size(); } + // Tick is expected to be called at least once a second to prevent missing schedules. size_t - execute_expired_tasks() + tick() { - return execute_expired_tasks(clock->now()); + return tick(clock.now()); } size_t - execute_expired_tasks(std::chrono::system_clock::time_point now); + tick(std::chrono::system_clock::time_point now); std::chrono::system_clock::duration time_until_next() const; - std::shared_ptr get_clock() const { return clock; } + ClockType& get_clock() + { + return clock; + } + + friend std::ostream& operator<<<>(std::ostream& stream, const Cron& c); private: - // Priority queue placing smallest (i.e. nearest in time) items on top. - std::priority_queue, std::greater<>> tasks{}; - void print_queue(std::priority_queue, std::greater<>> queue); - std::shared_ptr clock{}; + class Queue + // Priority queue placing smallest (i.e. nearest in time) items on top. + : public std::priority_queue, std::greater<>> + { + public: + // Inherit to allow access to the container. + const std::vector& get_tasks() const + { + return c; + } + + std::vector& get_tasks() + { + return c; + } + }; + + Queue tasks{}; + ClockType clock{}; + bool first_tick = true; + std::chrono::system_clock::time_point last_tick{}; }; + + template + bool Cron::add_schedule(std::string name, const std::string& schedule, std::function work) + { + auto cron = CronData::create(schedule); + bool res = cron.is_valid(); + if (res) + { + + Task t{std::move(name), CronSchedule{cron}, std::move(work)}; + if (t.calculate_next(clock.now())) + { + tasks.push(t); + } + } + + return res; + } + + template + std::chrono::system_clock::duration Cron::time_until_next() const + { + system_clock::duration d{}; + if (tasks.empty()) + { + d = std::numeric_limits::max(); + } + else + { + d = tasks.top().time_until_expiry(clock.now()); + } + + return d; + } + + template + size_t Cron::tick(system_clock::time_point now) + { + size_t res = 0; + + if (first_tick) + { + first_tick = false; + } + else if (now - last_tick < hours{3}) + { + // Reschedule all tasks. + for (auto& t : tasks.get_tasks()) + { + t.calculate_next(now); + } + } + else if(now < last_tick && now >= last_tick - hours{3}) + { + // Prevent tasks from running until the clock has reached current 'last_tick'. + for (auto& t : tasks.get_tasks()) + { + //t.set_back_limit(last_tick); + } + } + + last_tick = now; + + std::vector executed{}; + + while (!tasks.empty() + && tasks.top().is_expired(now)) + { + executed.push_back(tasks.top()); + tasks.pop(); + auto& t = executed[executed.size() - 1]; + t.execute(); + } + + res = executed.size(); + + // Place executed tasks back onto the priority queue. + std::for_each(executed.begin(), executed.end(), [this, &now](Task& task) + { + // Must calculate new schedules using second after 'now', otherwise + // we'll run the same task over and over if it takes less than 1s to execute. + if (task.calculate_next(now + 1s)) + { + tasks.push(task); + } + }); + + return res; + } + + template + std::ostream& operator<<(std::ostream& stream, const Cron& c) + { + std::for_each(c.tasks.get_tasks().cbegin(), c.tasks.get_tasks().cend(), + [&stream, &c](const Task& t) + { + stream << t.get_status(c.clock.now()) << '\n'; + }); + + return stream; + } } \ No newline at end of file diff --git a/libcron/CronClock.h b/libcron/CronClock.h index 71b02c4..f261e92 100644 --- a/libcron/CronClock.h +++ b/libcron/CronClock.h @@ -10,20 +10,20 @@ namespace libcron class ICronClock { public: - virtual std::chrono::system_clock::time_point now() = 0; - virtual std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) = 0; + virtual std::chrono::system_clock::time_point now() const = 0; + virtual std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) const = 0; }; class UTCClock : public ICronClock { public: - std::chrono::system_clock::time_point now() override + std::chrono::system_clock::time_point now() const override { return std::chrono::system_clock::now(); } - std::chrono::seconds utc_offset(std::chrono::system_clock::time_point) override + std::chrono::seconds utc_offset(std::chrono::system_clock::time_point) const override { return 0s; } @@ -33,13 +33,13 @@ namespace libcron : public ICronClock { public: - std::chrono::system_clock::time_point now() override + std::chrono::system_clock::time_point now() const override { auto now = system_clock::now(); return now + utc_offset(now); } - std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) override + std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) const override { auto t = system_clock::to_time_t(now); tm tm{}; diff --git a/libcron/Task.cpp b/libcron/Task.cpp index 6aba733..0171cf8 100644 --- a/libcron/Task.cpp +++ b/libcron/Task.cpp @@ -56,7 +56,7 @@ namespace libcron s+= std::to_string(dt.day) + " "; s+= std::to_string(dt.hour) + ":"; s+= std::to_string(dt.min) + ":"; - s+= std::to_string(dt.sec) + " UTC"; + s+= std::to_string(dt.sec); return s; } } \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 442251d..6ef7147 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,4 +15,9 @@ add_executable( CronDataTest.cpp CronScheduleTest.cpp CronTest.cpp) -target_link_libraries(${PROJECT_NAME} libcron) \ No newline at end of file +target_link_libraries(${PROJECT_NAME} libcron) + +set_target_properties(${PROJECT_NAME} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" + LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}" + RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}") \ No newline at end of file diff --git a/test/CronTest.cpp b/test/CronTest.cpp index 5d5ac5e..49b4d3a 100644 --- a/test/CronTest.cpp +++ b/test/CronTest.cpp @@ -19,190 +19,260 @@ std::string create_schedule_expiring_in(std::chrono::system_clock::time_point no return res; } - -SCENARIO("Adding a task") -{ - GIVEN("A Cron instance with no task") - { - Cron c; - auto expired = false; - - THEN("Starts with no task") - { - REQUIRE(c.count() == 0); - } - - WHEN("Adding a task that runs every second") - { - REQUIRE(c.add_schedule("A task", "* * * * * ?", - [&expired]() - { - expired = true; - }) - ); - - THEN("Count is 1 and task was not expired two seconds ago") - { - REQUIRE(c.count() == 1); - c.execute_expired_tasks(c.get_clock()->now() - 2s); - REQUIRE_FALSE(expired); - } - AND_THEN("Task is expired when calculating based on current time") - { - c.execute_expired_tasks(); - THEN("Task is expired") - { - REQUIRE(expired); - } - } - } - } -} - -SCENARIO("Adding a task that expires in the future") -{ - GIVEN("A Cron instance with task expiring in 3 seconds") - { - auto expired = false; - - Cron c; - REQUIRE(c.add_schedule("A task", create_schedule_expiring_in(c.get_clock()->now(), hours{0}, minutes{0}, seconds{3}), - [&expired]() - { - expired = true; - }) - ); - - THEN("Not yet expired") - { - REQUIRE_FALSE(expired); - } - AND_WHEN("When waiting one second") - { - std::this_thread::sleep_for(1s); - c.execute_expired_tasks(); - THEN("Task has not yet expired") - { - REQUIRE_FALSE(expired); - } - } - AND_WHEN("When waiting three seconds") - { - std::this_thread::sleep_for(3s); - c.execute_expired_tasks(); - THEN("Task has expired") - { - REQUIRE(expired); - } - } - } -} - -SCENARIO("Task priority") -{ - GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order") - { - auto _3_second_expired = 0; - auto _5_second_expired = 0; - - - Cron c; - REQUIRE(c.add_schedule("Five", create_schedule_expiring_in(c.get_clock()->now(), hours{0}, minutes{0}, seconds{5}), - [&_5_second_expired]() - { - _5_second_expired++; - }) - ); - - REQUIRE(c.add_schedule("Three", create_schedule_expiring_in(c.get_clock()->now(), hours{0}, minutes{0}, seconds{3}), - [&_3_second_expired]() - { - _3_second_expired++; - }) - ); - - THEN("Not yet expired") - { - REQUIRE_FALSE(_3_second_expired); - REQUIRE_FALSE(_5_second_expired); - } - - WHEN("Waiting 1 seconds") - { - std::this_thread::sleep_for(1s); - c.execute_expired_tasks(); - - THEN("Task has not yet expired") - { - REQUIRE(_3_second_expired == 0); - REQUIRE(_5_second_expired == 0); - } - } - AND_WHEN("Waiting 3 seconds") - { - std::this_thread::sleep_for(3s); - c.execute_expired_tasks(); - - THEN("3 second task has expired") - { - REQUIRE(_3_second_expired == 1); - REQUIRE(_5_second_expired == 0); - } - } - AND_WHEN("Waiting 5 seconds") - { - std::this_thread::sleep_for(5s); - c.execute_expired_tasks(); - - THEN("3 and 5 second task has expired") - { - REQUIRE(_3_second_expired == 1); - REQUIRE(_5_second_expired == 1); - } - } - AND_WHEN("Waiting based on the time given by the Cron instance") - { - std::this_thread::sleep_for(c.time_until_next()); - c.execute_expired_tasks(); - - THEN("3 second task has expired") - { - REQUIRE(_3_second_expired == 1); - REQUIRE(_5_second_expired == 0); - } - } - AND_WHEN("Waiting based on the time given by the Cron instance") - { - std::this_thread::sleep_for(c.time_until_next()); - REQUIRE(c.execute_expired_tasks() == 1); - - std::this_thread::sleep_for(c.time_until_next()); - REQUIRE(c.execute_expired_tasks() == 1); - - THEN("3 and 5 second task has each expired once") - { - REQUIRE(_3_second_expired == 1); - REQUIRE(_5_second_expired == 1); - } - } - } -} - -//SCENARIO("Clock changes") +//SCENARIO("Adding a task") //{ -// GIVEN("A Cron instance with a single task expiring in 4h") +// GIVEN("A Cron instance with no task") // { -// Cron c; -// auto clock = c.get_clock(); -// system_clock::time_point time_from_task; -// auto now = clock->now(); -// REQUIRE(c.add_schedule("Task", create_schedule_expiring_in(now, hours{4}, minutes{0}, seconds{0}), -// [clock, &time_from_task]() +// Cron<> c; +// auto expired = false; +// +// THEN("Starts with no task") +// { +// REQUIRE(c.count() == 0); +// } +// +// WHEN("Adding a task that runs every second") +// { +// REQUIRE(c.add_schedule("A task", "* * * * * ?", +// [&expired]() +// { +// expired = true; +// }) +// ); +// +// THEN("Count is 1 and task was not expired two seconds ago") +// { +// REQUIRE(c.count() == 1); +// c.tick(c.get_clock().now() - 2s); +// REQUIRE_FALSE(expired); +// } +// AND_THEN("Task is expired when calculating based on current time") +// { +// c.tick(); +// THEN("Task is expired") +// { +// REQUIRE(expired); +// } +// } +// } +// } +//} +// +//SCENARIO("Adding a task that expires in the future") +//{ +// GIVEN("A Cron instance with task expiring in 3 seconds") +// { +// auto expired = false; +// +// Cron<> c; +// REQUIRE(c.add_schedule("A task", +// create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), +// [&expired]() // { -// time_from_task = clock->now(); +// expired = true; // }) // ); // -// +// THEN("Not yet expired") +// { +// REQUIRE_FALSE(expired); +// } +// AND_WHEN("When waiting one second") +// { +// std::this_thread::sleep_for(1s); +// c.tick(); +// THEN("Task has not yet expired") +// { +// REQUIRE_FALSE(expired); +// } +// } +// AND_WHEN("When waiting three seconds") +// { +// std::this_thread::sleep_for(3s); +// c.tick(); +// THEN("Task has expired") +// { +// REQUIRE(expired); +// } +// } // } -//} \ No newline at end of file +//} +// +//SCENARIO("Task priority") +//{ +// GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order") +// { +// auto _3_second_expired = 0; +// auto _5_second_expired = 0; +// +// +// Cron<> c; +// REQUIRE(c.add_schedule("Five", +// create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{5}), +// [&_5_second_expired]() +// { +// _5_second_expired++; +// }) +// ); +// +// REQUIRE(c.add_schedule("Three", +// create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), +// [&_3_second_expired]() +// { +// _3_second_expired++; +// }) +// ); +// +// THEN("Not yet expired") +// { +// REQUIRE_FALSE(_3_second_expired); +// REQUIRE_FALSE(_5_second_expired); +// } +// +// WHEN("Waiting 1 seconds") +// { +// std::this_thread::sleep_for(1s); +// c.tick(); +// +// THEN("Task has not yet expired") +// { +// REQUIRE(_3_second_expired == 0); +// REQUIRE(_5_second_expired == 0); +// } +// } +// AND_WHEN("Waiting 3 seconds") +// { +// std::this_thread::sleep_for(3s); +// c.tick(); +// +// THEN("3 second task has expired") +// { +// REQUIRE(_3_second_expired == 1); +// REQUIRE(_5_second_expired == 0); +// } +// } +// AND_WHEN("Waiting 5 seconds") +// { +// std::this_thread::sleep_for(5s); +// c.tick(); +// +// THEN("3 and 5 second task has expired") +// { +// REQUIRE(_3_second_expired == 1); +// REQUIRE(_5_second_expired == 1); +// } +// } +// AND_WHEN("Waiting based on the time given by the Cron instance") +// { +// std::this_thread::sleep_for(c.time_until_next()); +// c.tick(); +// +// THEN("3 second task has expired") +// { +// REQUIRE(_3_second_expired == 1); +// REQUIRE(_5_second_expired == 0); +// } +// } +// AND_WHEN("Waiting based on the time given by the Cron instance") +// { +// std::this_thread::sleep_for(c.time_until_next()); +// REQUIRE(c.tick() == 1); +// +// std::this_thread::sleep_for(c.time_until_next()); +// REQUIRE(c.tick() == 1); +// +// THEN("3 and 5 second task has each expired once") +// { +// REQUIRE(_3_second_expired == 1); +// REQUIRE(_5_second_expired == 1); +// } +// } +// } +//} +// +class TestClock + : public ICronClock +{ + public: + std::chrono::system_clock::time_point now() const override + { + return current_time; + } + + std::chrono::seconds utc_offset(std::chrono::system_clock::time_point) const override + { + return 0s; + } + + void add(system_clock::duration time) + { + current_time += time; + } + + void set(system_clock::time_point new_time) + { + current_time = new_time; + } + + private: + system_clock::time_point current_time = system_clock::now(); + +}; + +SCENARIO("Clock changes") +{ + GIVEN("A Cron instance with a single task expiring in 4h") + { + Cron c{}; + auto& clock = c.get_clock(); + + // Midnight + clock.set(sys_days{2018_y / 05 / 05}); + + // Every hour + REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", []() + { + }) + ); + + // https://linux.die.net/man/8/cron + + WHEN("Clock changes <3h forward") + { + THEN("Task expires accordingly") + { + REQUIRE(c.tick() == 1); + clock.add(minutes{30}); // 00:30 + REQUIRE(c.tick() == 0); + clock.add(minutes{30}); // 01:00 + REQUIRE(c.tick() == 1); + REQUIRE(c.tick() == 0); + REQUIRE(c.tick() == 0); + clock.add(minutes{30}); // 01:30 + REQUIRE(c.tick() == 0); + clock.add(minutes{15}); // 01:45 + REQUIRE(c.tick() == 0); + clock.add(minutes{15}); // 02:00 + REQUIRE(c.tick() == 1); + } + } + AND_WHEN("Clock is moved forward >= 3h") + { + THEN("Task are rescheduled, not run") + { + REQUIRE(c.tick() == 1); + std::cout << c << std::endl; + clock.add(hours{3}); // 03:00 + REQUIRE(c.tick() == 1); // Rescheduled + std::cout << c << std::endl; + clock.add(minutes{15}); // 03:15 + REQUIRE(c.tick() == 1); + std::cout << c << std::endl; + clock.add(minutes{45}); // 04:00 + REQUIRE(c.tick() == 1); + std::cout << c << std::endl; + } + } + + } +} \ No newline at end of file