├── .gitignore ├── CMakeLists.txt ├── Cron.h ├── InterruptableSleep.h ├── License.txt ├── README.md ├── Scheduler.h └── example.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | cmake-build-debug/ 3 | libScheduler.so 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(BosmaScheduler) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | # threads 7 | set(THREADS_PREFER_PTHREAD_FLAG ON) 8 | find_package(Threads REQUIRED) 9 | 10 | add_executable(example example.cpp) 11 | target_link_libraries(example Threads::Threads) -------------------------------------------------------------------------------- /Cron.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace Bosma { 8 | using Clock = std::chrono::system_clock; 9 | 10 | inline void add(std::tm &tm, Clock::duration time) { 11 | auto tp = Clock::from_time_t(std::mktime(&tm)); 12 | auto tp_adjusted = tp + time; 13 | auto tm_adjusted = Clock::to_time_t(tp_adjusted); 14 | tm = *std::localtime(&tm_adjusted); 15 | } 16 | 17 | class BadCronExpression : public std::exception { 18 | public: 19 | explicit BadCronExpression(std::string msg) : msg_(std::move(msg)) {} 20 | 21 | const char *what() const noexcept override { return (msg_.c_str()); } 22 | 23 | private: 24 | std::string msg_; 25 | }; 26 | 27 | inline void 28 | verify_and_set(const std::string &token, const std::string &expression, int &field, const int lower_bound, 29 | const int upper_bound, const bool adjust = false) { 30 | if (token == "*") 31 | field = -1; 32 | else { 33 | try { 34 | field = std::stoi(token); 35 | } catch (const std::invalid_argument &) { 36 | throw BadCronExpression("malformed cron string (`" + token + "` not an integer or *): " + expression); 37 | } catch (const std::out_of_range &) { 38 | throw BadCronExpression("malformed cron string (`" + token + "` not convertable to int): " + expression); 39 | } 40 | if (field < lower_bound || field > upper_bound) { 41 | std::ostringstream oss; 42 | oss << "malformed cron string ('" << token << "' must be <= " << upper_bound << " and >= " << lower_bound 43 | << "): " << expression; 44 | throw BadCronExpression(oss.str()); 45 | } 46 | if (adjust) 47 | field--; 48 | } 49 | } 50 | 51 | class Cron { 52 | public: 53 | explicit Cron(const std::string &expression) { 54 | std::istringstream iss(expression); 55 | std::vector tokens{std::istream_iterator{iss}, 56 | std::istream_iterator{}}; 57 | 58 | if (tokens.size() != 5) throw BadCronExpression("malformed cron string (must be 5 fields): " + expression); 59 | 60 | verify_and_set(tokens[0], expression, minute, 0, 59); 61 | verify_and_set(tokens[1], expression, hour, 0, 23); 62 | verify_and_set(tokens[2], expression, day, 1, 31); 63 | verify_and_set(tokens[3], expression, month, 1, 12, true); 64 | verify_and_set(tokens[4], expression, day_of_week, 0, 6); 65 | } 66 | 67 | // http://stackoverflow.com/a/322058/1284550 68 | Clock::time_point cron_to_next(const Clock::time_point from = Clock::now()) const { 69 | // get current time as a tm object 70 | auto now = Clock::to_time_t(from); 71 | std::tm next(*std::localtime(&now)); 72 | // it will always at least run the next minute 73 | next.tm_sec = 0; 74 | add(next, std::chrono::minutes(1)); 75 | while (true) { 76 | if (month != -1 && next.tm_mon != month) { 77 | // add a month 78 | // if this will bring us over a year, increment the year instead and reset the month 79 | if (next.tm_mon + 1 > 11) { 80 | next.tm_mon = 0; 81 | next.tm_year++; 82 | } else 83 | next.tm_mon++; 84 | 85 | next.tm_mday = 1; 86 | next.tm_hour = 0; 87 | next.tm_min = 0; 88 | continue; 89 | } 90 | if (day != -1 && next.tm_mday != day) { 91 | add(next, std::chrono::hours(24)); 92 | next.tm_hour = 0; 93 | next.tm_min = 0; 94 | continue; 95 | } 96 | if (day_of_week != -1 && next.tm_wday != day_of_week) { 97 | add(next, std::chrono::hours(24)); 98 | next.tm_hour = 0; 99 | next.tm_min = 0; 100 | continue; 101 | } 102 | if (hour != -1 && next.tm_hour != hour) { 103 | add(next, std::chrono::hours(1)); 104 | next.tm_min = 0; 105 | continue; 106 | } 107 | if (minute != -1 && next.tm_min != minute) { 108 | add(next, std::chrono::minutes(1)); 109 | continue; 110 | } 111 | break; 112 | } 113 | 114 | // telling mktime to figure out dst 115 | next.tm_isdst = -1; 116 | return Clock::from_time_t(std::mktime(&next)); 117 | } 118 | 119 | int minute, hour, day, month, day_of_week; 120 | }; 121 | } -------------------------------------------------------------------------------- /InterruptableSleep.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace Bosma { 10 | class InterruptableSleep { 11 | 12 | using Clock = std::chrono::system_clock; 13 | 14 | // InterruptableSleep offers a sleep that can be interrupted by any thread. 15 | // It can be interrupted multiple times 16 | // and be interrupted before any sleep is called (the sleep will immediately complete) 17 | // Has same interface as condition_variables and futures, except with sleep instead of wait. 18 | // For a given object, sleep can be called on multiple threads safely, but is not recommended as behaviour is undefined. 19 | 20 | public: 21 | InterruptableSleep() : interrupted(false) { 22 | } 23 | 24 | InterruptableSleep(const InterruptableSleep &) = delete; 25 | 26 | InterruptableSleep(InterruptableSleep &&) noexcept = delete; 27 | 28 | ~InterruptableSleep() noexcept = default; 29 | 30 | InterruptableSleep &operator=(const InterruptableSleep &) noexcept = delete; 31 | 32 | InterruptableSleep &operator=(InterruptableSleep &&) noexcept = delete; 33 | 34 | void sleep_for(Clock::duration duration) { 35 | std::unique_lock ul(m); 36 | cv.wait_for(ul, duration, [this] { return interrupted; }); 37 | interrupted = false; 38 | } 39 | 40 | void sleep_until(Clock::time_point time) { 41 | std::unique_lock ul(m); 42 | cv.wait_until(ul, time, [this] { return interrupted; }); 43 | interrupted = false; 44 | } 45 | 46 | void sleep() { 47 | std::unique_lock ul(m); 48 | cv.wait(ul, [this] { return interrupted; }); 49 | interrupted = false; 50 | } 51 | 52 | void interrupt() { 53 | std::lock_guard lg(m); 54 | interrupted = true; 55 | cv.notify_one(); 56 | } 57 | 58 | private: 59 | bool interrupted; 60 | std::mutex m; 61 | std::condition_variable cv; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bosma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scheduler 2 | Modern C++ Header-Only Scheduling Library. Tasks run in thread pool. Requires C++11 and [ctpl_stl.h](https://github.com/vit-vit/CTPL) in the path. 3 | 4 | Inspired by the [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler) gem. Offers mostly the same functionality. 5 | 6 | ```C++ 7 | #include "Scheduler.h" 8 | 9 | // number of tasks that can run simultaneously 10 | // Note: not the number of tasks that can be added, 11 | // but number of tasks that can be run in parallel 12 | unsigned int max_n_threads = 12; 13 | 14 | // Make a new scheduling object. 15 | // Note: s cannot be moved or copied 16 | Bosma::Scheduler s(max_n_threads); 17 | 18 | // every second call message("every second") 19 | s.every(1s, message, "every second"); 20 | 21 | // in one minute 22 | s.in(1min, []() { std::cout << "in one minute" << std::endl; }); 23 | 24 | // in one second run lambda, then wait a second, run lambda, and so on 25 | // different from every in that multiple instances of the function will not be run concurrently 26 | s.interval(1s, []() { 27 | std::this_thread::sleep_for(5s); 28 | std::cout << "once every 6s" << std::endl; 29 | }); 30 | 31 | s.every(1min, []() { std::cout << "every minute" << std::endl; }); 32 | 33 | // https://en.wikipedia.org/wiki/Cron 34 | s.cron("* * * * *", []() { std::cout << "top of every minute" << std::endl; }); 35 | 36 | // Time formats supported: 37 | // %Y/%m/%d %H:%M:%S, %Y-%m-%d %H:%M:%S, %H:%M:%S 38 | // With only a time given, it will run tomorrow if that time has already passed. 39 | // But with a date given, it will run immediately if that time has already passed. 40 | s.at("2017-04-19 12:31:15", []() { std::cout << "at a specific time." << std::endl; }); 41 | 42 | s.cron("5 0 * * *", []() { std::cout << "every day 5 minutes after midnight" << std::endl; }); 43 | ``` 44 | See [example.cpp](example.cpp) for a full example. 45 | -------------------------------------------------------------------------------- /Scheduler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "ctpl_stl.h" 7 | 8 | #include "InterruptableSleep.h" 9 | #include "Cron.h" 10 | 11 | namespace Bosma { 12 | using Clock = std::chrono::system_clock; 13 | 14 | class Task { 15 | public: 16 | explicit Task(std::function &&f, bool recur = false, bool interval = false) : 17 | f(std::move(f)), recur(recur), interval(interval) {} 18 | 19 | virtual Clock::time_point get_new_time() const = 0; 20 | 21 | std::function f; 22 | 23 | bool recur; 24 | bool interval; 25 | }; 26 | 27 | class InTask : public Task { 28 | public: 29 | explicit InTask(std::function &&f) : Task(std::move(f)) {} 30 | 31 | // dummy time_point because it's not used 32 | Clock::time_point get_new_time() const override { return Clock::time_point(Clock::duration(0)); } 33 | }; 34 | 35 | class EveryTask : public Task { 36 | public: 37 | EveryTask(Clock::duration time, std::function &&f, bool interval = false) : 38 | Task(std::move(f), true, interval), time(time) {} 39 | 40 | Clock::time_point get_new_time() const override { 41 | return Clock::now() + time; 42 | }; 43 | Clock::duration time; 44 | }; 45 | 46 | class CronTask : public Task { 47 | public: 48 | CronTask(const std::string &expression, std::function &&f) : Task(std::move(f), true), 49 | cron(expression) {} 50 | 51 | Clock::time_point get_new_time() const override { 52 | return cron.cron_to_next(); 53 | }; 54 | Cron cron; 55 | }; 56 | 57 | inline bool try_parse(std::tm &tm, const std::string &expression, const std::string &format) { 58 | std::stringstream ss(expression); 59 | return !(ss >> std::get_time(&tm, format.c_str())).fail(); 60 | } 61 | 62 | class Scheduler { 63 | public: 64 | explicit Scheduler(unsigned int max_n_tasks = 4) : done(false), threads(max_n_tasks + 1) { 65 | threads.push([this](int) { 66 | while (!done) { 67 | if (tasks.empty()) { 68 | sleeper.sleep(); 69 | } else { 70 | auto time_of_first_task = (*tasks.begin()).first; 71 | sleeper.sleep_until(time_of_first_task); 72 | } 73 | manage_tasks(); 74 | } 75 | }); 76 | } 77 | 78 | Scheduler(const Scheduler &) = delete; 79 | 80 | Scheduler(Scheduler &&) noexcept = delete; 81 | 82 | Scheduler &operator=(const Scheduler &) = delete; 83 | 84 | Scheduler &operator=(Scheduler &&) noexcept = delete; 85 | 86 | ~Scheduler() { 87 | done = true; 88 | sleeper.interrupt(); 89 | } 90 | 91 | template 92 | void in(const Clock::time_point time, _Callable &&f, _Args &&... args) { 93 | std::shared_ptr t = std::make_shared( 94 | std::bind(std::forward<_Callable>(f), std::forward<_Args>(args)...)); 95 | add_task(time, std::move(t)); 96 | } 97 | 98 | template 99 | void in(const Clock::duration time, _Callable &&f, _Args &&... args) { 100 | in(Clock::now() + time, std::forward<_Callable>(f), std::forward<_Args>(args)...); 101 | } 102 | 103 | template 104 | void at(const std::string &time, _Callable &&f, _Args &&... args) { 105 | // get current time as a tm object 106 | auto time_now = Clock::to_time_t(Clock::now()); 107 | std::tm tm = *std::localtime(&time_now); 108 | 109 | // our final time as a time_point 110 | Clock::time_point tp; 111 | 112 | if (try_parse(tm, time, "%H:%M:%S")) { 113 | // convert tm back to time_t, then to a time_point and assign to final 114 | tp = Clock::from_time_t(std::mktime(&tm)); 115 | 116 | // if we've already passed this time, the user will mean next day, so add a day. 117 | if (Clock::now() >= tp) 118 | tp += std::chrono::hours(24); 119 | } else if (try_parse(tm, time, "%Y-%m-%d %H:%M:%S")) { 120 | tp = Clock::from_time_t(std::mktime(&tm)); 121 | } else if (try_parse(tm, time, "%Y/%m/%d %H:%M:%S")) { 122 | tp = Clock::from_time_t(std::mktime(&tm)); 123 | } else { 124 | // could not parse time 125 | throw std::runtime_error("Cannot parse time string: " + time); 126 | } 127 | 128 | in(tp, std::forward<_Callable>(f), std::forward<_Args>(args)...); 129 | } 130 | 131 | template 132 | void every(const Clock::duration time, _Callable &&f, _Args &&... args) { 133 | std::shared_ptr t = std::make_shared(time, std::bind(std::forward<_Callable>(f), 134 | std::forward<_Args>(args)...)); 135 | auto next_time = t->get_new_time(); 136 | add_task(next_time, std::move(t)); 137 | } 138 | 139 | // expression format: 140 | // from https://en.wikipedia.org/wiki/Cron#Overview 141 | // ┌───────────── minute (0 - 59) 142 | // │ ┌───────────── hour (0 - 23) 143 | // │ │ ┌───────────── day of month (1 - 31) 144 | // │ │ │ ┌───────────── month (1 - 12) 145 | // │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) 146 | // │ │ │ │ │ 147 | // │ │ │ │ │ 148 | // * * * * * 149 | template 150 | void cron(const std::string &expression, _Callable &&f, _Args &&... args) { 151 | std::shared_ptr t = std::make_shared(expression, std::bind(std::forward<_Callable>(f), 152 | std::forward<_Args>(args)...)); 153 | auto next_time = t->get_new_time(); 154 | add_task(next_time, std::move(t)); 155 | } 156 | 157 | template 158 | void interval(const Clock::duration time, _Callable &&f, _Args &&... args) { 159 | std::shared_ptr t = std::make_shared(time, std::bind(std::forward<_Callable>(f), 160 | std::forward<_Args>(args)...), true); 161 | add_task(Clock::now(), std::move(t)); 162 | } 163 | 164 | private: 165 | std::atomic done; 166 | 167 | Bosma::InterruptableSleep sleeper; 168 | 169 | std::multimap> tasks; 170 | std::mutex lock; 171 | ctpl::thread_pool threads; 172 | 173 | void add_task(const Clock::time_point time, std::shared_ptr t) { 174 | std::lock_guard l(lock); 175 | tasks.emplace(time, std::move(t)); 176 | sleeper.interrupt(); 177 | } 178 | 179 | void manage_tasks() { 180 | std::lock_guard l(lock); 181 | 182 | auto end_of_tasks_to_run = tasks.upper_bound(Clock::now()); 183 | 184 | // if there are any tasks to be run and removed 185 | if (end_of_tasks_to_run != tasks.begin()) { 186 | // keep track of tasks that will be re-added 187 | decltype(tasks) recurred_tasks; 188 | 189 | // for all tasks that have been triggered 190 | for (auto i = tasks.begin(); i != end_of_tasks_to_run; ++i) { 191 | 192 | auto &task = (*i).second; 193 | 194 | if (task->interval) { 195 | // if it's an interval task, only add the task back after f() is completed 196 | threads.push([this, task](int) { 197 | task->f(); 198 | // no risk of race-condition, 199 | // add_task() will wait for manage_tasks() to release lock 200 | add_task(task->get_new_time(), task); 201 | }); 202 | } else { 203 | threads.push([task](int) { 204 | task->f(); 205 | }); 206 | // calculate time of next run and add the new task to the tasks to be recurred 207 | if (task->recur) 208 | recurred_tasks.emplace(task->get_new_time(), std::move(task)); 209 | } 210 | } 211 | 212 | // remove the completed tasks 213 | tasks.erase(tasks.begin(), end_of_tasks_to_run); 214 | 215 | // re-add the tasks that are recurring 216 | for (auto &task : recurred_tasks) 217 | tasks.emplace(task.first, std::move(task.second)); 218 | } 219 | } 220 | }; 221 | } -------------------------------------------------------------------------------- /example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Scheduler.h" 4 | 5 | void message(const std::string &s) { 6 | std::cout << s << std::endl; 7 | } 8 | 9 | int main() { 10 | // number of tasks that can run simultaneously 11 | // Note: not the number of tasks that can be added, 12 | // but number of tasks that can be run in parallel 13 | unsigned int max_n_threads = 12; 14 | 15 | // Make a new scheduling object. 16 | // Note: s cannot be moved or copied 17 | Bosma::Scheduler s(max_n_threads); 18 | 19 | // every second call message("every second") 20 | s.every(std::chrono::seconds(1), message, "every second"); 21 | 22 | // in one minute 23 | s.in(std::chrono::minutes(1), []() { std::cout << "in one minute" << std::endl; }); 24 | 25 | // run lambda, then wait a second, run lambda, and so on 26 | // different from every in that multiple instances of the function will never be run concurrently 27 | s.interval(std::chrono::seconds(1), []() { 28 | std::cout << "right away, then once every 6s" << std::endl; 29 | std::this_thread::sleep_for(std::chrono::seconds(5)); 30 | }); 31 | 32 | // https://en.wikipedia.org/wiki/Cron 33 | s.cron("* * * * *", []() { std::cout << "top of every minute" << std::endl; }); 34 | 35 | // Time formats supported: 36 | // %Y/%m/%d %H:%M:%S, %Y-%m-%d %H:%M:%S, %H:%M:%S 37 | // With only a time given, it will run tomorrow if that time has already passed. 38 | // But with a date given, it will run immediately if that time has already passed. 39 | s.at("2017-04-19 12:31:15", []() { std::cout << "at a specific time." << std::endl; }); 40 | 41 | s.cron("5 0 * * *", []() { std::cout << "every day 5 minutes after midnight" << std::endl; }); 42 | 43 | // destructor of Bosma::Scheduler will cancel all schedules but finish any tasks currently running 44 | std::this_thread::sleep_for(std::chrono::minutes(10)); 45 | } --------------------------------------------------------------------------------