diff --git a/README.md b/README.md index cfa3fac..4a8cfcf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,67 @@ # libcron A C++ scheduling library using cron formatting. -# Local time vs UTC +# Using the Scheduler + +Libcron offers an easy to use API to add callbacks with corresponding cron-formatted strings: + +``` +libcron::Cron cron; + +cron.add_schedule("Hello from Cron", "* * * * * ?", [=]() { + std::cout << "Hello from libcron!" std::endl; +}); +``` + +To trigger the execution of callbacks, one must call `libcron::Cron::tick` at least once a second to prevent missing schedules: + +``` +while(true) +{ + cron.tick(); + std::this_thread::sleep_for(500mS); +} +``` + + + +## Removing schedules from `libcron::Cron` + +libcron::Cron offers two convenient functions to remove schedules: + +- `clear_schedules()` will remove all schedules +- `remove_schedule(std::string)` will remove a specific schedule + +For example, `cron.remove_schedule("Hello from Cron")` will remove the previously added task. + + + +## Removing/Adding tasks at runtime in a multithreaded environment + +When Calling `libcron::Cron::tick` from another thread than `add_schedule`, `clear_schedule` and `remove_schedule`, one must take care to protect the internal resources of `libcron::Cron` so that tasks are not removed or added while `libcron::Cron` is iterating over the schedules. `libcron::Cron` can take care of that, you simply have to define your own aliases: + +``` +/* The default class uses NullLock, which does not lock the resources at runtime */ +template +class Cron +{ + ... +} + +/* Define an alias for a thread-safe Cron scheduler which automatically locks ressources when needed */ +using CronMt = libcron::Cron + +CronMt cron; +cron.add_schedule("Hello from Cron", "* * * * * ?", [=]() { + std::cout << "Hello from CronMt!" std::endl; +}); + +.... +``` + +However, this comes with costs: Whenever you call `tick`, a `std::mutex` will be locked and unlocked. So only use the `libcron::Locker` to protect resources when you really need too. + +## Local time vs UTC This library uses `std::chrono::system_clock::timepoint` as its time unit. While that is UTC by default, the Cron-class uses a `LocalClock` by default which offsets `system_clock::now()` by the current UTC-offset. If you wish to work in @@ -55,6 +115,7 @@ Each part is separated by one or more whitespaces. It is thus important to keep * Invalid: * 0, 3, 40-50 * * * * ? + `Day of month` and `day of week` are mutually exclusive so one of them must at always be ignored using the '?'-character to ensure that it is not possible to specify a statement which results in an impossible mix of these fields. @@ -86,4 +147,3 @@ when using randomization, i.e. mutual exclusiveness and no extra spaces. # Used Third party libraries Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/) - diff --git a/libcron/include/libcron/Cron.h b/libcron/include/libcron/Cron.h index 6746ada..43651cc 100644 --- a/libcron/include/libcron/Cron.h +++ b/libcron/include/libcron/Cron.h @@ -4,23 +4,44 @@ #include #include #include +#include #include "Task.h" #include "CronClock.h" namespace libcron { - template + class NullLock + { + public: + void lock() {} + void unlock() {} + }; + + class Locker + { + public: + Locker() : lck(m, std::defer_lock) {} + void lock() { lck.lock(); } + void unlock() { lck.unlock(); } + private: + std::mutex m{}; + std::unique_lock lck; + }; + + template class Cron; - template - std::ostream& operator<<(std::ostream& stream, const Cron& c); + template + std::ostream& operator<<(std::ostream& stream, const Cron& c); - template + template class Cron { public: bool add_schedule(std::string name, const std::string& schedule, std::function work); + void clear_schedules(); + void remove_schedule(const std::string& name); size_t count() const { @@ -48,7 +69,7 @@ namespace libcron void get_time_until_expiry_for_tasks( std::vector>& status) const; - friend std::ostream& operator<<<>(std::ostream& stream, const Cron& c); + friend std::ostream& operator<<<>(std::ostream& stream, const Cron& c); private: class Queue @@ -66,6 +87,50 @@ namespace libcron { return c; } + + void clear() + { + lock.lock(); + + while (!empty()) + pop(); + + lock.unlock(); + } + + template + void remove(T to_remove) + { + lock.lock(); + + /* Copy current elements */ + std::vector temp{}; + std::swap(temp, c); + + /* Refill with elements ensuring correct order by calling push */ + for (const auto& task : temp) + { + if (task != to_remove) + push(task); + } + + lock.unlock(); + } + + void lock_queue() + { + /* Do not allow to manipulate the Queue */ + lock.lock(); + } + + void release_queue() + { + /* Allow Access to the Queue Manipulating-Functions */ + lock.unlock(); + } + + private: + LockType lock; }; @@ -75,25 +140,39 @@ namespace libcron std::chrono::system_clock::time_point last_tick{}; }; - template - bool Cron::add_schedule(std::string name, const std::string& schedule, std::function work) + 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) { + tasks.lock_queue(); Task t{std::move(name), CronSchedule{cron}, std::move(work)}; if (t.calculate_next(clock.now())) { tasks.push(t); } + tasks.release_queue(); } return res; } - template - std::chrono::system_clock::duration Cron::time_until_next() const + template + void Cron::clear_schedules() + { + tasks.clear(); + } + + template + void Cron::remove_schedule(const std::string& name) + { + tasks.remove(name); + } + + template + std::chrono::system_clock::duration Cron::time_until_next() const { std::chrono::system_clock::duration d{}; if (tasks.empty()) @@ -108,9 +187,10 @@ namespace libcron return d; } - template - size_t Cron::tick(std::chrono::system_clock::time_point now) + template + size_t Cron::tick(std::chrono::system_clock::time_point now) { + tasks.lock_queue(); size_t res = 0; if(!first_tick) @@ -189,11 +269,12 @@ namespace libcron } }); + tasks.release_queue(); return res; } - template - void Cron::get_time_until_expiry_for_tasks(std::vector + void Cron::get_time_until_expiry_for_tasks(std::vector>& status) const { auto now = clock.now(); @@ -206,8 +287,8 @@ namespace libcron }); } - template - std::ostream& operator<<(std::ostream& stream, const Cron& c) + 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) @@ -217,4 +298,4 @@ namespace libcron return stream; } -} \ No newline at end of file +} diff --git a/libcron/include/libcron/Task.h b/libcron/include/libcron/Task.h index b0a918d..72fcda8 100644 --- a/libcron/include/libcron/Task.h +++ b/libcron/include/libcron/Task.h @@ -54,4 +54,24 @@ namespace libcron bool valid = false; std::chrono::system_clock::time_point last_run = std::numeric_limits::min(); }; -} \ No newline at end of file +} + +inline bool operator==(const std::string &lhs, const libcron::Task &rhs) +{ + return lhs == rhs.get_name(); +} + +inline bool operator==(const libcron::Task &lhs, const std::string &rhs) +{ + return lhs.get_name() == rhs; +} + +inline bool operator!=(const std::string &lhs, const libcron::Task &rhs) +{ + return !(lhs == rhs); +} + +inline bool operator!=(const libcron::Task &lhs, const std::string &rhs) +{ + return !(lhs == rhs); +} diff --git a/test/CronTest.cpp b/test/CronTest.cpp index 18f1f58..aa66ad0 100644 --- a/test/CronTest.cpp +++ b/test/CronTest.cpp @@ -334,4 +334,71 @@ SCENARIO("Multiple ticks per second") } -} \ No newline at end of file +} + +SCENARIO("Tasks can be added and removed from the scheduler") +{ + GIVEN("A Cron instance with no task") + { + Cron<> c; + auto expired = false; + + WHEN("Adding 5 tasks that runs every second") + { + REQUIRE(c.add_schedule("Task-1", "* * * * * ?", + [&expired]() + { + expired = true; + }) + ); + + REQUIRE(c.add_schedule("Task-2", "* * * * * ?", + [&expired]() + { + expired = true; + }) + ); + + REQUIRE(c.add_schedule("Task-3", "* * * * * ?", + [&expired]() + { + expired = true; + }) + ); + + REQUIRE(c.add_schedule("Task-4", "* * * * * ?", + [&expired]() + { + expired = true; + }) + ); + + REQUIRE(c.add_schedule("Task-5", "* * * * * ?", + [&expired]() + { + expired = true; + }) + ); + + THEN("Count is 5") + { + REQUIRE(c.count() == 5); + } + AND_THEN("Removing all scheduled tasks") + { + c.clear_schedules(); + REQUIRE(c.count() == 0); + } + AND_THEN("Removing a task that does not exist") + { + c.remove_schedule("Task-6"); + REQUIRE(c.count() == 5); + } + AND_THEN("Removing a task that does exist") + { + c.remove_schedule("Task-5"); + REQUIRE(c.count() == 4); + } + } + } +}