├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc ├── README.md ├── ecron.md ├── ecron_app.md ├── ecron_event.md ├── ecron_event_handler_controller.md ├── ecron_event_sup.md ├── ecron_sup.md ├── ecron_time.md ├── edoc-info ├── erlang.png ├── overview.edoc └── stylesheet.css ├── rebar.config ├── src ├── ecron.app.src ├── ecron.erl ├── ecron.hrl ├── ecron_app.erl ├── ecron_event.erl ├── ecron_event_handler_controller.erl ├── ecron_event_sup.erl ├── ecron_sup.erl └── ecron_time.erl └── test ├── ecron_event_handler_test.erl └── ecron_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | ebin/ 3 | .eunit/ 4 | *~ 5 | */*~ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2011 Erlang Solutions Ltd 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Erlang Solutions nor the names of its 12 | contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 19 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 22 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all compile clean eunit test doc dialyzer 2 | 3 | all: compile eunit test doc 4 | 5 | compile: 6 | rebar compile 7 | 8 | clean: 9 | rebar clean 10 | 11 | eunit: 12 | rebar eunit 13 | 14 | test: eunit 15 | 16 | doc: 17 | rebar doc 18 | 19 | dialyzer: compile 20 | rebar skip_deps=true dialyze 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The ecron application

4 | 5 | The ecron application 6 | ===================== 7 | The Ecron application. 8 | 9 | __Authors:__ Francesca Gangemi ([`francesca@erlang-solutions.com`](mailto:francesca@erlang-solutions.com)), Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)). 10 | 11 | The Ecron application 12 | 13 | 14 | 15 | The Ecron application executes scheduled functions. 16 | A list of functions to execute might be specified in the ecron application 17 | resource file as value of the `scheduled` environment variable. 18 | 19 | Each entry specifies a job and must contain the scheduled time and a MFA 20 | tuple `{Module, Function, Arguments}`. 21 | It's also possible to configure options for a retry algorithm to run in case 22 | MFA fails. 23 |
 24 | Job =  {{Date, Time}, MFA, Retry, Seconds} |
 25 |        {{Date, Time}, MFA}
 26 | 
27 | 28 | 29 | `Seconds = integer()` is the retry interval. 30 | 31 | 32 | 33 | `Retry = integer() | infinity` is the number of times to retry. 34 | 35 | 36 | Example of ecron.app 37 |
 38 | ...
 39 | {env,[{scheduled,
 40 |        [{{{ '*', '*', '*'}, {0 ,0,0}}, {my_mod, my_fun1, Args}},
 41 |         {{{ '*', 12 ,  25}, {0 ,0,0}}, {my_mod, my_fun2, Args}},
 42 |         {{{ '*', 1  ,  1 }, {0 ,0,0}}, {my_mod, my_fun3, Args}, infinity, 60},
 43 |         {{{2010, 1  ,  1 }, {12,0,0}}, {my_mod, my_fun3, Args}},
 44 |         {{{ '*', 12 ,last}, {0 ,0,0}}, {my_mod, my_fun4, Args}]}]},
 45 | ...
 46 | 
47 | 48 | 49 | Once the ecron application is started, it's possible to dynamically add new 50 | jobs using the `ecron:insert/2` or `ecron:insert/4` 51 | API. 52 | 53 | 54 | 55 | The MFA is executed when a task is set to run. 56 | The MFA has to return `ok`, `{ok, Data}`, `{apply, fun()}` 57 | or `{error, Reason}`. 58 | If `{error, Reason}` is returned and the job was defined with retry options 59 | (Retry and Seconds were specified together with the MFA) then ecron will try 60 | to execute MFA later according to the given configuration. 61 | 62 | 63 | 64 | The MFA may return `{apply, fun()}` where `fun()` has arity zero. 65 | 66 | 67 | 68 | `fun` will be immediately executed after MFA execution. 69 | The `fun` has to return `ok`, `{ok, Data}` or `{error, Reason}`. 70 | 71 | 72 | 73 | If the MFA or `fun` terminates abnormally or returns an invalid 74 | data type (not `ok`, `{ok, Data}` or `{error, Reason}`), an event 75 | is forwarded to the event manager and no retries are executed. 76 | 77 | 78 | 79 | If the return value of the fun is `{error, Reason}` and retry 80 | options were given in the job specification then the `fun` is 81 | rescheduled to be executed after the configurable amount of time. 82 | 83 | 84 | 85 | Data which does not change between retries of the `fun` 86 | must be calculated outside the scope of the `fun`. 87 | Data which changes between retries has to be calculated within the scope 88 | of the `fun`. 89 |

90 | 91 | In the following example, ScheduleTime will change each time the function is 92 | scheduled, while ExecutionTime will change for every retry. If static data 93 | has to persist across calls or retries, this is done through a function in 94 | the MFA or the fun. 95 | 96 |
 97 | print() ->
 98 |   ScheduledTime = time(),
 99 |   {apply, fun() ->
100 |       ExecutionTime = time(),
101 |       io:format("Scheduled:~p~n",[ScheduledTime]),
102 |       io:format("Execution:~p~n",[ExecutionTime]),
103 |       {error, retry}
104 |   end}.
105 | 
106 | 107 | 108 | Event handlers may be configured in the application resource file specifying 109 | for each of them, a tuple as the following: 110 | 111 |
{Handler, Args}
112 | 
113 | Handler = Module | {Module,Id}
114 | Module = atom()
115 | Id = term()
116 | Args = term()
117 | 
118 | `Module:init/1` will be called to initiate the event handler and 119 | its internal state 120 |

121 | 122 |

123 | 124 | Example of ecron.app 125 |
126 | ...
127 | {env, [{event_handlers, [{ecron_event, []}]}]},
128 | ...
129 | 
130 | 131 | 132 | The API `add_event_handler/2` and 133 | `delete_event_handler/1` 134 | allow user to dynamically add and remove event handlers. 135 | 136 | 137 | 138 | All the configured event handlers will receive the following events: 139 | 140 | 141 | 142 | `{mfa_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 143 | when MFA is executed. 144 | 145 | 146 | 147 | `{fun_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 148 | when `fun` is executed. 149 | 150 | 151 | 152 | `{retry, {Schedule, MFA}, Fun, DueDateTime}` 153 | when MFA, or `fun`, is rescheduled to be executed later after a failure. 154 | 155 | 156 | 157 | `{max_retry, {Schedule, MFA}, Fun, DueDateTime}` when MFA, 158 | or `fun` has reached maximum number of retry specified when 159 | the job was inserted. 160 | 161 | 162 | 163 | `Result` is the return value of MFA or `fun`. 164 | If an exception occurs during evaluation of MFA, or `fun`, then 165 | it's caught and sent in the event. 166 | (E.g. `Result = {'EXIT',{Reason,Stack}}`). 167 | 168 | 169 | 170 | `Schedule = {Date, Time}` as given when the job was inserted, E.g. 171 | `{{'*','*','*'}, {0,0,0}}` 172 |

173 | 174 | `DueDateTime = {Date, Time}` is the exact Date and Time when the MFA, 175 | or the `fun`, was supposed to run. 176 | E.g. `{{2010,1,1}, {0,0,0}}` 177 |

178 | 179 | `ExecutionDateTime = {Date, Time}` is the exact Date and Time 180 | when the MFA, or the `fun`, was executed. 181 |

182 | 183 |

184 | 185 |

186 | 187 | If a node is restarted while there are jobs in the list then these jobs are 188 | not lost. When Ecron starts it takes a list of scheduled MFA from the 189 | environment variable `scheduled` and inserts them into a persistent table 190 | (mnesia). If an entry of the scheduled MFA specifies the same parameters 191 | values of a job already present in the table then the entry won't be inserted 192 | avoiding duplicated jobs. 193 |

194 | 195 | No duplicated are removed from the MFA list configured in the `scheduled` variable. 196 | 197 |
198 | Copyright (c) 2009-2011  Erlang Solutions Ltd
199 | All rights reserved.
200 | 
201 | Redistribution and use in source and binary forms, with or without
202 | modification, are permitted provided that the following conditions are met:
203 | * Redistributions of source code must retain the above copyright
204 |   notice, this list of conditions and the following disclaimer.
205 | * Redistributions in binary form must reproduce the above copyright
206 |   notice, this list of conditions and the following disclaimer in the
207 |   documentation and/or other materials provided with the distribution.
208 | * Neither the name of the Erlang Solutions nor the names of its
209 |   contributors may be used to endorse or promote products
210 |   derived from this software without specific prior written permission.
211 | 
212 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
213 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
214 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
215 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
216 | BE  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
217 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
218 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
219 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
220 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
221 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
222 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
223 | 
224 | 225 | 226 |

Modules

227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 |
ecron
ecron_app
ecron_event
ecron_event_handler_controller
ecron_event_sup
ecron_sup
ecron_time
238 | 239 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The ecron application

4 | 5 | The ecron application 6 | ===================== 7 | The Ecron application. 8 | 9 | __Authors:__ Francesca Gangemi ([`francesca@erlang-solutions.com`](mailto:francesca@erlang-solutions.com)), Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)). 10 | 11 | The Ecron application 12 | 13 | 14 | 15 | The Ecron application executes scheduled functions. 16 | A list of functions to execute might be specified in the ecron application 17 | resource file as value of the `scheduled` environment variable. 18 | 19 | Each entry specifies a job and must contain the scheduled time and a MFA 20 | tuple `{Module, Function, Arguments}`. 21 | It's also possible to configure options for a retry algorithm to run in case 22 | MFA fails. 23 |
 24 | Job =  {{Date, Time}, MFA, Retry, Seconds} |
 25 |        {{Date, Time}, MFA}
 26 | 
27 | 28 | 29 | `Seconds = integer()` is the retry interval. 30 | 31 | 32 | 33 | `Retry = integer() | infinity` is the number of times to retry. 34 | 35 | 36 | Example of ecron.app 37 |
 38 | ...
 39 | {env,[{scheduled,
 40 |        [{{{ '*', '*', '*'}, {0 ,0,0}}, {my_mod, my_fun1, Args}},
 41 |         {{{ '*', 12 ,  25}, {0 ,0,0}}, {my_mod, my_fun2, Args}},
 42 |         {{{ '*', 1  ,  1 }, {0 ,0,0}}, {my_mod, my_fun3, Args}, infinity, 60},
 43 |         {{{2010, 1  ,  1 }, {12,0,0}}, {my_mod, my_fun3, Args}},
 44 |         {{{ '*', 12 ,last}, {0 ,0,0}}, {my_mod, my_fun4, Args}]}]},
 45 | ...
 46 | 
47 | 48 | 49 | Once the ecron application is started, it's possible to dynamically add new 50 | jobs using the `ecron:insert/2` or `ecron:insert/4` 51 | API. 52 | 53 | 54 | 55 | The MFA is executed when a task is set to run. 56 | The MFA has to return `ok`, `{ok, Data}`, `{apply, fun()}` 57 | or `{error, Reason}`. 58 | If `{error, Reason}` is returned and the job was defined with retry options 59 | (Retry and Seconds were specified together with the MFA) then ecron will try 60 | to execute MFA later according to the given configuration. 61 | 62 | 63 | 64 | The MFA may return `{apply, fun()}` where `fun()` has arity zero. 65 | 66 | 67 | 68 | `fun` will be immediately executed after MFA execution. 69 | The `fun` has to return `ok`, `{ok, Data}` or `{error, Reason}`. 70 | 71 | 72 | 73 | If the MFA or `fun` terminates abnormally or returns an invalid 74 | data type (not `ok`, `{ok, Data}` or `{error, Reason}`), an event 75 | is forwarded to the event manager and no retries are executed. 76 | 77 | 78 | 79 | If the return value of the fun is `{error, Reason}` and retry 80 | options were given in the job specification then the `fun` is 81 | rescheduled to be executed after the configurable amount of time. 82 | 83 | 84 | 85 | Data which does not change between retries of the `fun` 86 | must be calculated outside the scope of the `fun`. 87 | Data which changes between retries has to be calculated within the scope 88 | of the `fun`. 89 |

90 | 91 | In the following example, ScheduleTime will change each time the function is 92 | scheduled, while ExecutionTime will change for every retry. If static data 93 | has to persist across calls or retries, this is done through a function in 94 | the MFA or the fun. 95 | 96 |
 97 | print() ->
 98 |   ScheduledTime = time(),
 99 |   {apply, fun() ->
100 |       ExecutionTime = time(),
101 |       io:format("Scheduled:~p~n",[ScheduledTime]),
102 |       io:format("Execution:~p~n",[ExecutionTime]),
103 |       {error, retry}
104 |   end}.
105 | 
106 | 107 | 108 | Event handlers may be configured in the application resource file specifying 109 | for each of them, a tuple as the following: 110 | 111 |
{Handler, Args}
112 | 
113 | Handler = Module | {Module,Id}
114 | Module = atom()
115 | Id = term()
116 | Args = term()
117 | 
118 | `Module:init/1` will be called to initiate the event handler and 119 | its internal state 120 |

121 | 122 |

123 | 124 | Example of ecron.app 125 |
126 | ...
127 | {env, [{event_handlers, [{ecron_event, []}]}]},
128 | ...
129 | 
130 | 131 | 132 | The API `add_event_handler/2` and 133 | `delete_event_handler/1` 134 | allow user to dynamically add and remove event handlers. 135 | 136 | 137 | 138 | All the configured event handlers will receive the following events: 139 | 140 | 141 | 142 | `{mfa_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 143 | when MFA is executed. 144 | 145 | 146 | 147 | `{fun_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 148 | when `fun` is executed. 149 | 150 | 151 | 152 | `{retry, {Schedule, MFA}, Fun, DueDateTime}` 153 | when MFA, or `fun`, is rescheduled to be executed later after a failure. 154 | 155 | 156 | 157 | `{max_retry, {Schedule, MFA}, Fun, DueDateTime}` when MFA, 158 | or `fun` has reached maximum number of retry specified when 159 | the job was inserted. 160 | 161 | 162 | 163 | `Result` is the return value of MFA or `fun`. 164 | If an exception occurs during evaluation of MFA, or `fun`, then 165 | it's caught and sent in the event. 166 | (E.g. `Result = {'EXIT',{Reason,Stack}}`). 167 | 168 | 169 | 170 | `Schedule = {Date, Time}` as given when the job was inserted, E.g. 171 | `{{'*','*','*'}, {0,0,0}}` 172 |

173 | 174 | `DueDateTime = {Date, Time}` is the exact Date and Time when the MFA, 175 | or the `fun`, was supposed to run. 176 | E.g. `{{2010,1,1}, {0,0,0}}` 177 |

178 | 179 | `ExecutionDateTime = {Date, Time}` is the exact Date and Time 180 | when the MFA, or the `fun`, was executed. 181 |

182 | 183 |

184 | 185 |

186 | 187 | If a node is restarted while there are jobs in the list then these jobs are 188 | not lost. When Ecron starts it takes a list of scheduled MFA from the 189 | environment variable `scheduled` and inserts them into a persistent table 190 | (mnesia). If an entry of the scheduled MFA specifies the same parameters 191 | values of a job already present in the table then the entry won't be inserted 192 | avoiding duplicated jobs. 193 |

194 | 195 | No duplicated are removed from the MFA list configured in the `scheduled` variable. 196 | 197 |
198 | Copyright (c) 2009-2011  Erlang Solutions Ltd
199 | All rights reserved.
200 | 
201 | Redistribution and use in source and binary forms, with or without
202 | modification, are permitted provided that the following conditions are met:
203 | * Redistributions of source code must retain the above copyright
204 |   notice, this list of conditions and the following disclaimer.
205 | * Redistributions in binary form must reproduce the above copyright
206 |   notice, this list of conditions and the following disclaimer in the
207 |   documentation and/or other materials provided with the distribution.
208 | * Neither the name of the Erlang Solutions nor the names of its
209 |   contributors may be used to endorse or promote products
210 |   derived from this software without specific prior written permission.
211 | 
212 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
213 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
214 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
215 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
216 | BE  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
217 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
218 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
219 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
220 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
221 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
222 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
223 | 
224 | 225 | 226 |

Modules

227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 |
ecron
ecron_app
ecron_event
ecron_event_handler_controller
ecron_event_sup
ecron_sup
ecron_time
238 | 239 | -------------------------------------------------------------------------------- /doc/ecron.md: -------------------------------------------------------------------------------- 1 | Module ecron 2 | ============ 3 | 4 | 5 |

Module ecron

6 | 7 | * [Description](#description) 8 | * [Function Index](#index) 9 | * [Function Details](#functions) 10 | 11 | 12 | The Ecron API module. 13 | 14 | 15 | 16 | __Behaviours:__ [`gen_server`](gen_server.md). 17 | 18 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 19 | 20 |

Description

21 | 22 | 23 | 24 | 25 | 26 | The Ecron application executes scheduled functions. 27 | A list of functions to execute might be specified in the ecron application 28 | resource file as value of the `scheduled` environment variable. 29 | 30 | Each entry specifies a job and must contain the scheduled time and a MFA 31 | tuple `{Module, Function, Arguments}`. 32 | It's also possible to configure options for a retry algorithm to run in case 33 | MFA fails. 34 |
 35 |   Job =  {{Date, Time}, MFA, Retry, Seconds} |
 36 |          {{Date, Time}, MFA}
 37 |   
38 | 39 | 40 | `Seconds = integer()` is the retry interval. 41 | 42 | 43 | 44 | `Retry = integer() | infinity` is the number of times to retry. 45 | 46 | 47 | Example of ecron.app 48 |
 49 |   ...
 50 |   {env,[{scheduled,
 51 |          [{{{ '*', '*', '*'}, {0 ,0,0}}, {my_mod, my_fun1, Args}},
 52 |           {{{ '*', 12 ,  25}, {0 ,0,0}}, {my_mod, my_fun2, Args}},
 53 |           {{{ '*', 1  ,  1 }, {0 ,0,0}}, {my_mod, my_fun3, Args}, infinity, 60},
 54 |           {{{2010, 1  ,  1 }, {12,0,0}}, {my_mod, my_fun3, Args}},
 55 |           {{{ '*', 12 ,last}, {0 ,0,0}}, {my_mod, my_fun4, Args}]}]},
 56 |   ...
 57 |   
58 | 59 | 60 | Once the ecron application is started, it's possible to dynamically add new 61 | jobs using the `ecron:insert/2` or `ecron:insert/4` 62 | API. 63 | 64 | 65 | 66 | The MFA is executed when a task is set to run. 67 | The MFA has to return `ok`, `{ok, Data}`, `{apply, fun()}` 68 | or `{error, Reason}`. 69 | If `{error, Reason}` is returned and the job was defined with retry options 70 | (Retry and Seconds were specified together with the MFA) then ecron will try 71 | to execute MFA later according to the given configuration. 72 | 73 | 74 | 75 | The MFA may return `{apply, fun()}` where `fun()` has arity zero. 76 | 77 | 78 | 79 | `fun` will be immediately executed after MFA execution. 80 | The `fun` has to return `ok`, `{ok, Data}` or `{error, Reason}`. 81 | 82 | 83 | 84 | If the MFA or `fun` terminates abnormally or returns an invalid 85 | data type (not `ok`, `{ok, Data}` or `{error, Reason}`), an event 86 | is forwarded to the event manager and no retries are executed. 87 | 88 | 89 | 90 | If the return value of the fun is `{error, Reason}` and retry 91 | options were given in the job specification then the `fun` is 92 | rescheduled to be executed after the configurable amount of time. 93 | 94 | 95 | 96 | Data which does not change between retries of the `fun` 97 | must be calculated outside the scope of the `fun`. 98 | Data which changes between retries has to be calculated within the scope 99 | of the `fun`. 100 |

101 | 102 | In the following example, ScheduleTime will change each time the function is 103 | scheduled, while ExecutionTime will change for every retry. If static data 104 | has to persist across calls or retries, this is done through a function in 105 | the MFA or the fun. 106 | 107 |
108 |   print() ->
109 |     ScheduledTime = time(),
110 |     {apply, fun() ->
111 |         ExecutionTime = time(),
112 |         io:format("Scheduled:~p~n",[ScheduledTime]),
113 |         io:format("Execution:~p~n",[ExecutionTime]),
114 |         {error, retry}
115 |     end}.
116 |   
117 | 118 | 119 | Event handlers may be configured in the application resource file specifying 120 | for each of them, a tuple as the following: 121 | 122 |
{Handler, Args}
123 |  
124 |   Handler = Module | {Module,Id}
125 |   Module = atom()
126 |   Id = term()
127 |   Args = term()
128 |   
129 | `Module:init/1` will be called to initiate the event handler and 130 | its internal state 131 |

132 | 133 |

134 | 135 | Example of ecron.app 136 |
137 |   ...
138 |   {env, [{event_handlers, [{ecron_event, []}]}]},
139 |   ...
140 |   
141 | 142 | 143 | The API `add_event_handler/2` and 144 | `delete_event_handler/1` 145 | allow user to dynamically add and remove event handlers. 146 | 147 | 148 | 149 | All the configured event handlers will receive the following events: 150 | 151 | 152 | 153 | `{mfa_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 154 | when MFA is executed. 155 | 156 | 157 | 158 | `{fun_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}` 159 | when `fun` is executed. 160 | 161 | 162 | 163 | `{retry, {Schedule, MFA}, Fun, DueDateTime}` 164 | when MFA, or `fun`, is rescheduled to be executed later after a failure. 165 | 166 | 167 | 168 | `{max_retry, {Schedule, MFA}, Fun, DueDateTime}` when MFA, 169 | or `fun` has reached maximum number of retry specified when 170 | the job was inserted. 171 | 172 | 173 | 174 | `Result` is the return value of MFA or `fun`. 175 | If an exception occurs during evaluation of MFA, or `fun`, then 176 | it's caught and sent in the event. 177 | (E.g. `Result = {'EXIT',{Reason,Stack}}`). 178 | 179 | `Schedule = {Date, Time}` as given when the job was inserted, E.g. 180 | `{{'*','*','*'}, {0,0,0}}` 181 |

182 | 183 | `DueDateTime = {Date, Time}` is the exact Date and Time when the MFA, 184 | or the `fun`, was supposed to run. 185 | E.g. `{{2010,1,1}, {0,0,0}}` 186 |

187 | 188 | `ExecutionDateTime = {Date, Time}` is the exact Date and Time 189 | when the MFA, or the `fun`, was executed. 190 |

191 | 192 |

193 | 194 |

195 | 196 | If a node is restarted while there are jobs in the list then these jobs are 197 | not lost. When Ecron starts it takes a list of scheduled MFA from the 198 | environment variable `scheduled` and inserts them into a persistent table 199 | (mnesia). If an entry of the scheduled MFA specifies the same parameters 200 | values of a job already present in the table then the entry won't be inserted 201 | avoiding duplicated jobs. 202 |

203 | 204 | No duplicated are removed from the MFA list configured in the `scheduled` variable. 205 | 206 | 207 |

Function Index

208 | 209 | 210 | 211 |
add_event_handler/2Adds a new event handler.
delete/1Deletes a cron job from the list.
delete_all/0Delete all the scheduled jobs.
delete_event_handler/1Deletes an event handler.
execute_all/0Executes all cron jobs in the queue, irrespective of the time they are 212 | scheduled to run.
insert/2Schedules the MFA at the given Date and Time.
insert/4Schedules the MFA at the given Date and Time and retry if it fails.
install/0Create mnesia tables on those nodes where disc_copies resides according 213 | to the schema.
install/1Create mnesia tables on Nodes.
list/0Returns a list of job records defined in ecron.hrl.
list_event_handlers/0Returns a list of all event handlers installed by the 214 | ecron:add_event_handler/2 API or configured in the 215 | event_handlers environment variable.
print_list/0Prints a pretty list of records sorted by Job ID.
refresh/0Deletes all jobs and recreates the table from the environment variables.
216 | 217 | 218 | 219 | 220 |

Function Details

221 | 222 | 223 | 224 | 225 |

add_event_handler/2

226 | 227 | 228 | 229 | 230 | 231 |
add_event_handler(Handler, Args) -> {ok, Pid} | {error, Reason}
232 | 233 | 234 | 235 | 236 | Adds a new event handler. The handler is added regardless of whether 237 | it's already present, thus duplicated handlers may exist. 238 | 239 |

delete/1

240 | 241 | 242 | 243 | 244 | 245 |
delete(ID) -> ok
246 |

247 | 248 | 249 | 250 | 251 | Deletes a cron job from the list. 252 | If the job does not exist, the function still returns ok 253 | 254 | __See also:__ [print_list/0](#print_list-0). 255 | 256 |

delete_all/0

257 | 258 | 259 | 260 | 261 | 262 |
delete_all() -> ok
263 |

264 | 265 | 266 | 267 | 268 | Delete all the scheduled jobs 269 | 270 |

delete_event_handler/1

271 | 272 | 273 | 274 | 275 | 276 |
delete_event_handler(Pid) -> ok
277 | 278 | 279 | 280 | 281 | Deletes an event handler. Pid is the pid() returned by 282 | `add_event_handler/2`. 283 | 284 |

execute_all/0

285 | 286 | 287 | 288 | 289 | 290 |
execute_all() -> ok
291 |

292 | 293 | 294 | 295 | 296 | Executes all cron jobs in the queue, irrespective of the time they are 297 | scheduled to run. This might be used at startup and shutdown, ensuring no 298 | data is lost and backedup data is handled correctly. 299 |

300 | 301 | It asynchronously returns `ok` and then executes all the jobs 302 | in parallel. 303 |

304 | 305 | No retry will be executed even if the MFA, or the `fun`, fails 306 | mechanism is enabled for that job. Also in case of periodic jobs MFA won't 307 | be rescheduled. Thus the jobs list will always be empty after calling 308 | `execute_all/0`. 309 | 310 |

insert/2

311 | 312 | 313 | 314 | 315 | 316 |
insert(DateTime, MFA) -> ok
317 | 318 | 319 | 320 | 321 | Schedules the MFA at the given Date and Time. 322 |

323 | 324 | Inserts the MFA into the queue to be scheduled at 325 | {Year,Month, Day},{Hours, Minutes,Seconds} 326 |

327 | 328 |
329 |   Month = 1..12 | '*'
330 |   Day = 1..31 | '*' | last
331 |   Hours = 0..23
332 |   Minutes = 0..59
333 |   Seconds = 0..59
334 |   
335 | 336 | 337 | If `Day = last` then the MFA will be executed last day of the month. 338 | 339 | 340 | 341 | `{'*', Time}` runs the MFA every day at the given time and it's 342 | the same as writing `{{'*','*','*'}, Time}`. 343 | 344 | 345 | 346 | `{{'*', '*', Day}, Time}` runs the MFA every month at the given 347 | Day and Time. It must be `Day = 1..28 | last` 348 | 349 | 350 | 351 | `{{'*', Month, Day}, Time}` runs the MFA every year at the given 352 | Month, Day and Time. Day must be valid for the given month or the atom 353 | `last`. 354 | If `Month = 2` then it must be `Day = 1..28 | last` 355 | 356 | 357 | 358 | Combinations of the format `{'*', Month, '*'}` are not allowed. 359 | 360 | 361 | 362 | `{{Year, Month, Day}, Time}` runs the MFA at the given Date and Time. 363 | 364 | Returns `{error, Reason}` if invalid parameters have been passed. 365 | 366 |

insert/4

367 | 368 | 369 | 370 | 371 | 372 |
insert(DateTime, MFA, Retry, Seconds::RetrySeconds) -> ok
373 | 374 | 375 | 376 | 377 | 378 | 379 | Schedules the MFA at the given Date and Time and retry if it fails. 380 | 381 | Same description of insert/2. Additionally if MFA returns 382 | `{error, Reason}` ecron will retry to execute 383 | it after `RetrySeconds`. The MFA will be rescheduled for a 384 | maximum of Retry times. If MFA returns `{apply, fun()}` and the 385 | return value of `fun()` is `{error, Reason}` the 386 | retry mechanism applies to `fun`. If Retry is equal to 3 387 | then MFA will be executed for a maximum of four times. The first time 388 | when is supposed to run according to the schedule and then three more 389 | times at interval of RetrySeconds. 390 | 391 |

install/0

392 | 393 | 394 | 395 | 396 | 397 |
install() -> ok
398 |

399 | 400 | 401 | 402 | 403 | Create mnesia tables on those nodes where disc_copies resides according 404 | to the schema. 405 |

406 | 407 | Before starting the `ecron` application 408 | for the first time a new database must be created, `mnesia:create_schema/1` and tables created by `ecron:install/0` or 409 | `ecron:install/1` 410 |

411 | 412 | E.g. 413 |

414 | 415 |
416 |   >mnesia:create_schema([node()]).
417 |   >mnesia:start().
418 |   >ecron:install().
419 |   
420 | 421 |

install/1

422 | 423 | 424 | 425 | 426 | 427 |
install(Nodes) -> ok
428 |

429 | 430 | 431 | 432 | 433 | Create mnesia tables on Nodes. 434 | 435 |

list/0

436 | 437 | 438 | 439 | 440 | 441 |
list() -> JobList
442 |

443 | 444 | 445 | 446 | 447 | Returns a list of job records defined in ecron.hrl 448 | 449 |

list_event_handlers/0

450 | 451 | 452 | 453 | 454 | 455 |
list_event_handlers() -> [{Pid, Handler}]
456 | 457 | 458 | 459 | 460 | Returns a list of all event handlers installed by the 461 | `ecron:add_event_handler/2` API or configured in the 462 | `event_handlers` environment variable. 463 | 464 |

print_list/0

465 | 466 | 467 | 468 | 469 | 470 |
print_list() -> ok
471 |

472 | 473 | 474 | 475 | 476 | Prints a pretty list of records sorted by Job ID. 477 |

478 | 479 | E.g. 480 |

481 | 482 |
483 |   -----------------------------------------------------------------------
484 |   ID: 208
485 |   Function To Execute: mfa
486 |   Next Execution DateTime: {{2009,11,8},{15,59,54}}
487 |   Scheduled Execution DateTime: {{2009,11,8},{15,59,34}}
488 |   MFA: {ecron_tests,test_function,[fra]}
489 |   Schedule: {{'*','*',8},{15,59,34}}
490 |   Max Retry Times: 4
491 |   Retry Interval: 20
492 |   -----------------------------------------------------------------------
493 |   
494 | 495 | 496 | __`ID`__ is the Job ID and should be used as argument in 497 | `delete/1`. 498 |

499 | 500 | __`Function To Execute`__ says if the job refers to the 501 | MFA or the `fun` returned by MFA. 502 | 503 | 504 | 505 | __`Next Execution DateTime`__ is the date and time when 506 | the job will be executed. 507 | 508 | 509 | 510 | __`Scheduled Execution DateTime`__ is the date and time 511 | when the job was supposed to be executed according to the given 512 | `Schedule`.`Next Execution DateTime` and 513 | `Scheduled Execution DateTime` are different if the MFA, or 514 | the `fun`, failed and it will be retried later 515 | (as in the example given above). 516 | 517 | 518 | 519 | __`MFA`__ is a tuple with Module, Function and Arguments as 520 | given when the job was inserted. 521 |

522 | 523 | __`Schedule`__ is the schedule for the MFA as given when the 524 | job was insterted. 525 |

526 | 527 | __`Max Retry Times`__ is the number of times ecron will retry to 528 | execute the job in case of failure. It may be less than the value given 529 | when the job was inserted if a failure and a retry has already occured. 530 | 531 | __`Retry Interval`__ is the number of seconds ecron will wait 532 | after a failure before retrying to execute the job. It's the value given 533 | when the job was inserted. 534 | 535 |

refresh/0

536 | 537 | 538 | 539 | 540 | 541 |
refresh() -> ok
542 |

543 | 544 | 545 | 546 | 547 | Deletes all jobs and recreates the table from the environment variables. -------------------------------------------------------------------------------- /doc/ecron_app.md: -------------------------------------------------------------------------------- 1 | Module ecron_app 2 | ================ 3 | 4 | 5 |

Module ecron_app

6 | 7 | * [Description](#description) 8 | * [Function Index](#index) 9 | * [Function Details](#functions) 10 | 11 | 12 | Ecron application. 13 | 14 | 15 | 16 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 17 | 18 |

Function Index

19 | 20 | 21 | 22 |
start/2Start the ecron application.
stop/1Stop the ecron application.
23 | 24 | 25 | 26 | 27 |

Function Details

28 | 29 | 30 | 31 | 32 |

start/2

33 | 34 | 35 | 36 | 37 | 38 |
start(Type::_Type, Args::_Args) -> Result
39 | 40 | 41 | 42 | 43 | Start the ecron application 44 | 45 |

stop/1

46 | 47 | 48 | 49 | 50 | 51 |
stop(Args::_Args) -> Result
52 | 53 | 54 | 55 | 56 | Stop the ecron application -------------------------------------------------------------------------------- /doc/ecron_event.md: -------------------------------------------------------------------------------- 1 | Module ecron_event 2 | ================== 3 | 4 | 5 |

Module ecron_event

6 | 7 | * [Description](#description) 8 | * [Function Index](#index) 9 | * [Function Details](#functions) 10 | 11 | 12 | Event Handler. 13 | 14 | 15 | 16 | __Behaviours:__ [`gen_event`](gen_event.md). 17 | 18 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 19 | 20 |

Function Index

21 | 22 | 23 | 24 |
code_change/3Convert process state when code is changed.
handle_call/2Whenever an event manager receives a request sent using 25 | gen_event:call/3,4, this function is called for the specified event 26 | handler to handle the request.
handle_event/2
handle_info/2This function is called for each installed event handler when 27 | an event manager receives any other message than an event or a synchronous 28 | request (or a system message).
init/1
terminate/2Whenever an event handler is deleted from an event manager, 29 | this function is called.
30 | 31 | 32 | 33 | 34 |

Function Details

35 | 36 | 37 | 38 | 39 |

code_change/3

40 | 41 | 42 | 43 | 44 | 45 |
code_change(OldVsn, State, Extra) -> {ok, NewState}
46 |

47 | 48 | 49 | 50 | 51 | Convert process state when code is changed 52 | 53 |

handle_call/2

54 | 55 | 56 | 57 | 58 | 59 |
handle_call(Request, State) -> {ok, Reply, State} | {swap_handler, Reply, Args1, State1, Mod2, Args2} | {remove_handler, Reply}
60 |

61 | 62 | 63 | 64 | 65 | Whenever an event manager receives a request sent using 66 | gen_event:call/3,4, this function is called for the specified event 67 | handler to handle the request. 68 | 69 |

handle_event/2

70 | 71 | 72 | 73 | 74 | 75 | `handle_event(Event, State) -> any()` 76 | 77 | 78 | 79 |

handle_info/2

80 | 81 | 82 | 83 | 84 | 85 |
handle_info(Info, State) -> {ok, State} | {swap_handler, Args1, State1, Mod2, Args2} | remove_handler
86 |

87 | 88 | 89 | 90 | 91 | This function is called for each installed event handler when 92 | an event manager receives any other message than an event or a synchronous 93 | request (or a system message). 94 | 95 |

init/1

96 | 97 | 98 | 99 | 100 | 101 | `init(Args) -> any()` 102 | 103 | 104 | 105 |

terminate/2

106 | 107 | 108 | 109 | 110 | 111 |
terminate(Reason, State) -> void()
112 |

113 | 114 | 115 | 116 | 117 | Whenever an event handler is deleted from an event manager, 118 | this function is called. It should be the opposite of Module:init/1 and 119 | do any necessary cleaning up. -------------------------------------------------------------------------------- /doc/ecron_event_handler_controller.md: -------------------------------------------------------------------------------- 1 | Module ecron_event_handler_controller 2 | ===================================== 3 | 4 | 5 |

Module ecron_event_handler_controller

6 | 7 | * [Function Index](#index) 8 | * [Function Details](#functions) 9 | 10 | 11 | 12 | 13 | 14 | 15 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 16 | 17 |

Function Index

18 | 19 | 20 | 21 |
init/3
start_link/2
system_continue/3
system_terminate/4
22 | 23 | 24 | 25 | 26 |

Function Details

27 | 28 | 29 | 30 | 31 |

init/3

32 | 33 | 34 | 35 | 36 | 37 | `init(Parent, Handler, Args) -> any()` 38 | 39 | 40 | 41 |

start_link/2

42 | 43 | 44 | 45 | 46 | 47 |
start_link(Handler, Args) -> term() | {error, Reason}
48 |

49 | 50 | 51 | 52 | 53 |

system_continue/3

54 | 55 | 56 | 57 | 58 | 59 | `system_continue(Parent, Deb, State) -> any()` 60 | 61 | 62 | 63 |

system_terminate/4

64 | 65 | 66 | 67 | 68 | 69 |
system_terminate(Reason::any(), Parent::any(), Deb::any(), State::any()) -> none()
70 |

71 | 72 | 73 | -------------------------------------------------------------------------------- /doc/ecron_event_sup.md: -------------------------------------------------------------------------------- 1 | Module ecron_event_sup 2 | ====================== 3 | 4 | 5 |

Module ecron_event_sup

6 | 7 | * [Function Index](#index) 8 | * [Function Details](#functions) 9 | 10 | 11 | 12 | 13 | 14 | 15 | __Behaviours:__ [`supervisor`](supervisor.md). 16 | 17 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 18 | 19 |

Function Index

20 | 21 | 22 | 23 |
init/1Whenever a supervisor is started using supervisor:start_link/2,3, 24 | this function is called by the new process to find out about restart strategy, 25 | maximum restart frequency and child specifications.
list_handlers/0Returns a list of all event handlers installed by the 26 | start_handler/2 API.
start_handler/2Starts a child that will add a new event handler.
start_link/0Starts the supervisor.
stop_handler/1Stop the child with the given Pid.
27 | 28 | 29 | 30 | 31 |

Function Details

32 | 33 | 34 | 35 | 36 |

init/1

37 | 38 | 39 | 40 | 41 | 42 |
init(Args) -> {ok, {SupFlags, [ChildSpec]}} | ignore
43 |

44 | 45 | 46 | 47 | 48 | Whenever a supervisor is started using supervisor:start_link/2,3, 49 | this function is called by the new process to find out about restart strategy, 50 | maximum restart frequency and child specifications. 51 | 52 |

list_handlers/0

53 | 54 | 55 | 56 | 57 | 58 |
list_handlers() -> [{Pid, Handler}]
59 | 60 | 61 | 62 | 63 | Returns a list of all event handlers installed by the 64 | `start_handler/2` API 65 | 66 |

start_handler/2

67 | 68 | 69 | 70 | 71 | 72 |
start_handler(Handler, Args) -> {ok, Pid} | {error, Error}
73 |

74 | 75 | 76 | 77 | 78 | Starts a child that will add a new event handler 79 | 80 |

start_link/0

81 | 82 | 83 | 84 | 85 | 86 |
start_link() -> {ok, Pid} | ignore | {error, Error}
87 |

88 | 89 | 90 | 91 | 92 | Starts the supervisor 93 | 94 |

stop_handler/1

95 | 96 | 97 | 98 | 99 | 100 |
stop_handler(Pid) -> ok
101 |

102 | 103 | 104 | 105 | 106 | Stop the child with the given Pid. It will terminate the associated 107 | event handler -------------------------------------------------------------------------------- /doc/ecron_sup.md: -------------------------------------------------------------------------------- 1 | Module ecron_sup 2 | ================ 3 | 4 | 5 |

Module ecron_sup

6 | 7 | * [Description](#description) 8 | * [Function Index](#index) 9 | * [Function Details](#functions) 10 | 11 | 12 | Supervisor module. 13 | 14 | 15 | 16 | __Authors:__ Francesca Gangemi ([`francesca.gangemi@erlang-solutions.com`](mailto:francesca.gangemi@erlang-solutions.com)). 17 | 18 |

Function Index

19 | 20 | 21 | 22 |
init/1Whenever a supervisor is started using supervisor:start_link/2,3, 23 | this function is called by the new process to find out about restart strategy, 24 | maximum restart frequency and child specifications.
start_link/0Starts the supervisor.
25 | 26 | 27 | 28 | 29 |

Function Details

30 | 31 | 32 | 33 | 34 |

init/1

35 | 36 | 37 | 38 | 39 | 40 |
init(Args) -> {ok, {SupFlags, [ChildSpec]}} | ignore
41 |

42 | 43 | 44 | 45 | 46 | Whenever a supervisor is started using supervisor:start_link/2,3, 47 | this function is called by the new process to find out about restart strategy, 48 | maximum restart frequency and child specifications. 49 | 50 |

start_link/0

51 | 52 | 53 | 54 | 55 | 56 |
start_link() -> {ok, Pid} | ignore | {error, Error}
57 |

58 | 59 | 60 | 61 | 62 | Starts the supervisor -------------------------------------------------------------------------------- /doc/ecron_time.md: -------------------------------------------------------------------------------- 1 | Module ecron_time 2 | ================= 3 | 4 | 5 |

Module ecron_time

6 | 7 | * [Description](#description) 8 | * [Function Index](#index) 9 | * [Function Details](#functions) 10 | 11 | 12 | Ecron counterparts of the built-in time functions. 13 | 14 | 15 | 16 | __Authors:__ Ulf Wiger ([`ulf.wiger@erlang-solutions.com`](mailto:ulf.wiger@erlang-solutions.com)). 17 | 18 |

Description

19 | 20 | 21 | 22 | 23 | 24 | This module wraps the standard time functions, `erlang:localtime()`, 25 | `erlang:universaltime()`, `erlang:now()` and `os:timestamp()`. 26 | 27 | The reason for this is to enable mocking to simulate time within ecron. 28 | 29 |

Function Index

30 | 31 | 32 | 33 |
localtime/0
now/0
timestamp/0
universaltime/0
34 | 35 | 36 | 37 | 38 |

Function Details

39 | 40 | 41 | 42 | 43 |

localtime/0

44 | 45 | 46 | 47 | 48 | 49 | `localtime() -> any()` 50 | 51 | 52 | 53 |

now/0

54 | 55 | 56 | 57 | 58 | 59 | `now() -> any()` 60 | 61 | 62 | 63 |

timestamp/0

64 | 65 | 66 | 67 | 68 | 69 | `timestamp() -> any()` 70 | 71 | 72 | 73 |

universaltime/0

74 | 75 | 76 | 77 | 78 | 79 | `universaltime() -> any()` 80 | 81 | -------------------------------------------------------------------------------- /doc/edoc-info: -------------------------------------------------------------------------------- 1 | {application,ecron}. 2 | {packages,[]}. 3 | {modules,[ecron,ecron_app,ecron_event,ecron_event_handler_controller, 4 | ecron_event_sup,ecron_sup,ecron_time]}. 5 | -------------------------------------------------------------------------------- /doc/erlang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fra/ecron/d959d1e1954bdbff8bda0b804116bca5f132edbc/doc/erlang.png -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @author Francesca Gangemi 2 | @author Ulf Wiger 3 | 4 | @doc The Ecron application 5 | 6 | The Ecron application executes scheduled functions. 7 | A list of functions to execute might be specified in the ecron application 8 | resource file as value of the `scheduled' environment variable. 9 | 10 | Each entry specifies a job and must contain the scheduled time and a MFA 11 | tuple `{Module, Function, Arguments}'. 12 | It's also possible to configure options for a retry algorithm to run in case 13 | MFA fails. 14 |
 15 | Job =  {{Date, Time}, MFA, Retry, Seconds} |
 16 |        {{Date, Time}, MFA}
 17 | 
18 | `Seconds = integer()' is the retry interval. 19 | 20 | `Retry = integer() | infinity' is the number of times to retry. 21 | 22 | 23 | Example of ecron.app 24 |
 25 | ...
 26 | {env,[{scheduled,
 27 |        [{{{ '*', '*', '*'}, {0 ,0,0}}, {my_mod, my_fun1, Args}},
 28 |         {{{ '*', 12 ,  25}, {0 ,0,0}}, {my_mod, my_fun2, Args}},
 29 |         {{{ '*', 1  ,  1 }, {0 ,0,0}}, {my_mod, my_fun3, Args}, infinity, 60},
 30 |         {{{2010, 1  ,  1 }, {12,0,0}}, {my_mod, my_fun3, Args}},
 31 |         {{{ '*', 12 ,last}, {0 ,0,0}}, {my_mod, my_fun4, Args}]}]},
 32 | ...
 33 | 
34 | Once the ecron application is started, it's possible to dynamically add new 35 | jobs using the `ecron:insert/2' or `ecron:insert/4' 36 | API. 37 | 38 | The MFA is executed when a task is set to run. 39 | The MFA has to return `ok', `{ok, Data}', `{apply, fun()}' 40 | or `{error, Reason}'. 41 | If `{error, Reason}' is returned and the job was defined with retry options 42 | (Retry and Seconds were specified together with the MFA) then ecron will try 43 | to execute MFA later according to the given configuration. 44 | 45 | The MFA may return `{apply, fun()}' where `fun()' has arity zero. 46 | 47 | `fun' will be immediately executed after MFA execution. 48 | The `fun' has to return `ok', `{ok, Data}' or `{error, Reason}'. 49 | 50 | If the MFA or `fun' terminates abnormally or returns an invalid 51 | data type (not `ok', `{ok, Data}' or `{error, Reason}'), an event 52 | is forwarded to the event manager and no retries are executed. 53 | 54 | If the return value of the fun is `{error, Reason}' and retry 55 | options were given in the job specification then the `fun' is 56 | rescheduled to be executed after the configurable amount of time. 57 | 58 | Data which does not change between retries of the `fun' 59 | must be calculated outside the scope of the `fun'. 60 | Data which changes between retries has to be calculated within the scope 61 | of the `fun'.
62 | In the following example, ScheduleTime will change each time the function is 63 | scheduled, while ExecutionTime will change for every retry. If static data 64 | has to persist across calls or retries, this is done through a function in 65 | the MFA or the fun. 66 | 67 |
 68 | print() ->
 69 |   ScheduledTime = time(),
 70 |   {apply, fun() ->
 71 |       ExecutionTime = time(),
 72 |       io:format("Scheduled:~p~n",[ScheduledTime]),
 73 |       io:format("Execution:~p~n",[ExecutionTime]),
 74 |       {error, retry}
 75 |   end}.
 76 | 
77 | Event handlers may be configured in the application resource file specifying 78 | for each of them, a tuple as the following: 79 | 80 |
{Handler, Args}
 81 | 
 82 | Handler = Module | {Module,Id}
 83 | Module = atom()
 84 | Id = term()
 85 | Args = term()
 86 | 
87 | `Module:init/1' will be called to initiate the event handler and 88 | its internal state

89 | Example of ecron.app 90 |
 91 | ...
 92 | {env, [{event_handlers, [{ecron_event, []}]}]},
 93 | ...
 94 | 
95 | The API `add_event_handler/2' and 96 | `delete_event_handler/1' 97 | allow user to dynamically add and remove event handlers. 98 | 99 | All the configured event handlers will receive the following events: 100 | 101 | `{mfa_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}' 102 | when MFA is executed. 103 | 104 | `{fun_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}' 105 | when `fun' is executed. 106 | 107 | `{retry, {Schedule, MFA}, Fun, DueDateTime}' 108 | when MFA, or `fun', is rescheduled to be executed later after a failure. 109 | 110 | `{max_retry, {Schedule, MFA}, Fun, DueDateTime}' when MFA, 111 | or `fun' has reached maximum number of retry specified when 112 | the job was inserted. 113 | 114 | `Result' is the return value of MFA or `fun'. 115 | If an exception occurs during evaluation of MFA, or `fun', then 116 | it's caught and sent in the event. 117 | (E.g. Result = {'EXIT',{Reason,Stack}}). 118 | 119 | `Schedule = {Date, Time}' as given when the job was inserted, E.g. 120 | {{'*','*','*'}, {0,0,0}}
121 | `DueDateTime = {Date, Time} ' is the exact Date and Time when the MFA, 122 | or the `fun', was supposed to run. 123 | E.g. ` {{2010,1,1}, {0,0,0}}'
124 | `ExecutionDateTime = {Date, Time} ' is the exact Date and Time 125 | when the MFA, or the `fun', was executed.


126 | If a node is restarted while there are jobs in the list then these jobs are 127 | not lost. When Ecron starts it takes a list of scheduled MFA from the 128 | environment variable `scheduled' and inserts them into a persistent table 129 | (mnesia). If an entry of the scheduled MFA specifies the same parameters 130 | values of a job already present in the table then the entry won't be inserted 131 | avoiding duplicated jobs.
132 | No duplicated are removed from the MFA list configured in the ` 133 | scheduled' variable. 134 | 135 |
136 | Copyright (c) 2009-2011  Erlang Solutions Ltd
137 | All rights reserved.
138 | 
139 | Redistribution and use in source and binary forms, with or without
140 | modification, are permitted provided that the following conditions are met:
141 | * Redistributions of source code must retain the above copyright
142 |   notice, this list of conditions and the following disclaimer.
143 | * Redistributions in binary form must reproduce the above copyright
144 |   notice, this list of conditions and the following disclaimer in the
145 |   documentation and/or other materials provided with the distribution.
146 | * Neither the name of the Erlang Solutions nor the names of its
147 |   contributors may be used to endorse or promote products
148 |   derived from this software without specific prior written permission.
149 | 
150 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
151 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
152 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
153 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
154 | BE  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
155 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
156 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
157 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
158 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
159 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
160 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
161 | 
162 | 163 | @end 164 | -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* standard EDoc style sheet */ 2 | body { 3 | font-family: Verdana, Arial, Helvetica, sans-serif; 4 | margin-left: .25in; 5 | margin-right: .2in; 6 | margin-top: 0.2in; 7 | margin-bottom: 0.2in; 8 | color: #000000; 9 | background-color: #ffffff; 10 | } 11 | h1,h2 { 12 | margin-left: -0.2in; 13 | } 14 | div.navbar { 15 | background-color: #add8e6; 16 | padding: 0.2em; 17 | } 18 | h2.indextitle { 19 | padding: 0.4em; 20 | background-color: #add8e6; 21 | } 22 | h3.function,h3.typedecl { 23 | background-color: #add8e6; 24 | padding-left: 1em; 25 | } 26 | div.spec { 27 | margin-left: 2em; 28 | background-color: #eeeeee; 29 | } 30 | a.module,a.package { 31 | text-decoration:none 32 | } 33 | a.module:hover,a.package:hover { 34 | background-color: #eeeeee; 35 | } 36 | ul.definitions { 37 | list-style-type: none; 38 | } 39 | ul.index { 40 | list-style-type: none; 41 | background-color: #eeeeee; 42 | } 43 | 44 | /* 45 | * Minor style tweaks 46 | */ 47 | ul { 48 | list-style-type: square; 49 | } 50 | table { 51 | border-collapse: collapse; 52 | } 53 | td { 54 | padding: 3 55 | } 56 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {erl_opts, [debug_info]}. 3 | {deps, [ 4 | {meck, ".*", {git, "git://github.com/esl/meck.git", "HEAD"}}, 5 | {edown, ".*", 6 | {git, "git://github.com/esl/edown.git", "HEAD"}} 7 | ]}. 8 | {dialyzer_opts, [{warnings, [no_unused, 9 | no_improper_lists, no_fun_app, no_match, 10 | no_opaque, no_fail_call, 11 | error_handling, no_match, 12 | unmatched_returns, 13 | behaviours, underspecs]}]}. 14 | {edoc_opts, [{doclet, edown_doclet}, 15 | {top_level_readme, 16 | {"./README.md", 17 | "http://github.com/esl/ecron"}}]}. 18 | -------------------------------------------------------------------------------- /src/ecron.app.src: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang; -*- 2 | {application, ecron, 3 | [ 4 | {description, ""}, 5 | {vsn, git}, 6 | {registered, []}, 7 | {mod, {ecron_app, []}}, 8 | {applications, [kernel, stdlib, mnesia]}, 9 | {build_dependencies, []}, 10 | {env, [{event_handlers,[{ecron_event, []}]}]} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/ecron.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Francesca Gangemi 3 | %% @doc The Ecron API module. 4 | %% 5 | %% The Ecron application executes scheduled functions. 6 | %% A list of functions to execute might be specified in the ecron application 7 | %% resource file as value of the `scheduled' environment variable. 8 | %% 9 | %% Each entry specifies a job and must contain the scheduled time and a MFA 10 | %% tuple `{Module, Function, Arguments}'. 11 | %% It's also possible to configure options for a retry algorithm to run in case 12 | %% MFA fails. 13 | %%
  14 | %% Job =  {{Date, Time}, MFA, Retry, Seconds} |
  15 | %%        {{Date, Time}, MFA}
  16 | %% 
17 | %% `Seconds = integer()' is the retry interval. 18 | %% 19 | %% `Retry = integer() | infinity' is the number of times to retry. 20 | %% 21 | %% 22 | %% Example of ecron.app 23 | %%
  24 | %% ...
  25 | %% {env,[{scheduled,
  26 | %%        [{{{ '*', '*', '*'}, {0 ,0,0}}, {my_mod, my_fun1, Args}},
  27 | %%         {{{ '*', 12 ,  25}, {0 ,0,0}}, {my_mod, my_fun2, Args}},
  28 | %%         {{{ '*', 1  ,  1 }, {0 ,0,0}}, {my_mod, my_fun3, Args}, infinity, 60},
  29 | %%         {{{2010, 1  ,  1 }, {12,0,0}}, {my_mod, my_fun3, Args}},
  30 | %%         {{{ '*', 12 ,last}, {0 ,0,0}}, {my_mod, my_fun4, Args}]}]},
  31 | %% ...
  32 | %% 
33 | %% Once the ecron application is started, it's possible to dynamically add new 34 | %% jobs using the `ecron:insert/2' or `ecron:insert/4' 35 | %% API. 36 | %% 37 | %% The MFA is executed when a task is set to run. 38 | %% The MFA has to return `ok', `{ok, Data}', `{apply, fun()}' 39 | %% or `{error, Reason}'. 40 | %% If `{error, Reason}' is returned and the job was defined with retry options 41 | %% (Retry and Seconds were specified together with the MFA) then ecron will try 42 | %% to execute MFA later according to the given configuration. 43 | %% 44 | %% The MFA may return `{apply, fun()}' where `fun()' has arity zero. 45 | %% 46 | %% `fun' will be immediately executed after MFA execution. 47 | %% The `fun' has to return `ok', `{ok, Data}' or `{error, Reason}'. 48 | %% 49 | %% If the MFA or `fun' terminates abnormally or returns an invalid 50 | %% data type (not `ok', `{ok, Data}' or `{error, Reason}'), an event 51 | %% is forwarded to the event manager and no retries are executed. 52 | %% 53 | %% If the return value of the fun is `{error, Reason}' and retry 54 | %% options were given in the job specification then the `fun' is 55 | %% rescheduled to be executed after the configurable amount of time. 56 | %% 57 | %% Data which does not change between retries of the `fun' 58 | %% must be calculated outside the scope of the `fun'. 59 | %% Data which changes between retries has to be calculated within the scope 60 | %% of the `fun'.
61 | %% In the following example, ScheduleTime will change each time the function is 62 | %% scheduled, while ExecutionTime will change for every retry. If static data 63 | %% has to persist across calls or retries, this is done through a function in 64 | %% the MFA or the fun. 65 | %% 66 | %%
  67 | %% print() ->
  68 | %%   ScheduledTime = time(),
  69 | %%   {apply, fun() ->
  70 | %%       ExecutionTime = time(),
  71 | %%       io:format("Scheduled:~p~n",[ScheduledTime]),
  72 | %%       io:format("Execution:~p~n",[ExecutionTime]),
  73 | %%       {error, retry}
  74 | %%   end}.
  75 | %% 
76 | %% Event handlers may be configured in the application resource file specifying 77 | %% for each of them, a tuple as the following: 78 | %% 79 | %%
{Handler, Args}
  80 | %%
  81 | %% Handler = Module | {Module,Id}
  82 | %% Module = atom()
  83 | %% Id = term()
  84 | %% Args = term()
  85 | %% 
86 | %% `Module:init/1' will be called to initiate the event handler and 87 | %% its internal state

88 | %% Example of ecron.app 89 | %%
  90 | %% ...
  91 | %% {env, [{event_handlers, [{ecron_event, []}]}]},
  92 | %% ...
  93 | %% 
94 | %% The API `add_event_handler/2' and 95 | %% `delete_event_handler/1' 96 | %% allow user to dynamically add and remove event handlers. 97 | %% 98 | %% All the configured event handlers will receive the following events: 99 | %% 100 | %% `{mfa_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}' 101 | %% when MFA is executed. 102 | %% 103 | %% `{fun_result, Result, {Schedule, {M, F, A}}, DueDateTime, ExecutionDateTime}' 104 | %% when `fun' is executed. 105 | %% 106 | %% `{retry, {Schedule, MFA}, Fun, DueDateTime}' 107 | %% when MFA, or `fun', is rescheduled to be executed later after a failure. 108 | %% 109 | %% `{max_retry, {Schedule, MFA}, Fun, DueDateTime}' when MFA, 110 | %% or `fun' has reached maximum number of retry specified when 111 | %% the job was inserted. 112 | %% 113 | %% `Result' is the return value of MFA or `fun'. 114 | %% If an exception occurs during evaluation of MFA, or `fun', then 115 | %% it's caught and sent in the event. 116 | %% (E.g. Result = {'EXIT',{Reason,Stack}}). 117 | %% 118 | %% `Schedule = {Date, Time}' as given when the job was inserted, E.g. 119 | %% {{'*','*','*'}, {0,0,0}}
120 | %% `DueDateTime = {Date, Time} ' is the exact Date and Time when the MFA, 121 | %% or the `fun', was supposed to run. 122 | %% E.g. ` {{2010,1,1}, {0,0,0}}'
123 | %% `ExecutionDateTime = {Date, Time} ' is the exact Date and Time 124 | %% when the MFA, or the `fun', was executed.


125 | %% If a node is restarted while there are jobs in the list then these jobs are 126 | %% not lost. When Ecron starts it takes a list of scheduled MFA from the 127 | %% environment variable `scheduled' and inserts them into a persistent table 128 | %% (mnesia). If an entry of the scheduled MFA specifies the same parameters 129 | %% values of a job already present in the table then the entry won't be inserted 130 | %% avoiding duplicated jobs.
131 | %% No duplicated are removed from the MFA list configured in the ` 132 | %% scheduled' variable. 133 | %% 134 | %% @end 135 | 136 | %%% Copyright (c) 2009-2010 Erlang Solutions 137 | %%% All rights reserved. 138 | %%% 139 | %%% Redistribution and use in source and binary forms, with or without 140 | %%% modification, are permitted provided that the following conditions are met: 141 | %%% * Redistributions of source code must retain the above copyright 142 | %%% notice, this list of conditions and the following disclaimer. 143 | %%% * Redistributions in binary form must reproduce the above copyright 144 | %%% notice, this list of conditions and the following disclaimer in the 145 | %%% documentation and/or other materials provided with the distribution. 146 | %%% * Neither the name of the Erlang Solutions nor the names of its 147 | %%% contributors may be used to endorse or promote products 148 | %%% derived from this software without specific prior written permission. 149 | %%% 150 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 151 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 152 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 153 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 154 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 155 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 156 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 157 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 158 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 159 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 160 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 161 | %%------------------------------------------------------------------------------ 162 | -module(ecron). 163 | -author('francesca.gangemi@erlang-solutions.com'). 164 | -copyright('Erlang Solutions Ltd.'). 165 | 166 | -behaviour(gen_server). 167 | 168 | %% API 169 | -export([install/0, 170 | install/1, 171 | start_link/0, 172 | insert/2, 173 | insert/4, 174 | list/0, 175 | print_list/0, 176 | execute_all/0, 177 | refresh/0, 178 | delete/1, 179 | delete_all/0, 180 | add_event_handler/2, 181 | list_event_handlers/0, 182 | delete_event_handler/1]). 183 | 184 | %% gen_server callbacks 185 | -export([init/1, 186 | handle_call/3, 187 | handle_cast/2, 188 | handle_info/2, 189 | terminate/2, 190 | code_change/3]). 191 | 192 | -export([execute_job/2, 193 | create_add_job/1]). 194 | 195 | -include("ecron.hrl"). 196 | 197 | %%============================================================================== 198 | %% API functions 199 | %%============================================================================== 200 | 201 | %%------------------------------------------------------------------------------ 202 | %% @spec start_link() -> {ok,Pid} | ignore | {error,Error} 203 | %% @doc Start the server 204 | %% @private 205 | %% @end 206 | %%------------------------------------------------------------------------------ 207 | start_link() -> 208 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 209 | 210 | 211 | %%------------------------------------------------------------------------------ 212 | %% @spec install() -> ok 213 | %% @doc Create mnesia tables on those nodes where disc_copies resides according 214 | %% to the schema.
215 | %% Before starting the `ecron' application 216 | %% for the first time a new database must be created, `mnesia:create_schema/1 217 | %% ' and tables created by `ecron:install/0' or 218 | %% `ecron:install/1'
219 | %% E.g.
220 | %%
 221 | %% >mnesia:create_schema([node()]).
 222 | %% >mnesia:start().
 223 | %% >ecron:install().
 224 | %% 
225 | %% @end 226 | %%------------------------------------------------------------------------------ 227 | install() -> 228 | install(mnesia:table_info(schema,disc_copies)). 229 | 230 | %%------------------------------------------------------------------------------ 231 | %% @spec install(Nodes) -> ok 232 | %% @doc Create mnesia tables on Nodes. 233 | %% @end 234 | %%------------------------------------------------------------------------------ 235 | install(Nodes) -> 236 | create_table(?JOB_COUNTER, [{disc_copies, Nodes}]), 237 | create_table(?JOB_TABLE, [{type, ordered_set}, 238 | {attributes, record_info(fields, job)}, 239 | {disc_copies, Nodes}]). 240 | 241 | %%------------------------------------------------------------------------------ 242 | %% @spec add_event_handler(Handler, Args) -> {ok, Pid} | {error, Reason} 243 | %% Handler = Module | {Module,Id} 244 | %% Module = atom() 245 | %% Id = term() 246 | %% Args = term() 247 | %% Pid = pid() 248 | %% @doc Adds a new event handler. The handler is added regardless of whether 249 | %% it's already present, thus duplicated handlers may exist. 250 | %% @end 251 | %%------------------------------------------------------------------------------ 252 | add_event_handler(Handler, Args) -> 253 | ecron_event_sup:start_handler(Handler, Args). 254 | 255 | %%------------------------------------------------------------------------------ 256 | %% @spec delete_event_handler(Pid) -> ok 257 | %% Pid = pid() 258 | %% @doc Deletes an event handler. Pid is the pid() returned by 259 | %% `add_event_handler/2'. 260 | %% @end 261 | %%------------------------------------------------------------------------------ 262 | delete_event_handler(Pid) -> 263 | ecron_event_sup:stop_handler(Pid). 264 | 265 | %%------------------------------------------------------------------------------ 266 | %% @spec list_event_handlers() -> [{Pid, Handler}] 267 | %% Handler = Module | {Module,Id} 268 | %% Module = atom() 269 | %% Id = term() 270 | %% Pid = pid() 271 | %% @doc Returns a list of all event handlers installed by the 272 | %% `ecron:add_event_handler/2' API or configured in the 273 | %% `event_handlers' environment variable. 274 | %% @end 275 | %%------------------------------------------------------------------------------ 276 | list_event_handlers() -> 277 | ecron_event_sup:list_handlers(). 278 | 279 | %%------------------------------------------------------------------------------ 280 | %% @spec insert(DateTime, MFA) -> ok 281 | %% DateTime = {Date, Time} 282 | %% Date = {Year, Month, Day} | '*' 283 | %% Time = {Hours, Minutes, Seconds} 284 | %% Year = integer() | '*' 285 | %% Month = integer() | '*' 286 | %% Day = integer() | '*' | last 287 | %% Hours = integer() 288 | %% Minutes = integer() 289 | %% Seconds = integer() 290 | %% MFA = {Module, Function, Args} 291 | %% @doc Schedules the MFA at the given Date and Time.
292 | %% Inserts the MFA into the queue to be scheduled at 293 | %% {Year,Month, Day},{Hours, Minutes,Seconds}
294 | %%
 295 | %% Month = 1..12 | '*'
 296 | %% Day = 1..31 | '*' | last
 297 | %% Hours = 0..23
 298 | %% Minutes = 0..59
 299 | %% Seconds = 0..59
 300 | %% 
301 | %% If `Day = last' then the MFA will be executed last day of the month. 302 | %% 303 | %% {'*', Time} runs the MFA every day at the given time and it's 304 | %% the same as writing {{'*','*','*'}, Time}. 305 | %% 306 | %% {{'*', '*', Day}, Time} runs the MFA every month at the given 307 | %% Day and Time. It must be `Day = 1..28 | last' 308 | %% 309 | %% {{'*', Month, Day}, Time} runs the MFA every year at the given 310 | %% Month, Day and Time. Day must be valid for the given month or the atom 311 | %% `last'. 312 | %% If `Month = 2' then it must be `Day = 1..28 | last' 313 | %% 314 | %% Combinations of the format {'*', Month, '*'} are not allowed. 315 | %% 316 | %% `{{Year, Month, Day}, Time}' runs the MFA at the given Date and Time. 317 | %% 318 | %% Returns `{error, Reason}' if invalid parameters have been passed. 319 | %% @end 320 | %%------------------------------------------------------------------------------ 321 | insert({Date, Time} = _DateTime, MFA) -> 322 | insert({Date, Time}, MFA, undefined, undefined). 323 | 324 | %%------------------------------------------------------------------------------ 325 | %% @spec insert(DateTime, MFA, Retry, RetrySeconds) -> ok 326 | %% DateTime = {Date, Time} 327 | %% Date = {Year, Month, Day} | '*' 328 | %% Time = {Hours, Minutes, Seconds} 329 | %% Year = integer() | '*' 330 | %% Month = integer() | '*' 331 | %% Day = integer() | '*' | last 332 | %% Hours = integer() 333 | %% Minutes = integer() 334 | %% Seconds = integer() 335 | %% Retry = integer() | infinity 336 | %% RetrySeconds = integer() 337 | %% MFA = {Module, Function, Args} 338 | %% @doc Schedules the MFA at the given Date and Time and retry if it fails. 339 | %% 340 | %% Same description of insert/2. Additionally if MFA returns 341 | %% `{error, Reason}' ecron will retry to execute 342 | %% it after `RetrySeconds'. The MFA will be rescheduled for a 343 | %% maximum of Retry times. If MFA returns `{apply, fun()}' and the 344 | %% return value of `fun()' is `{error, Reason}' the 345 | %% retry mechanism applies to `fun'. If Retry is equal to 3 346 | %% then MFA will be executed for a maximum of four times. The first time 347 | %% when is supposed to run according to the schedule and then three more 348 | %% times at interval of RetrySeconds. 349 | %% @end 350 | %%------------------------------------------------------------------------------ 351 | insert({Date, Time} = _DateTime, MFA, Retry, Seconds) -> 352 | case validate(Date, Time) of 353 | ok -> 354 | DueSec = sec({Date, Time}), 355 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 356 | Key = {DueSec, mnesia:dirty_update_counter(?JOB_COUNTER, job,1)}, 357 | Job = #job{key = Key, 358 | mfa = {MFA, DueTime}, 359 | schedule = {Date, Time}, 360 | retry = {Retry, Seconds}}, 361 | gen_server:cast(?MODULE, {insert, Job}); 362 | Error -> 363 | Error 364 | end. 365 | 366 | 367 | %% @spec list() -> JobList 368 | %% @doc Returns a list of job records defined in ecron.hrl 369 | %% @end 370 | %%------------------------------------------------------------------------------ 371 | list() -> 372 | gen_server:call(?MODULE, list, 60000). 373 | 374 | %%------------------------------------------------------------------------------ 375 | %% @spec print_list() -> ok 376 | %% @doc Prints a pretty list of records sorted by Job ID.
377 | %% E.g.
378 | %%
 379 | %% -----------------------------------------------------------------------
 380 | %% ID: 208
 381 | %% Function To Execute: mfa
 382 | %% Next Execution DateTime: {{2009,11,8},{15,59,54}}
 383 | %% Scheduled Execution DateTime: {{2009,11,8},{15,59,34}}
 384 | %% MFA: {ecron_tests,test_function,[fra]}
 385 | %% Schedule: {{'*','*',8},{15,59,34}}
 386 | %% Max Retry Times: 4
 387 | %% Retry Interval: 20
 388 | %% -----------------------------------------------------------------------
 389 | %% 
390 | %% `ID' is the Job ID and should be used as argument in 391 | %% `delete/1'.
392 | %% `Function To Execute' says if the job refers to the 393 | %% MFA or the `fun' returned by MFA. 394 | %% 395 | %% `Next Execution DateTime' is the date and time when 396 | %% the job will be executed. 397 | %% 398 | %% `Scheduled Execution DateTime' is the date and time 399 | %% when the job was supposed to be executed according to the given 400 | %% `Schedule'.`Next Execution DateTime' and 401 | %% `Scheduled Execution DateTime' are different if the MFA, or 402 | %% the `fun', failed and it will be retried later 403 | %% (as in the example given above). 404 | %% 405 | %% `MFA' is a tuple with Module, Function and Arguments as 406 | %% given when the job was inserted.
407 | %% `Schedule' is the schedule for the MFA as given when the 408 | %% job was insterted.
409 | %% `Max Retry Times' is the number of times ecron will retry to 410 | %% execute the job in case of failure. It may be less than the value given 411 | %% when the job was inserted if a failure and a retry has already occured. 412 | %% 413 | %% `Retry Interval' is the number of seconds ecron will wait 414 | %% after a failure before retrying to execute the job. It's the value given 415 | %% when the job was inserted. 416 | %% @end 417 | %%------------------------------------------------------------------------------ 418 | print_list() -> 419 | Jobs = gen_server:call(?MODULE, list, 60000), 420 | SortedJobs = lists:usort( 421 | fun(J1, J2) -> 422 | element(2, J1#job.key) =< element(2, J2#job.key) 423 | end, 424 | Jobs), 425 | lists:foreach( 426 | fun(Job) -> 427 | #job{key = {ExecSec, Id}, 428 | schedule = Schedule, 429 | mfa = {MFA, DueDateTime}, 430 | retry = {RetryTimes, Seconds}, 431 | client_fun = Fun 432 | } = Job, 433 | {Function, ExpectedDateTime} = 434 | case Fun of 435 | undefined -> {mfa, DueDateTime}; 436 | {_, DT} -> {'fun', DT} 437 | end, 438 | ExecDateTime = calendar:gregorian_seconds_to_datetime(ExecSec), 439 | io:format("~70c-~n",[$-]), 440 | io:format("ID: ~p~nFunction To Execute: ~p~n" 441 | "Next Execution DateTime: ~p~n", 442 | [Id, Function, ExecDateTime]), 443 | io:format("Scheduled Execution DateTime: ~w~nMFA: ~w~n" 444 | "Schedule: ~p~n", 445 | [ExpectedDateTime, MFA, Schedule]), 446 | io:format("Max Retry Times: ~p~nRetry Interval: ~p~n", 447 | [RetryTimes, Seconds]), 448 | io:format("~70c-~n",[$-]) 449 | end, SortedJobs). 450 | 451 | %%------------------------------------------------------------------------------ 452 | %% @spec refresh() -> ok 453 | %% @doc Deletes all jobs and recreates the table from the environment variables. 454 | %% @end 455 | %%------------------------------------------------------------------------------ 456 | refresh() -> 457 | gen_server:cast(?MODULE, refresh). 458 | 459 | %%------------------------------------------------------------------------------ 460 | %% @spec execute_all() -> ok 461 | %% @doc Executes all cron jobs in the queue, irrespective of the time they are 462 | %% scheduled to run. This might be used at startup and shutdown, ensuring no 463 | %% data is lost and backedup data is handled correctly.
464 | %% It asynchronously returns `ok' and then executes all the jobs 465 | %% in parallel.
466 | %% No retry will be executed even if the MFA, or the `fun', fails 467 | %% mechanism is enabled for that job. Also in case of periodic jobs MFA won't 468 | %% be rescheduled. Thus the jobs list will always be empty after calling 469 | %% `execute_all/0'. 470 | %% @end 471 | %%------------------------------------------------------------------------------ 472 | execute_all() -> 473 | gen_server:cast(?MODULE, execute_all). 474 | 475 | %%------------------------------------------------------------------------------ 476 | %% @spec delete(ID) -> ok 477 | %% @doc Deletes a cron job from the list. 478 | %% If the job does not exist, the function still returns ok 479 | %% @see print_list/0 480 | %% @end 481 | %%------------------------------------------------------------------------------ 482 | delete(ID) -> 483 | gen_server:cast(?MODULE, {delete, ID}). 484 | 485 | %%------------------------------------------------------------------------------ 486 | %% @spec delete_all() -> ok 487 | %% @doc Delete all the scheduled jobs 488 | %% @end 489 | %%------------------------------------------------------------------------------ 490 | delete_all() -> 491 | gen_server:cast(?MODULE, delete). 492 | 493 | %%============================================================================== 494 | %% gen_server callbacks 495 | %%============================================================================== 496 | 497 | %%------------------------------------------------------------------------------ 498 | %% @spec init(Args) -> {ok, State} 499 | %% @doc 500 | %% @private 501 | %% @end 502 | %%------------------------------------------------------------------------------ 503 | init(_Args) -> 504 | NewScheduled = 505 | case application:get_env(ecron, scheduled) of 506 | undefined -> []; 507 | {ok, MFAList} -> MFAList 508 | end, 509 | OldScheduled = mnesia:activity(async_dirty, 510 | fun() -> 511 | Objs = mnesia:select(?JOB_TABLE, 512 | [{#job{_ = '_'}, 513 | [], ['$_']}]), 514 | {atomic, ok} = mnesia:clear_table(?JOB_TABLE), 515 | Objs 516 | end), 517 | Scheduled = remove_duplicated(NewScheduled, OldScheduled, OldScheduled), 518 | lists:foreach(fun create_add_job/1, Scheduled), 519 | case mnesia:dirty_first(?JOB_TABLE) of 520 | {DueSec, _} -> 521 | {ok, [], get_timeout(DueSec)}; 522 | '$end_of_table' -> 523 | {ok, []} 524 | end. 525 | %%------------------------------------------------------------------------------ 526 | %% @spec handle_info(Info, State) -> {noreply, State} | 527 | %% {noreply, State, Timeout} | 528 | %% {stop, Reason, State} 529 | %% @doc Handling all non call/cast messages 530 | %% @private 531 | %% @end 532 | %%------------------------------------------------------------------------------ 533 | handle_info(timeout, State) -> 534 | case mnesia:dirty_first(?JOB_TABLE) of 535 | '$end_of_table' -> 536 | {noreply, State}; 537 | K -> 538 | check_job(K, State) 539 | end; 540 | 541 | handle_info(_, State) -> 542 | case mnesia:dirty_first(?JOB_TABLE) of 543 | {DueSec, _} -> 544 | {reply, ok, State, get_timeout(DueSec)}; 545 | '$end_of_table' -> 546 | {reply, ok, State} 547 | end. 548 | 549 | %%------------------------------------------------------------------------------ 550 | %% @spec handle_call(Request, From, State) -> {reply, Reply, State, Timeout}| 551 | %% {reply, Reply, State} 552 | %% @doc Handling call messages 553 | %% @private 554 | %% @end 555 | %%------------------------------------------------------------------------------ 556 | handle_call(list, _From, State) -> 557 | Keys = mnesia:dirty_all_keys(?JOB_TABLE), 558 | Jobs = lists:map(fun(Key) -> 559 | [J] = mnesia:dirty_read(?JOB_TABLE, Key), 560 | J 561 | end, Keys), 562 | case Keys of 563 | [] -> 564 | {reply, Jobs, State}; 565 | Keys -> 566 | {DueSec, _} = hd(Keys), 567 | {reply, Jobs, State, get_timeout(DueSec)} 568 | end. 569 | 570 | %%------------------------------------------------------------------------------ 571 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 572 | %% {noreply, State, Timeout} | 573 | %% {stop, Reason, State} 574 | %% @doc Handling cast messages 575 | %% @private 576 | %% @end 577 | %%------------------------------------------------------------------------------ 578 | handle_cast({insert, Job}, State) -> 579 | ok = mnesia:dirty_write(Job), 580 | {DueSec, _} = mnesia:dirty_first(?JOB_TABLE), 581 | {noreply, State, get_timeout(DueSec)}; 582 | 583 | handle_cast({delete, {_, _} = Key}, State) -> 584 | ok = mnesia:dirty_delete(?JOB_TABLE, Key), 585 | case mnesia:dirty_first(?JOB_TABLE) of 586 | {DueSec, _} -> 587 | {noreply, State, get_timeout(DueSec)}; 588 | '$end_of_table' -> 589 | {noreply, State} 590 | end; 591 | 592 | handle_cast({delete, ID}, State) -> 593 | ok = mnesia:activity(async_dirty, 594 | fun() -> 595 | case mnesia:select(?JOB_TABLE, 596 | [{#job{key = {'_', ID}, _='_'}, 597 | [], ['$_']}]) of 598 | [] -> 599 | ok; 600 | [Obj] -> 601 | ok = mnesia:delete_object(Obj) 602 | end 603 | end), 604 | case mnesia:dirty_first(?JOB_TABLE) of 605 | {DueSec, _} -> 606 | {noreply, State, get_timeout(DueSec)}; 607 | '$end_of_table' -> 608 | {noreply, State} 609 | end; 610 | 611 | handle_cast(delete, State) -> 612 | {atomic, ok} = mnesia:clear_table(?JOB_TABLE), 613 | {noreply, State}; 614 | 615 | handle_cast(execute_all, State) -> 616 | Scheduled = mnesia:activity(async_dirty, 617 | fun() -> 618 | Objs = mnesia:select(?JOB_TABLE, 619 | [{#job{_ = '_'}, 620 | [], ['$_']}]), 621 | {atomic, ok} = mnesia:clear_table(?JOB_TABLE), 622 | Objs 623 | end), 624 | lists:foreach(fun(Job) -> 625 | NoRetryJob = Job#job{retry={undefined, undefined}}, 626 | spawn(?MODULE, execute_job, [NoRetryJob, false]) 627 | end, Scheduled), 628 | case mnesia:dirty_first(?JOB_TABLE) of 629 | {DueSec, _} -> 630 | {noreply, State, get_timeout(DueSec)}; 631 | '$end_of_table' -> 632 | {noreply, State} 633 | end; 634 | 635 | handle_cast(refresh, State) -> 636 | {atomic, ok} = mnesia:clear_table(?JOB_TABLE), 637 | NewScheduled = 638 | case application:get_env(ecron, scheduled) of 639 | undefined -> []; 640 | {ok, MFAList} -> MFAList 641 | end, 642 | lists:foreach(fun create_add_job/1, NewScheduled), 643 | case mnesia:dirty_first(?JOB_TABLE) of 644 | {DueSec, _} -> 645 | {noreply, State, get_timeout(DueSec)}; 646 | '$end_of_table' -> 647 | {noreply, State} 648 | end; 649 | 650 | handle_cast(stop, State) -> 651 | {stop, normal, State}; 652 | handle_cast(_Msg, State) -> 653 | {noreply, State}. 654 | 655 | %%------------------------------------------------------------------------------ 656 | %% @spec terminate(Reason, State) -> void() 657 | %% Reason = term() 658 | %% State = term() 659 | %% @doc This function is called by a gen_server when it is about to 660 | %% terminate. It should be the opposite of Module:init/1 and do 661 | %% any necessary cleaning up. When it returns, the gen_server 662 | %% terminates with Reason. The return value is ignored. 663 | %% @private 664 | %% @end 665 | %%------------------------------------------------------------------------------ 666 | terminate(_Reason, _State) -> 667 | ok. 668 | 669 | %%------------------------------------------------------------------------------ 670 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 671 | %% @doc Convert process state when code is changed 672 | %% @private 673 | %% @end 674 | %%------------------------------------------------------------------------------ 675 | code_change(_OldVsn, State, _Extra) -> 676 | case mnesia:dirty_first(?JOB_TABLE) of 677 | {DueSec, _} -> 678 | {ok, State, get_timeout(DueSec)}; 679 | '$end_of_table' -> 680 | {ok, State} 681 | end. 682 | 683 | 684 | 685 | %%============================================================================== 686 | %% Internal functions 687 | %%============================================================================== 688 | sec() -> 689 | calendar:datetime_to_gregorian_seconds(ecron_time:localtime()). 690 | sec({'*', Time}) -> 691 | sec({{'*','*','*'}, Time}); 692 | 693 | sec({{'*','*','*'}, Time}) -> 694 | {Date1, Time1} = ecron_time:localtime(), 695 | Now = calendar:datetime_to_gregorian_seconds({Date1, Time1}), 696 | Due = calendar:datetime_to_gregorian_seconds({Date1, Time}), 697 | case Due - Now of 698 | Diff when Diff =<0 -> 699 | %% The Job will be executed tomorrow at Time 700 | Due + 86400; 701 | _Diff -> 702 | Due 703 | end; 704 | 705 | 706 | sec({{'*','*',Day}, Time}) -> 707 | {{Year1, Month1, Day1}, Time1} = ecron_time:localtime(), 708 | Now = calendar:datetime_to_gregorian_seconds({{Year1, Month1, Day1}, Time1}), 709 | RealDay = get_real_day(Year1, Month1, Day), 710 | Due = calendar:datetime_to_gregorian_seconds({{Year1, Month1, RealDay}, Time}), 711 | case Due - Now of 712 | Diff when Diff =<0 -> 713 | %% The Job will be executed next month 714 | DueDate = add_month({Year1, Month1, Day}), 715 | calendar:datetime_to_gregorian_seconds({DueDate, Time}); 716 | _Diff -> 717 | Due 718 | end; 719 | 720 | sec({{'*', Month, Day}, Time}) -> 721 | {{Year1, Month1, Day1}, Time1} = ecron_time:localtime(), 722 | Now = calendar:datetime_to_gregorian_seconds({{Year1, Month1, Day1}, Time1}), 723 | RealDay = get_real_day(Year1, Month, Day), 724 | Due = calendar:datetime_to_gregorian_seconds({{Year1, Month, RealDay}, Time}), 725 | case Due - Now of 726 | Diff when Diff =<0 -> 727 | %% The Job will be executed next year 728 | calendar:datetime_to_gregorian_seconds({{Year1+1, Month, Day}, Time}); 729 | _Diff -> 730 | Due 731 | end; 732 | 733 | sec({{Year, Month, Day}, Time}) -> 734 | RealDay = get_real_day(Year, Month, Day), 735 | calendar:datetime_to_gregorian_seconds({{Year, Month, RealDay}, Time}). 736 | 737 | add_month({Y, M, D}) -> 738 | case M of 739 | 12 -> {Y+1, 1, get_real_day(Y+1, 1, D)}; 740 | M -> {Y, M+1, get_real_day(Y, M+1, D)} 741 | end. 742 | 743 | get_real_day(Year, Month, last) -> 744 | calendar:last_day_of_the_month(Year, Month); 745 | 746 | get_real_day(_, _, Day) -> 747 | Day. 748 | 749 | %%------------------------------------------------------------------------------ 750 | %% @spec check_job(Key, State) -> {noreply, State} | {noreply, State, Timeout} 751 | %% @doc Checks if there's a job to execute in the table, extracts it and runs it 752 | %% @private 753 | %% @end 754 | %%------------------------------------------------------------------------------ 755 | check_job({Due, _} = K, State)-> 756 | case Due - sec() of 757 | Diff when Diff =< 0 -> 758 | [Job] = mnesia:dirty_read(?JOB_TABLE, K), 759 | ok = mnesia:dirty_delete(?JOB_TABLE, K), 760 | NeedReschedule = is_not_retried(Job), 761 | spawn(?MODULE, execute_job, [Job, NeedReschedule]), 762 | case mnesia:dirty_first(?JOB_TABLE) of 763 | '$end_of_table' -> 764 | {noreply, State}; 765 | K1-> 766 | check_job(K1, State) 767 | end; 768 | _Diff -> 769 | {noreply, State, get_timeout(Due)} 770 | end. 771 | 772 | %%------------------------------------------------------------------------------ 773 | %% @spec execute_job(Job, Reschedule) -> ok 774 | %% @doc Used internally. Execute the given Job. Reschedule it in case of a 775 | %% periodic job, or in case the date is in the future. 776 | %% @private 777 | %% @end 778 | %%------------------------------------------------------------------------------ 779 | execute_job(#job{client_fun = undefined, mfa = {{M, F, A}, DueTime}, 780 | retry = {RetryTimes, Interval}, schedule = Schedule}, 781 | Reschedule) -> 782 | ExecutionTime = ecron_time:localtime(), 783 | try apply(M, F, A) of 784 | {apply, Fun} when is_function(Fun, 0) -> 785 | notify({mfa_result, {apply, Fun}, {Schedule, {M, F, A}}, 786 | DueTime, ExecutionTime}), 787 | execute_fun( 788 | Fun, Schedule,{M, F, A}, DueTime, {RetryTimes, Interval}); 789 | ok -> 790 | notify({mfa_result, ok, {Schedule, {M, F, A}}, 791 | DueTime, ExecutionTime}); 792 | {ok, Data} -> 793 | notify({mfa_result, {ok, Data}, {Schedule, {M, F, A}}, 794 | DueTime, ExecutionTime}); 795 | {error, Reason} -> 796 | notify({mfa_result, {error, Reason}, {Schedule, {M, F, A}}, 797 | DueTime, ExecutionTime}), 798 | retry( 799 | {M, F, A}, undefined, Schedule, {RetryTimes, Interval}, DueTime); 800 | Return -> 801 | notify({mfa_result, Return, {Schedule, {M, F, A}}, DueTime, 802 | ExecutionTime}) 803 | catch 804 | _:Error -> 805 | notify( 806 | {mfa_result, Error, {Schedule, {M, F, A}}, DueTime, ExecutionTime}) 807 | end, 808 | case Reschedule of 809 | true -> insert(Schedule, {M, F, A}, RetryTimes, Interval); 810 | false -> ok 811 | end; 812 | 813 | 814 | execute_job(#job{client_fun = {Fun, DueTime}, 815 | mfa = {MFA, _}, 816 | schedule = Schedule, 817 | retry = Retry}, _) -> 818 | execute_fun(Fun, Schedule, MFA, DueTime, Retry). 819 | 820 | %%------------------------------------------------------------------------------ 821 | %% @spec execute_fun(Fun, Schedule, MFA, Time, Retry) -> ok 822 | %% @doc Executes the `fun' returned by MFA 823 | %% @private 824 | %% @end 825 | %%------------------------------------------------------------------------------ 826 | execute_fun(Fun, Schedule, MFA, DueTime, Retry) -> 827 | ExecutionTime = ecron_time:localtime(), 828 | try Fun() of 829 | ok -> 830 | notify({fun_result, ok, {Schedule, MFA}, DueTime, ExecutionTime}), 831 | ok; 832 | {ok, Data} -> 833 | notify({fun_result, {ok, Data}, {Schedule, MFA}, DueTime, 834 | ExecutionTime}), 835 | ok; 836 | {error, Reason} -> 837 | notify({fun_result, {error, Reason}, {Schedule, MFA}, DueTime, 838 | ExecutionTime}), 839 | retry(MFA, Fun, Schedule, Retry, DueTime); 840 | Error -> 841 | notify({fun_result, Error, {Schedule, MFA}, DueTime, ExecutionTime}) 842 | catch 843 | _:Error -> 844 | notify({fun_result, Error, {Schedule, MFA}, DueTime, 845 | ExecutionTime}) 846 | end. 847 | 848 | %%------------------------------------------------------------------------------ 849 | %% @spec retry(MFA, Fun, Schedule, Retry, Time) -> ok 850 | %% Retry = {RetryTimes, Seconds} 851 | %% RetryTimes = integer() 852 | %% Seconds = integer() 853 | %% @doc Reschedules the job if Retry options are given. 854 | %% Fun, or MFA if Fun is undefined, will be executed after Seconds. 855 | %% If RetryTimes is zero it means the job has been re-scheduled too many 856 | %% times therefore it won't be inserted again.
857 | %% An event is sent when the job is rescheduled and in case max number 858 | %% of retry is reached. 859 | %% @private 860 | %% @end 861 | %%------------------------------------------------------------------------------ 862 | retry(_MFA, _Fun, _Schedule, {undefined, undefined}, _) -> 863 | ok; 864 | retry(MFA, Fun, Schedule, {0, _Seconds}, DueTime) -> 865 | notify({max_retry, {Schedule, MFA}, Fun, DueTime}); 866 | retry(MFA, Fun, Schedule, {RetryTime, Seconds}, DueTime) -> 867 | notify({retry, {Schedule, MFA}, Fun, DueTime}), 868 | Now = sec(), 869 | DueSec = Now + Seconds, 870 | Key = {DueSec, mnesia:dirty_update_counter({job_counter, job},1)}, 871 | case Fun of 872 | undefined -> ClientFun = undefined; 873 | Fun -> ClientFun = {Fun, DueTime} 874 | end, 875 | Job = #job{key = Key, 876 | mfa = {MFA, DueTime}, 877 | schedule = Schedule, 878 | client_fun = ClientFun, 879 | retry = {RetryTime-1, Seconds}}, 880 | gen_server:cast(?MODULE, {insert, Job}). 881 | 882 | %%------------------------------------------------------------------------------ 883 | %% @spec create_add_job(Job) -> ok | error 884 | %% Job = {{Date, Time}, MFA} 885 | %% Job = {{Date, Time}, MFA, RetryTimes, Seconds} 886 | %% Job = #job{} 887 | %% @doc Used internally 888 | %% @private 889 | %% @end 890 | %%------------------------------------------------------------------------------ 891 | create_add_job({{Date, Time}, MFA}) -> 892 | create_add_job({{Date, Time}, MFA, undefined, undefined}); 893 | 894 | create_add_job({{Date, Time}, MFA, RetryTimes, Seconds}) -> 895 | case validate(Date, Time) of 896 | ok -> 897 | DueSec = sec({Date, Time}), 898 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 899 | Key = {DueSec, mnesia:dirty_update_counter({job_counter, job}, 1)}, 900 | Job = #job{key = Key, 901 | mfa = {MFA, DueTime}, 902 | schedule = {Date, Time}, 903 | retry = {RetryTimes, Seconds}}, 904 | ok = mnesia:dirty_write(Job); 905 | Error -> 906 | Error 907 | end; 908 | 909 | create_add_job(#job{client_fun = undefined, schedule = {Date, Time}} = Job) -> 910 | DueSec = sec({Date, Time}), 911 | Key = {DueSec, mnesia:dirty_update_counter({job_counter, job}, 1)}, 912 | ok = mnesia:dirty_write(Job#job{key = Key}); 913 | 914 | %% This entry was related to a previously failed MFA execution 915 | %% therefore it will retry after the configured retry_time 916 | create_add_job(#job{retry = {_, Seconds}} = Job) -> 917 | DueSec = sec() + Seconds, 918 | Key = {DueSec, mnesia:dirty_update_counter({job_counter, job}, 1)}, 919 | ok = mnesia:dirty_write(Job#job{key = Key}). 920 | 921 | %%------------------------------------------------------------------------------ 922 | %% @spec notify(Event) -> ok 923 | %% Event = term() 924 | %% @doc Sends the Event notification to all the configured event handlers 925 | %% @private 926 | %% @end 927 | %%------------------------------------------------------------------------------ 928 | notify(Event) -> 929 | ok = gen_event:notify(?EVENT_MANAGER, Event). 930 | 931 | validate('*', Time) -> 932 | validate({'*','*', '*'}, Time); 933 | 934 | validate(Date, Time) -> 935 | case validate_date(Date) andalso 936 | validate_time(Time) of 937 | true -> 938 | Now = sec(), 939 | case catch sec({Date, Time}) of 940 | {'EXIT', _} -> 941 | {error, date}; 942 | Sec when Sec - Now >0 -> 943 | ok; 944 | _ -> 945 | {error, date} 946 | end; 947 | false -> 948 | {error, date} 949 | end. 950 | 951 | validate_date({'*','*', '*'}) -> 952 | true; 953 | validate_date({'*','*', last}) -> 954 | true; 955 | validate_date({'*','*', Day}) when is_integer(Day), Day>0, Day<29 -> 956 | true; 957 | validate_date({'*', 2, Day}) when is_integer(Day), Day>0, Day<29 -> 958 | true; 959 | validate_date({'*', Month, last}) when is_integer(Month), Month>0, Month<13 -> 960 | true; 961 | validate_date({'*', Month, Day}) when is_integer(Day), is_integer(Month), 962 | Month>0, Month<13, Day>0 -> 963 | ShortMonths = [4,6,9,11], 964 | case lists:member(Month, ShortMonths) of 965 | true -> Day < 31; 966 | false -> Day < 32 967 | end; 968 | validate_date({Y, M, last}) -> 969 | is_integer(Y) andalso is_integer(M) 970 | andalso M>0 andalso M<13; 971 | 972 | validate_date(Date) -> 973 | try 974 | calendar:valid_date(Date) 975 | catch _:_ -> 976 | false 977 | end. 978 | 979 | validate_time({H, M, S}) -> 980 | is_integer(H) andalso is_integer(M) andalso is_integer(S) 981 | andalso H>=0 andalso H=<23 982 | andalso M>=0 andalso M=<59 983 | andalso S>=0 andalso S=<59; 984 | validate_time(_) -> 985 | false. 986 | 987 | 988 | %% return a lists of {Schedule,MFA} or {Schedule, MFA, RetryTimes, RetrySeconds} 989 | %% and #job{} where elements from the 990 | %% ScheduledMFAList are removed if they are present in the jobList 991 | remove_duplicated([], _, AccJobs) -> 992 | AccJobs; 993 | remove_duplicated([{Schedule, MFA}|T], JobList, AccJobs) -> 994 | case lists:any(fun(J) -> 995 | element(1,J#job.mfa)==MFA 996 | andalso J#job.schedule == Schedule 997 | andalso J#job.client_fun == undefined 998 | end, JobList) of 999 | true -> 1000 | %% The MFA was already present in the table 1001 | remove_duplicated(T, JobList, AccJobs); 1002 | false -> 1003 | remove_duplicated(T, JobList, [{Schedule, MFA}|AccJobs]) 1004 | end; 1005 | remove_duplicated([{Schedule, MFA, Retry, Sec}|T], JobList, AccJobs) -> 1006 | case lists:any(fun(J) -> 1007 | element(1,J#job.mfa) == MFA 1008 | andalso J#job.schedule == Schedule 1009 | andalso J#job.client_fun == undefined 1010 | andalso J#job.schedule == {Retry, Sec} 1011 | end, JobList) of 1012 | true -> 1013 | %% The MFA was already present in the table 1014 | remove_duplicated(T, JobList, AccJobs); 1015 | false -> 1016 | remove_duplicated(T, JobList, [{Schedule, MFA, Retry, Sec}|AccJobs]) 1017 | end. 1018 | 1019 | get_timeout(DueSec) -> 1020 | case DueSec - sec() of 1021 | Diff when Diff =< 0 -> 5; 1022 | Diff when Diff > 2592000 -> 1023 | 2592000000; %% Check the queue once every 30 days anyway 1024 | Diff -> Diff*1000 1025 | end. 1026 | 1027 | create_table(Table, Opts) -> 1028 | {atomic, ok} = mnesia:create_table(Table, Opts), 1029 | ok. 1030 | 1031 | %% Check whether it's the first attempt to execute a job and not a retried one 1032 | is_not_retried(#job{client_fun = undefined, key = {DueSec, _}, 1033 | mfa = {_, ExpectedDateTime}}) -> 1034 | DueDateTime = calendar:gregorian_seconds_to_datetime(DueSec), 1035 | DueDateTime == ExpectedDateTime; 1036 | is_not_retried(_) -> 1037 | false. 1038 | -------------------------------------------------------------------------------- /src/ecron.hrl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Francesca Gangemi 3 | %%% @doc 4 | %%% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%%----------------------------------------------------------------------------- 32 | -define(JOB_TABLE, job). 33 | -define(JOB_COUNTER, job_counter). 34 | 35 | -define(EVENT_MANAGER, event_mngr). 36 | 37 | -record(job, {key, %% {NextExecutionGregorianSeconds, Id} 38 | mfa, %% {MFA, DueDateTime} 39 | schedule, %% {Date, Time} 40 | client_fun, %% {fun(), DueDateTime} 41 | retry}). %% {RetryTimes, Seconds} 42 | -------------------------------------------------------------------------------- /src/ecron_app.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Francesca Gangemi 3 | %%% @doc Ecron application 4 | %%% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%%----------------------------------------------------------------------------- 32 | 33 | -module(ecron_app). 34 | -author('francesca.gangemi@erlang-solutions.com'). 35 | -copyright('Erlang Solutions Ltd.'). 36 | 37 | -export([start/2, 38 | stop/1]). 39 | 40 | -include("ecron.hrl"). 41 | 42 | %%============================================================================== 43 | %% API functions 44 | %%============================================================================== 45 | %%------------------------------------------------------------------------------ 46 | %% @spec start(_Type, _Args) -> Result 47 | %% _Type = atom() 48 | %% _Args = list() 49 | %% Result = {ok, pid()} | {timeout, BatTabList} | {error, Reason} 50 | %% @doc Start the ecron application 51 | %% @end 52 | %%------------------------------------------------------------------------------ 53 | start(_Type, _Args) -> 54 | case mnesia:wait_for_tables([?JOB_TABLE, ?JOB_COUNTER],10000) of 55 | ok -> 56 | {ok, Pid} = ecron_sup:start_link(), 57 | case application:get_env(ecron, event_handlers) of 58 | undefined -> EH = []; 59 | {ok, Handlers} -> EH = Handlers 60 | end, 61 | lists:foreach( 62 | fun({Handler, Args}) -> 63 | {ok, _} = ecron_event_sup:start_handler(Handler, Args) 64 | end, EH), 65 | {ok, Pid}; 66 | Error -> 67 | Error 68 | end. 69 | 70 | 71 | %%------------------------------------------------------------------------------ 72 | %% @spec stop(_Args) -> Result 73 | %% _Args = list() 74 | %% @doc Stop the ecron application 75 | %% @end 76 | %%------------------------------------------------------------------------------ 77 | stop(_Args) -> 78 | ok. 79 | 80 | -------------------------------------------------------------------------------- /src/ecron_event.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Francesca Gangemi 3 | %% @doc Event Handler 4 | %% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%------------------------------------------------------------------------------ 32 | -module(ecron_event). 33 | -author('francesca.gangemi@erlang-solutions.com'). 34 | -copyright('Erlang Solutions Ltd.'). 35 | 36 | 37 | -behaviour(gen_event). 38 | 39 | -export([init/1, 40 | handle_event/2, 41 | handle_call/2, 42 | handle_info/2, 43 | code_change/3, 44 | terminate/2]). 45 | 46 | init(_Args) -> 47 | {ok, []}. 48 | 49 | handle_event({mfa_result, ok, {Schedule, MFA}, 50 | DueDateTime, ExecutionDateTime}, State) -> 51 | io:format("***Executed ~p at ~w expected at ~w Result = ~p~n", 52 | [{Schedule, MFA}, 53 | ExecutionDateTime, DueDateTime, ok]), 54 | {ok, State}; 55 | 56 | handle_event({mfa_result, {ok, Data}, {Schedule, MFA}, 57 | DueDateTime, ExecutionDateTime}, State) -> 58 | io:format("***Executed ~p at ~w expected at ~w Result = ~p~n", 59 | [{Schedule, MFA}, 60 | ExecutionDateTime, DueDateTime, {ok, Data}]), 61 | {ok, State}; 62 | 63 | handle_event({mfa_result, {apply, Fun}, {Schedule, MFA}, 64 | DueDateTime, ExecutionDateTime}, State) -> 65 | io:format("***Executed ~p at ~w expected at ~w Result = ~p~n", 66 | [{Schedule, MFA}, 67 | ExecutionDateTime, DueDateTime, {apply, Fun}]), 68 | {ok, State}; 69 | 70 | handle_event({mfa_result, {error, Reason}, {Schedule, MFA}, 71 | DueDateTime, ExecutionDateTime}, State) -> 72 | io:format("***Executed ~p at ~w expected at ~w Result = ~p~n", 73 | [{Schedule, MFA}, 74 | ExecutionDateTime, DueDateTime, {error, Reason}]), 75 | {ok, State}; 76 | 77 | handle_event({mfa_result, Result, {Schedule, MFA}, 78 | DueDateTime, ExecutionDateTime}, State) -> 79 | io:format("***Executed ~p at ~w expected at ~w Result = ~p~n", 80 | [{Schedule, MFA}, 81 | ExecutionDateTime, DueDateTime, Result]), 82 | {ok, State}; 83 | 84 | handle_event({fun_result, Result, {Schedule, MFA}, 85 | DueDateTime, ExecutionDateTime}, State) -> 86 | io:format("***Executed fun from ~p at ~w expected at ~w Result = ~p~n", 87 | [{Schedule, MFA}, 88 | ExecutionDateTime, DueDateTime, Result]), 89 | {ok, State}; 90 | 91 | handle_event({retry, {Schedule, MFA}, undefined, DueDateTime}, State) -> 92 | io:format("***Retry ~p expected at ~w ~n", 93 | [{Schedule, MFA}, DueDateTime]), 94 | {ok, State}; 95 | 96 | handle_event({retry, {Schedule, MFA}, Fun, DueDateTime}, State) -> 97 | io:format("***Retry fun ~p from ~p expected at ~w ~n", 98 | [Fun, {Schedule, MFA}, DueDateTime]), 99 | {ok, State}; 100 | 101 | handle_event({max_retry, {Schedule, MFA}, undefined, DueDateTime}, State) -> 102 | io:format("***Max Retry reached ~p expected at ~p ~n", 103 | [{Schedule, MFA}, DueDateTime]), 104 | {ok, State}; 105 | 106 | handle_event({max_retry, {Schedule, MFA}, Fun, DueDateTime}, State) -> 107 | io:format("***Max Retry reached fun ~p from ~p expected at ~p ~n", 108 | [Fun, {Schedule, MFA}, DueDateTime]), 109 | {ok, State}; 110 | 111 | handle_event(Event, State) -> 112 | io:format("***Event =~p~n",[Event]), 113 | {ok, State}. 114 | 115 | %%------------------------------------------------------------------------------ 116 | %% @spec handle_call(Request, State) -> {ok, Reply, State} | 117 | %% {swap_handler, Reply, Args1, State1, 118 | %% Mod2, Args2} | 119 | %% {remove_handler, Reply} 120 | %% @doc Whenever an event manager receives a request sent using 121 | %% gen_event:call/3,4, this function is called for the specified event 122 | %% handler to handle the request. 123 | %% @end 124 | %%------------------------------------------------------------------------------ 125 | handle_call(_Request, State) -> 126 | Reply = ok, 127 | {ok, Reply, State}. 128 | 129 | %%------------------------------------------------------------------------------ 130 | %% @spec handle_info(Info, State) -> {ok, State} | 131 | %% {swap_handler, Args1, State1, Mod2, Args2} | 132 | %% remove_handler 133 | %% @doc This function is called for each installed event handler when 134 | %% an event manager receives any other message than an event or a synchronous 135 | %% request (or a system message). 136 | %% @end 137 | %%------------------------------------------------------------------------------ 138 | handle_info(_Info, State) -> 139 | {ok, State}. 140 | 141 | %%------------------------------------------------------------------------------ 142 | %% @spec terminate(Reason, State) -> void() 143 | %% @doc Whenever an event handler is deleted from an event manager, 144 | %% this function is called. It should be the opposite of Module:init/1 and 145 | %% do any necessary cleaning up. 146 | %% @end 147 | %%------------------------------------------------------------------------------ 148 | terminate(_Reason, _State) -> 149 | ok. 150 | 151 | %%------------------------------------------------------------------------------ 152 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 153 | %% @doc Convert process state when code is changed 154 | %% @end 155 | %%------------------------------------------------------------------------------ 156 | code_change(_OldVsn, State, _Extra) -> 157 | {ok, State}. 158 | -------------------------------------------------------------------------------- /src/ecron_event_handler_controller.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Francesca Gangemi 3 | %% @doc 4 | %% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%------------------------------------------------------------------------------ 32 | -module(ecron_event_handler_controller). 33 | -author('francesca.gangemi@erlang-solutions.com'). 34 | -copyright('Erlang Solutions Ltd.'). 35 | 36 | -export([start_link/2, 37 | init/3, 38 | system_continue/3, 39 | system_terminate/4 40 | ]). 41 | 42 | -include("ecron.hrl"). 43 | 44 | -record(state,{handler, parent, debug}). 45 | 46 | %%------------------------------------------------------------------------------ 47 | %% @spec start_link(Handler, Args) -> term() | {error, Reason} 48 | %% @doc 49 | %% @end 50 | %%------------------------------------------------------------------------------ 51 | start_link(Handler, Args) -> 52 | proc_lib:start_link(?MODULE, init, [self(), Handler, Args]). 53 | 54 | 55 | init(Parent, Handler, Args) -> 56 | ok = gen_event:add_sup_handler(?EVENT_MANAGER, Handler, Args), 57 | proc_lib:init_ack(Parent, {ok, self()}), 58 | loop(#state{handler = Handler, 59 | parent = Parent, 60 | debug = sys:debug_options([])}). 61 | 62 | loop(State) -> 63 | receive 64 | {system, From, Msg} -> 65 | handle_system_message(State, From, Msg), 66 | loop(State); 67 | {gen_event_EXIT, _Handler, normal} -> 68 | exit(normal); 69 | {gen_event_EXIT, _Handler, shutdown} -> 70 | exit(normal); 71 | {gen_event_EXIT, _Handler, {swapped, _NewHandler, _Pid}} -> 72 | loop(State); 73 | {gen_event_EXIT, Handler, Reason} -> 74 | error_logger:error_msg( 75 | "Handler ~p terminates with Reason=~p~nRestart it...", 76 | [Handler, Reason]), 77 | exit({handler_died, Reason}); 78 | terminate -> 79 | exit(normal); 80 | {get_handler, From} -> 81 | From ! {handler, State#state.handler}, 82 | loop(State); 83 | _E -> 84 | loop(State) 85 | end. 86 | 87 | 88 | 89 | handle_system_message(State, From, Msg) -> 90 | _ = sys:handle_debug(State#state.debug, fun write_debug/3, State, 91 | {system, From, Msg}), 92 | Parent = State#state.parent, 93 | Debug = State#state.debug, 94 | sys:handle_system_msg(Msg, From, Parent, ?MODULE, Debug, State). 95 | 96 | system_continue(_Parent, _Deb, State) -> 97 | loop(State). 98 | 99 | -spec system_terminate(any(), any(), any(), any()) -> no_return(). 100 | %% 101 | system_terminate(Reason, _Parent, _Deb, _State) -> 102 | exit(Reason). 103 | 104 | write_debug(Dev, Event, Name) -> 105 | io:format(Dev, "~p event = ~p~n", [Name, Event]). 106 | -------------------------------------------------------------------------------- /src/ecron_event_sup.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Francesca Gangemi 3 | %% @doc 4 | %% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%------------------------------------------------------------------------------ 32 | -module(ecron_event_sup). 33 | -author('francesca@erlang-consulting.com'). 34 | -copyright('Erlang Solutions Ltd.'). 35 | 36 | 37 | -behaviour(supervisor). 38 | 39 | %% API 40 | -export([start_handler/2, 41 | stop_handler/1, 42 | list_handlers/0 43 | ]). 44 | 45 | -export([start_link/0, 46 | init/1]). 47 | 48 | 49 | %%============================================================================== 50 | %% API functions 51 | %%============================================================================== 52 | %%------------------------------------------------------------------------------ 53 | %% @spec start_handler(Handler, Args) -> {ok,Pid} | {error,Error} 54 | %% @doc Starts a child that will add a new event handler 55 | %% @end 56 | %%------------------------------------------------------------------------------ 57 | start_handler(Handler, Args) -> 58 | supervisor:start_child(?MODULE, [Handler, Args]). 59 | 60 | %%------------------------------------------------------------------------------ 61 | %% @spec stop_handler(Pid) -> ok 62 | %% @doc Stop the child with the given Pid. It will terminate the associated 63 | %% event handler 64 | %% @end 65 | %%------------------------------------------------------------------------------ 66 | stop_handler(Pid) -> 67 | Children = supervisor:which_children(?MODULE), 68 | case lists:keysearch(Pid, 2, Children) of 69 | {value, {_, Pid, _, _}} -> 70 | Pid ! terminate, 71 | ok; 72 | false -> 73 | ok 74 | end. 75 | 76 | %%------------------------------------------------------------------------------ 77 | %% @spec list_handlers() -> [{Pid, Handler}] 78 | %% Handler = Module | {Module,Id} 79 | %% Module = atom() 80 | %% Id = term() 81 | %% Pid = pid() 82 | %% @doc Returns a list of all event handlers installed by the 83 | %% start_handler/2 API 84 | %% @end 85 | %%------------------------------------------------------------------------------ 86 | list_handlers() -> 87 | Children = supervisor:which_children(?MODULE), 88 | lists:foldl(fun({_, Pid, worker, [ecron_event_handler_controller]}, H) -> 89 | Pid ! {get_handler, self()}, 90 | receive 91 | {handler, Handler} -> 92 | [{Pid, Handler}|H] 93 | after 3000 -> 94 | [{Pid, undefined}|H] 95 | end 96 | end, [], Children). 97 | 98 | 99 | %%------------------------------------------------------------------------------ 100 | %% @spec init(Args) -> {ok,{SupFlags, [ChildSpec]}} | 101 | %% ignore 102 | %% @doc Whenever a supervisor is started using supervisor:start_link/2,3, 103 | %% this function is called by the new process to find out about restart strategy, 104 | %% maximum restart frequency and child specifications. 105 | %% @end 106 | %%------------------------------------------------------------------------------ 107 | init(_Args) -> 108 | 109 | Child_Spec = [{ecron_event_handler_controller, 110 | {ecron_event_handler_controller, start_link, []}, 111 | temporary, 5000, worker, 112 | [ecron_event_handler_controller]}], 113 | {ok,{{simple_one_for_one,3,1}, Child_Spec}}. 114 | 115 | 116 | %%------------------------------------------------------------------------------ 117 | %% @spec start_link() -> {ok,Pid} | ignore | {error,Error} 118 | %% @doc Starts the supervisor 119 | %% @end 120 | %%------------------------------------------------------------------------------ 121 | start_link() -> 122 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/ecron_sup.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @author Francesca Gangemi 3 | %%% @doc Supervisor module 4 | %%% @end 5 | 6 | %%% Copyright (c) 2009-2010 Erlang Solutions 7 | %%% All rights reserved. 8 | %%% 9 | %%% Redistribution and use in source and binary forms, with or without 10 | %%% modification, are permitted provided that the following conditions are met: 11 | %%% * Redistributions of source code must retain the above copyright 12 | %%% notice, this list of conditions and the following disclaimer. 13 | %%% * Redistributions in binary form must reproduce the above copyright 14 | %%% notice, this list of conditions and the following disclaimer in the 15 | %%% documentation and/or other materials provided with the distribution. 16 | %%% * Neither the name of the Erlang Solutions nor the names of its 17 | %%% contributors may be used to endorse or promote products 18 | %%% derived from this software without specific prior written permission. 19 | %%% 20 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | %%% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | %%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | %%% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | %%%----------------------------------------------------------------------------- 32 | -module(ecron_sup). 33 | -author('francesca.gangemi@erlang-solutions.com'). 34 | -copyright('Erlang Solutions Ltd.'). 35 | 36 | -export([start_link/0, 37 | init/1]). 38 | 39 | -include("ecron.hrl"). 40 | 41 | %%============================================================================== 42 | %% API functions 43 | %%============================================================================== 44 | 45 | %%------------------------------------------------------------------------------ 46 | %% @spec start_link() -> {ok,Pid} | ignore | {error,Error} 47 | %% @doc Starts the supervisor 48 | %% @end 49 | %%------------------------------------------------------------------------------ 50 | start_link() -> 51 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 52 | 53 | 54 | %%============================================================================== 55 | %% Supervisor callbacks 56 | %%============================================================================== 57 | 58 | %%------------------------------------------------------------------------------ 59 | %% @spec init(Args) -> {ok,{SupFlags, [ChildSpec]}} | 60 | %% ignore 61 | %% @doc Whenever a supervisor is started using supervisor:start_link/2,3, 62 | %% this function is called by the new process to find out about restart strategy, 63 | %% maximum restart frequency and child specifications. 64 | %% @end 65 | %%------------------------------------------------------------------------------ 66 | init(_Args) -> 67 | EventManager = {?EVENT_MANAGER, 68 | {gen_event, start_link, [{local, ?EVENT_MANAGER}]}, 69 | permanent, 10000, worker, [dynamic]}, 70 | 71 | Ecron = {ecron_server, {ecron, start_link, []}, 72 | permanent, 10000, worker, [ecron]}, 73 | 74 | EventSup = {ecron_event_sup, 75 | {ecron_event_sup, start_link, []}, 76 | permanent, 10000, supervisor, [ecron_event_sup]}, 77 | 78 | {ok,{{one_for_one,3,1}, [EventManager, EventSup, Ecron]}}. 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/ecron_time.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Ulf Wiger 3 | %% @doc Ecron counterparts of the built-in time functions. 4 | %% 5 | %% This module wraps the standard time functions, `erlang:localtime()', 6 | %% `erlang:universaltime()', `erlang:now()' and `os:timestamp()'. 7 | %% 8 | %% The reason for this is to enable mocking to simulate time within ecron. 9 | %% @end 10 | %% 11 | -module(ecron_time). 12 | -export([localtime/0, 13 | universaltime/0, 14 | now/0, 15 | timestamp/0]). 16 | 17 | 18 | localtime() -> 19 | erlang:localtime(). 20 | 21 | universaltime() -> 22 | erlang:universaltime(). 23 | 24 | now() -> 25 | erlang:now(). 26 | 27 | timestamp() -> 28 | os:timestamp(). 29 | -------------------------------------------------------------------------------- /test/ecron_event_handler_test.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------------------ 2 | %% @author Francesca Gangemi 3 | %% @doc Event Handler 4 | %% @end 5 | %%------------------------------------------------------------------------------ 6 | -module(ecron_event_handler_test). 7 | -author('francesca@erlang-consulting.com'). 8 | -copyright('Erlang Training & Consulting Ltd.'). 9 | 10 | 11 | -behaviour(gen_event). 12 | 13 | -export([init/1, 14 | handle_event/2, 15 | handle_call/2, 16 | handle_info/2, 17 | code_change/3, 18 | terminate/2]). 19 | 20 | init(_Args) -> 21 | ets:new(event_test, [named_table, public, duplicate_bag]), 22 | {ok, []}. 23 | 24 | 25 | handle_event(Event, State) -> 26 | ets:insert(event_test, Event), 27 | {ok, State}. 28 | 29 | %%------------------------------------------------------------------------------ 30 | %% @spec handle_call(Request, State) -> {ok, Reply, State} | 31 | %% {swap_handler, Reply, Args1, State1, 32 | %% Mod2, Args2} | 33 | %% {remove_handler, Reply} 34 | %% @doc Whenever an event manager receives a request sent using 35 | %% gen_event:call/3,4, this function is called for the specified event 36 | %% handler to handle the request. 37 | %% @end 38 | %%------------------------------------------------------------------------------ 39 | handle_call(_Request, State) -> 40 | Reply = ok, 41 | {ok, Reply, State}. 42 | 43 | %%------------------------------------------------------------------------------ 44 | %% @spec handle_info(Info, State) -> {ok, State} | 45 | %% {swap_handler, Args1, State1, Mod2, Args2} | 46 | %% remove_handler 47 | %% @doc This function is called for each installed event handler when 48 | %% an event manager receives any other message than an event or a synchronous 49 | %% request (or a system message). 50 | %% @end 51 | %%------------------------------------------------------------------------------ 52 | handle_info(_Info, State) -> 53 | {ok, State}. 54 | 55 | %%------------------------------------------------------------------------------ 56 | %% @spec terminate(Reason, State) -> void() 57 | %% @doc Whenever an event handler is deleted from an event manager, 58 | %% this function is called. It should be the opposite of Module:init/1 and 59 | %% do any necessary cleaning up. 60 | %% @end 61 | %%------------------------------------------------------------------------------ 62 | terminate(_Reason, _State) -> 63 | ets:delete(event_test), 64 | ok. 65 | 66 | %%------------------------------------------------------------------------------ 67 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 68 | %% @doc Convert process state when code is changed 69 | %% @end 70 | %%------------------------------------------------------------------------------ 71 | code_change(_OldVsn, State, _Extra) -> 72 | {ok, State}. 73 | -------------------------------------------------------------------------------- /test/ecron_tests.erl: -------------------------------------------------------------------------------- 1 | -module(ecron_tests). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include_lib("ecron/src/ecron.hrl"). 5 | 6 | -export([test_function/1, 7 | test_function1/1, 8 | test_not_ok_function/1, 9 | wrong_fun/1, 10 | wrong_mfa/1, 11 | ok_mfa/1, 12 | ok_mfa1/1, 13 | retry_mfa/1]). 14 | 15 | general_test_() -> 16 | {inorder, 17 | {setup, fun start_app/0, fun stop_app/1, 18 | [ 19 | ?_test(insert_validation()), 20 | ?_test(insert()), 21 | ?_test(insert1()), 22 | ?_test(insert2()), 23 | ?_test(insert3()), 24 | ?_test(insert_daily()), 25 | ?_test(insert_daily2()), 26 | ?_test(insert_daily3()), 27 | ?_test(insert_monthly()), 28 | ?_test(insert_yearly()), 29 | ?_test(insert_fun_not_ok()), 30 | {timeout, 10, ?_test(retry_mfa())}, 31 | ?_test(insert_wrong_fun()), 32 | ?_test(insert_wrong_mfa()), 33 | ?_test(delete_not_existent()), 34 | ?_test(insert_last()), 35 | ?_test(insert_last1()), 36 | ?_test(insert_last2()), 37 | ?_test(load_from_app_file()), 38 | {timeout, 10, ?_test(load_from_file_and_table())}, 39 | ?_test(refresh()), 40 | ?_test(execute_all()), 41 | ?_test(event_handler_api()) 42 | ]}}. 43 | 44 | start_app() -> 45 | ets:new(ecron_test, [named_table, public, duplicate_bag]), 46 | application:set_env(ecron, scheduled, []), 47 | mnesia:delete_schema([node()]), 48 | mnesia:create_schema([node()]), 49 | ok = application:start(mnesia), 50 | ok = ecron:install([node()]), 51 | ok = mnesia:wait_for_tables([?JOB_TABLE],10000), 52 | {atomic, ok} = mnesia:clear_table(?JOB_TABLE), 53 | ok = application:start(ecron), 54 | mock_datetime(), 55 | {ok, _} = ecron:add_event_handler(ecron_event_handler_test, []), 56 | ok = gen_event:delete_handler(?EVENT_MANAGER, ecron_event, []). 57 | 58 | mock_datetime() -> 59 | meck:new(ecron_time), 60 | BaseNow = {1313, 678000, 769000}, % equiv. {{2011,8,18}, {16,33,20}}. 61 | Diff = t_localtime_setup(BaseNow), 62 | meck:expect(ecron_time, localtime, fun() -> t_localtime(Diff) end), 63 | meck:expect(ecron_time, universaltime, 64 | fun() -> t_localtime(Diff) end), 65 | meck:expect(ecron_time, now, fun() -> t_now(Diff) end), 66 | meck:expect(ecron_time, timestamp, fun() -> t_now(Diff) end). 67 | 68 | t_localtime_setup(Now) -> 69 | RealNow = erlang:now(), 70 | DateTime = calendar:now_to_local_time(Now), 71 | RealDateTime = calendar:now_to_local_time(RealNow), 72 | Secs = calendar:datetime_to_gregorian_seconds(DateTime), 73 | RealSecs = calendar:datetime_to_gregorian_seconds(RealDateTime), 74 | {{Now, RealNow}, {DateTime, RealDateTime}, {Secs, RealSecs}}. 75 | 76 | t_localtime({_, _, {Secs, RealSecs}}) -> 77 | Cur = erlang:localtime(), 78 | CurSecs = calendar:datetime_to_gregorian_seconds(Cur), 79 | Diff = CurSecs - RealSecs, 80 | calendar:gregorian_seconds_to_datetime(Secs + Diff). 81 | 82 | t_now({{{MS1,S1,US1} = _Now, {MS2,S2,US2} = _RealNow}, _, _}) -> 83 | {MSc,Sc,USc} = erlang:now(), 84 | {MS1 + (MSc - MS2), S1 + (Sc - S2), US1 + (USc - US2)}. 85 | 86 | stop_app(_) -> 87 | application:stop(ecron), 88 | application:stop(mnesia), 89 | ets:delete(ecron_test). 90 | 91 | %% test ecron:insert/1 returns error when Date or Time are not correct 92 | insert_validation() -> 93 | {{Y, _M, _D},Time} = ecron_time:localtime(), 94 | ?assertMatch({error, _}, ecron:insert({4,{12,5,50}}, 95 | {ecron_tests, test_function, [any]})), 96 | ?assertMatch({error, _}, ecron:insert({{Y, 13, 2}, {12,5,5}}, 97 | {ecron_tests, test_function, [any]})), 98 | ?assertMatch({error, _}, ecron:insert({{2009, 1, wrong}, {12,5,5}}, 99 | {ecron_tests, test_function, [any]})), 100 | ?assertMatch({error, _}, ecron:insert({{2009, 12, 2}, {wrong,5,5}}, 101 | {ecron_tests, test_function, [any]})), 102 | ?assertMatch({error, _}, ecron:insert({{2009, 12, 2}, {'*',5,5}}, 103 | {ecron_tests, test_function, [any]})), 104 | ?assertMatch({error, _}, ecron:insert({{2009, 7, 2}, 2}, 105 | {ecron_tests, test_function, [any]})), 106 | ?assertMatch({error, _}, ecron:insert({{'*', '*', 29}, Time}, 107 | {ecron_tests, test_function, [any]})), 108 | ?assertMatch({error, _}, ecron:insert({{'*', 2, 29}, Time}, 109 | {ecron_tests, test_function, [any]})), 110 | ?assertMatch({error, _}, ecron:insert({{'*', 5, '*'}, Time}, 111 | {ecron_tests, test_function, [any]})), 112 | ?assertMatch({error, _}, ecron:insert({{'*', 4, 31}, Time}, 113 | {ecron_tests, test_function, [any]})), 114 | ?assertMatch({error, _}, ecron:insert({{'*', '*', '*'}, {60,0,0}}, 115 | {ecron_tests, test_function, [any]})), 116 | ?assertMatch({error, _}, ecron:insert({{'*', '*', '*'}, {56,60,0}}, 117 | {ecron_tests, test_function, [any]})), 118 | ?assertMatch({error, _}, ecron:insert({{'*', '*', '*'}, {56,45,60}}, 119 | {ecron_tests, test_function, [any]})), 120 | ?assertMatch({error, _}, ecron:insert({{Y-1, 7, 2}, Time}, 121 | {ecron_tests, test_function, [any]})). 122 | 123 | %% test job are correctly inserted into the queue with the right scheduled time 124 | %% Fixed Date 125 | %% MFA returns {apply, fun()} fun returns ok 126 | insert() -> 127 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 128 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 129 | {A,B,C} = now(), 130 | UniqueKey = lists:concat([A, "-", B, "-", C]), 131 | ?assertEqual(ok, ecron:insert(DateTime, 132 | {ecron_tests, test_function, [UniqueKey]})), 133 | timer:sleep(300), 134 | JobKey = mnesia:dirty_first(?JOB_TABLE), 135 | ?assertMatch({Schedule, _}, JobKey), 136 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, 137 | [UniqueKey]},DateTime}, 138 | schedule = DateTime, client_fun = undefined}], 139 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 140 | timer:sleep(2500), 141 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 142 | ?assertEqual([], ecron:list()), 143 | ?assertMatch([{fun_result, ok, 144 | {DateTime, {ecron_tests, test_function, [UniqueKey]}}, 145 | DateTime, DateTime}, 146 | {mfa_result, {apply, _}, 147 | {DateTime, {ecron_tests, test_function, [UniqueKey]}}, 148 | DateTime, DateTime}], ets:tab2list(event_test)), 149 | ets:delete_all_objects(ecron_test), 150 | ets:delete_all_objects(event_test). 151 | 152 | %% MFA returns {apply, fun()} fun returns {ok, Data} 153 | insert1() -> 154 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 155 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 156 | {A,B,C} = now(), 157 | UniqueKey = lists:concat([A, "-", B, "-", C]), 158 | ?assertEqual(ok, ecron:insert(DateTime, 159 | {ecron_tests, test_function1, [UniqueKey]})), 160 | timer:sleep(300), 161 | JobKey = mnesia:dirty_first(?JOB_TABLE), 162 | ?assertMatch({Schedule, _}, JobKey), 163 | ?assertMatch([#job{mfa = {{ecron_tests, test_function1, [UniqueKey]}, 164 | DateTime}, 165 | schedule = DateTime, client_fun = undefined}], 166 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 167 | timer:sleep(2500), 168 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 169 | ?assertEqual([], ecron:list()), 170 | ?assertMatch([{fun_result, {ok, UniqueKey}, 171 | {DateTime, {ecron_tests, test_function1, [UniqueKey]}}, 172 | DateTime, DateTime}, 173 | {mfa_result, {apply, _}, 174 | {DateTime, {ecron_tests, test_function1, [UniqueKey]}}, 175 | DateTime, DateTime}], ets:tab2list(event_test)), 176 | ets:delete_all_objects(ecron_test), 177 | ets:delete_all_objects(event_test). 178 | 179 | %% MFA returns ok 180 | insert2() -> 181 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 182 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 183 | {A,B,C} = now(), 184 | UniqueKey = lists:concat([A, "-", B, "-", C]), 185 | ?assertEqual(ok, ecron:insert(DateTime,{ecron_tests, ok_mfa, [UniqueKey]})), 186 | timer:sleep(300), 187 | JobKey = mnesia:dirty_first(?JOB_TABLE), 188 | ?assertMatch({Schedule, _}, JobKey), 189 | ?assertMatch([#job{mfa = {{ecron_tests, ok_mfa, [UniqueKey]},DateTime}, 190 | schedule = DateTime, client_fun = undefined}], 191 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 192 | timer:sleep(2500), 193 | ?assertEqual([{UniqueKey, Schedule}], ets:lookup(ecron_test, UniqueKey)), 194 | ?assertMatch([{mfa_result, ok, 195 | {DateTime, {ecron_tests, ok_mfa, [UniqueKey]}}, 196 | DateTime, DateTime}], ets:tab2list(event_test)), 197 | ?assertEqual([], ecron:list()), 198 | ets:delete_all_objects(ecron_test), 199 | ets:delete_all_objects(event_test). 200 | 201 | %% MFA returns {ok, Data} 202 | insert3() -> 203 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 204 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 205 | {A,B,C} = now(), 206 | UniqueKey = lists:concat([A, "-", B, "-", C]), 207 | ?assertEqual(ok, ecron:insert(DateTime,{ecron_tests, ok_mfa1, [UniqueKey]})), 208 | timer:sleep(300), 209 | JobKey = mnesia:dirty_first(?JOB_TABLE), 210 | ?assertMatch({Schedule, _}, JobKey), 211 | ?assertMatch([#job{mfa = {{ecron_tests, ok_mfa1, [UniqueKey]},DateTime}, 212 | schedule = DateTime, client_fun = undefined}], 213 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 214 | timer:sleep(2500), 215 | ?assertEqual([{UniqueKey, Schedule}], ets:lookup(ecron_test, UniqueKey)), 216 | ?assertMatch([{mfa_result, {ok, UniqueKey}, 217 | {DateTime, {ecron_tests, ok_mfa1, [UniqueKey]}}, 218 | DateTime, DateTime}], ets:tab2list(event_test)), 219 | ?assertEqual([], ecron:list()), 220 | ets:delete_all_objects(ecron_test), 221 | ets:delete_all_objects(event_test). 222 | 223 | %% Insert daily job, first execution in 2 seconds 224 | %% it also tests ecron:delete(Key) 225 | %% {{'*','*','*'}, Time} 226 | insert_daily() -> 227 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 228 | {{Y,M,D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule), 229 | {A,B,C} = now(), 230 | UniqueKey = lists:concat([A, "-", B, "-", C]), 231 | ?assertEqual(ok, ecron:insert({{'*', '*', '*'}, Time}, 232 | {ecron_tests, test_function, [UniqueKey]})), 233 | timer:sleep(300), 234 | ?assertMatch({Schedule, _}, mnesia:dirty_first(?JOB_TABLE)), 235 | JobKey = mnesia:dirty_first(?JOB_TABLE), 236 | ?assertMatch({Schedule, _}, JobKey), 237 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, 238 | {{Y,M,D}, Time}}, 239 | schedule = {{'*', '*', '*'}, Time}, 240 | client_fun = undefined, 241 | retry = {undefined, undefined}}], 242 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 243 | timer:sleep(2500), 244 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 245 | DueSec = Schedule+3600*24, 246 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 247 | [Job] = ecron:list(), 248 | ?assertMatch(#job{mfa = {{ecron_tests,test_function,[UniqueKey]},DueTime}, 249 | key = {DueSec, _}, 250 | schedule = {{'*', '*', '*'}, Time}, client_fun = undefined, 251 | retry = {undefined, undefined}}, Job), 252 | ?assertEqual(ok, ecron:delete(Job#job.key)), 253 | ?assertEqual([], ecron:list()), 254 | ets:delete_all_objects(ecron_test), 255 | ets:delete_all_objects(event_test). 256 | 257 | %% {'*', Time} and retry options 258 | insert_daily2() -> 259 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 260 | {{Y,M,D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule), 261 | {A,B,C} = now(), 262 | UniqueKey = lists:concat([A, "-", B, "-", C]), 263 | ?assertEqual(ok, ecron:insert({'*', Time}, 264 | {ecron_tests, test_function, [UniqueKey]}, 265 | 3, 1)), 266 | timer:sleep(300), 267 | ?assertMatch({Schedule, _}, mnesia:dirty_first(?JOB_TABLE)), 268 | JobKey = mnesia:dirty_first(?JOB_TABLE), 269 | ?assertMatch({Schedule, _}, JobKey), 270 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, 271 | {{Y,M,D}, Time}}, 272 | schedule = {'*', Time}, 273 | client_fun = undefined, retry = {3, 1}}], 274 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 275 | timer:sleep(2500), 276 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 277 | DueSec = Schedule+3600*24, 278 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 279 | [Job] = ecron:list(), 280 | ?assertMatch(#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, 281 | DueTime}, 282 | key = {DueSec, _}, 283 | schedule = {'*', Time}, 284 | client_fun = undefined, 285 | retry = {3, 1}}, Job), 286 | ?assertEqual(ok, ecron:delete(Job#job.key)), 287 | ?assertEqual([], ecron:list()), 288 | ets:delete_all_objects(ecron_test), 289 | ets:delete_all_objects(event_test). 290 | 291 | %% {'*', Time}, MFA returns {error, Reason} 292 | insert_daily3() -> 293 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 294 | {{Y,M,D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule), 295 | {A,B,C} = now(), 296 | UniqueKey = lists:concat([A, "-", B, "-", C]), 297 | ?assertEqual(ok, ecron:insert({'*', Time}, 298 | {ecron_tests, retry_mfa, [UniqueKey]}, 3, 1)), 299 | timer:sleep(300), 300 | ?assertMatch({Schedule, _}, mnesia:dirty_first(?JOB_TABLE)), 301 | JobKey = mnesia:dirty_first(?JOB_TABLE), 302 | ?assertMatch({Schedule, _}, JobKey), 303 | ?assertMatch([#job{mfa = {{ecron_tests, retry_mfa, [UniqueKey]}, 304 | {{Y,M,D}, Time}}, 305 | schedule = {'*', Time}, 306 | client_fun = undefined, 307 | retry = {3, 1}}], 308 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 309 | timer:sleep(2500), 310 | ?assertEqual([{UniqueKey, Schedule}], ets:lookup(ecron_test, UniqueKey)), 311 | DueSec = Schedule+3600*24, 312 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 313 | RetryDueSec = Schedule+1, 314 | [Job, Job1] = ecron:list(), 315 | ?assertMatch(#job{mfa = {{ecron_tests, retry_mfa, [UniqueKey]}, DueTime}, 316 | key = {DueSec, _}, 317 | schedule = {'*', Time}, 318 | client_fun = undefined, 319 | retry = {3, 1}}, Job1), 320 | ?assertMatch(#job{mfa = {{ecron_tests, retry_mfa, [UniqueKey]}, 321 | {{Y,M,D}, Time}}, 322 | key = {RetryDueSec, _}, schedule = {'*', Time}, 323 | client_fun = undefined, retry = {2, 1}}, Job), 324 | ?assertEqual(ok, ecron:delete(element(2, Job#job.key))), 325 | ?assertEqual(ok, ecron:delete(element(2, Job1#job.key))), 326 | ?assertEqual([], ecron:list()), 327 | ets:delete_all_objects(ecron_test), 328 | ets:delete_all_objects(event_test). 329 | 330 | %% Insert monthly job, first execution in 2 seconds 331 | %% it also tests ecron:delete(Key) 332 | insert_monthly() -> 333 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 334 | {{Y, M, D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule), 335 | {A,B,C} = now(), 336 | UniqueKey = lists:concat([A, "-", B, "-", C]), 337 | ?assertEqual(ok, ecron:insert({{'*', '*', D}, Time}, 338 | {ecron_tests, test_function, [UniqueKey]})), 339 | timer:sleep(300), 340 | ?assertMatch({Schedule, _}, mnesia:dirty_first(?JOB_TABLE)), 341 | JobKey = mnesia:dirty_first(?JOB_TABLE), 342 | ?assertMatch({Schedule, _}, JobKey), 343 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, 344 | {{Y, M, D}, Time}}, 345 | schedule = {{'*', '*', D}, Time}, 346 | client_fun = undefined}], 347 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 348 | timer:sleep(2500), 349 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 350 | DueSec = calendar:datetime_to_gregorian_seconds( 351 | {add_month({Y, M, D}), Time}), 352 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 353 | [Job] = ecron:list(), 354 | % ?debugFmt("UniqueKey~p DueSec=~p Time=~p~n",[UniqueKey, DueSec, Time]), 355 | ?assertMatch(#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, DueTime}, 356 | key = {DueSec, _}, 357 | schedule = {{'*', '*', D}, Time}, 358 | client_fun = undefined}, Job), 359 | ?assertEqual(ok, ecron:delete(Job#job.key)), 360 | ?assertEqual([], ecron:list()), 361 | ets:delete_all_objects(ecron_test), 362 | ets:delete_all_objects(event_test). 363 | 364 | %% Insert yearly job, first execution in 2 seconds 365 | %% it also tests ecron:delete(JobId) 366 | insert_yearly() -> 367 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 368 | {{Y, M, D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule), 369 | {A,B,C} = now(), 370 | UniqueKey = lists:concat([A, "-", B, "-", C]), 371 | ?assertEqual(ok, ecron:insert({{'*', M, D}, Time}, 372 | {ecron_tests, test_function, [UniqueKey]})), 373 | timer:sleep(300), 374 | ?assertMatch({Schedule, _}, mnesia:dirty_first(?JOB_TABLE)), 375 | JobKey = mnesia:dirty_first(?JOB_TABLE), 376 | ?assertMatch({Schedule, _}, JobKey), 377 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey]}, 378 | {{Y, M, D}, Time}}, 379 | schedule = {{'*', M, D}, Time}, 380 | client_fun = undefined}], 381 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 382 | timer:sleep(2500), 383 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 384 | DueSec = calendar:datetime_to_gregorian_seconds({{Y+1, M, D}, Time}), 385 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 386 | [Job] = ecron:list(), 387 | ?assertMatch(#job{mfa = {{ecron_tests, test_function, [UniqueKey]},DueTime}, 388 | key = {DueSec, _}, 389 | schedule = {{'*', M, D}, Time}, 390 | client_fun = undefined}, Job), 391 | ?assertEqual(ok, ecron:delete(element(2, Job#job.key))), 392 | ?assertEqual([], ecron:list()), 393 | ets:delete_all_objects(ecron_test), 394 | ets:delete_all_objects(event_test). 395 | 396 | %% retry MFA 3 times 397 | retry_mfa() -> 398 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 399 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 400 | {A,B,C} = now(), 401 | UniqueKey = lists:concat([A, "-", B, "-", C]), 402 | ?assertEqual(ok, ecron:insert(DateTime, 403 | {ecron_tests, retry_mfa, [UniqueKey]}, 3, 1)), 404 | timer:sleep(300), 405 | JobKey = mnesia:dirty_first(?JOB_TABLE), 406 | ?assertMatch({Schedule, _}, JobKey), 407 | ?assertMatch([#job{mfa = {{ecron_tests, retry_mfa, [UniqueKey]},DateTime}, 408 | schedule = DateTime, 409 | client_fun = undefined, retry = {3,1}}], 410 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 411 | 412 | timer:sleep(2100), 413 | DueSec = Schedule+1, 414 | [Job] = ecron:list(), 415 | ?assertMatch(#job{mfa = {{ecron_tests, retry_mfa, [UniqueKey]},DateTime}, 416 | key = {DueSec, _}, 417 | schedule = DateTime, client_fun = _, retry = {2,1}}, Job), 418 | DueSec1 = Schedule+2, 419 | ExecTime1 = calendar:gregorian_seconds_to_datetime(DueSec), 420 | DueSec2 = Schedule+3, 421 | ExecTime2 = calendar:gregorian_seconds_to_datetime(DueSec1), 422 | timer:sleep(3100), 423 | ?assertEqual([{UniqueKey, Schedule}, 424 | {UniqueKey, Schedule+1}, 425 | {UniqueKey, Schedule+2}, 426 | {UniqueKey, Schedule+3}], 427 | ets:tab2list(ecron_test)), 428 | ExecTime3 = calendar:gregorian_seconds_to_datetime(DueSec2), 429 | Events = ets:tab2list(event_test), 430 | ?assertMatch([{max_retry, 431 | {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 432 | undefined, DateTime}, 433 | {mfa_result, {error, retry}, 434 | {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 435 | DateTime, DateTime}, 436 | {mfa_result, {error, retry}, 437 | {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 438 | DateTime, ExecTime1}, 439 | {mfa_result, {error, retry}, 440 | {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 441 | DateTime, ExecTime2}, 442 | {mfa_result, {error, retry}, 443 | {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 444 | DateTime, ExecTime3}, 445 | {retry, {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 446 | undefined, DateTime}, 447 | {retry, {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 448 | undefined, DateTime}, 449 | {retry, {DateTime, {ecron_tests, retry_mfa, [UniqueKey]}}, 450 | undefined, DateTime} 451 | ], 452 | lists:keysort(1,Events)), 453 | ets:delete_all_objects(event_test), 454 | ?assertEqual([], ecron:list()), 455 | ets:delete_all_objects(ecron_test). 456 | 457 | %% Insert job to be scheduled on the last day of the current month 458 | insert_last() -> 459 | TmpSchedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 460 | {{Y,M,_}, Time} = calendar:gregorian_seconds_to_datetime(TmpSchedule), 461 | LastDay = calendar:last_day_of_the_month(Y, M), 462 | Schedule = calendar:datetime_to_gregorian_seconds({{Y, M, LastDay}, Time}), 463 | {A,B,C} = now(), 464 | UniqueKey = lists:concat([A, "-", B, "-", C]), 465 | ?assertEqual(ok, ecron:insert({{Y, M, last}, Time},{ecron_tests, ok_mfa, [UniqueKey]})), 466 | timer:sleep(300), 467 | JobKey = mnesia:dirty_first(?JOB_TABLE), 468 | ?assertMatch({Schedule, _}, JobKey), 469 | ?assertMatch([#job{mfa = {{ecron_tests, ok_mfa, [UniqueKey]}, 470 | {{Y, M, LastDay}, Time}}, 471 | schedule = {{Y, M, last}, Time}, 472 | client_fun = undefined}], 473 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 474 | ?assertEqual(1, length(ecron:list())), 475 | ?assertEqual(ok, ecron:delete_all()), 476 | ?assertEqual([], ecron:list()), 477 | ets:delete_all_objects(ecron_test), 478 | ets:delete_all_objects(event_test). 479 | 480 | %% Insert yearly job to be scheduled on the last day of the current month 481 | insert_last1() -> 482 | TmpSchedule = calendar:datetime_to_gregorian_seconds( 483 | ecron_time:localtime())+2, 484 | {{Y,M,_}, Time} = calendar:gregorian_seconds_to_datetime(TmpSchedule), 485 | LastDay = calendar:last_day_of_the_month(Y, M), 486 | Schedule = calendar:datetime_to_gregorian_seconds({{Y, M, LastDay}, Time}), 487 | {A,B,C} = now(), 488 | UniqueKey = lists:concat([A, "-", B, "-", C]), 489 | ?assertEqual(ok, ecron:insert({{'*', M, last}, Time}, 490 | {ecron_tests, ok_mfa, [UniqueKey]})), 491 | timer:sleep(300), 492 | JobKey = mnesia:dirty_first(?JOB_TABLE), 493 | ?assertMatch({Schedule, _}, JobKey), 494 | ?assertMatch([#job{mfa = {{ecron_tests, ok_mfa, [UniqueKey]}, 495 | {{Y, M, LastDay}, Time}}, 496 | schedule = {{'*', M, last}, Time}, 497 | client_fun = undefined}], 498 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 499 | ?assertEqual(1, length(ecron:list())), 500 | ?assertEqual(ok, ecron:delete_all()), 501 | ?assertEqual([], ecron:list()), 502 | ets:delete_all_objects(ecron_test), 503 | ets:delete_all_objects(event_test). 504 | 505 | %% Insert monthly job to be scheduled on the last day of a month 506 | insert_last2() -> 507 | TmpSchedule = calendar:datetime_to_gregorian_seconds( 508 | ecron_time:localtime())+2, 509 | {{Y,M,_}, Time} = calendar:gregorian_seconds_to_datetime(TmpSchedule), 510 | LastDay = calendar:last_day_of_the_month(Y, M), 511 | Schedule = calendar:datetime_to_gregorian_seconds({{Y, M, LastDay}, Time}), 512 | {A,B,C} = now(), 513 | UniqueKey = lists:concat([A, "-", B, "-", C]), 514 | ?assertEqual(ok, ecron:insert({{'*', '*', last}, Time}, 515 | {ecron_tests, ok_mfa, [UniqueKey]})), 516 | timer:sleep(300), 517 | JobKey = mnesia:dirty_first(?JOB_TABLE), 518 | ?assertMatch({Schedule, _}, JobKey), 519 | ?assertMatch([#job{mfa = {{ecron_tests, ok_mfa, [UniqueKey]}, 520 | {{Y, M, LastDay}, Time}}, 521 | schedule = {{'*', '*', last}, Time}, 522 | client_fun = undefined}], 523 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 524 | ?assertEqual(1, length(ecron:list())), 525 | ?assertEqual(ok, ecron:delete_all()), 526 | ?assertEqual([], ecron:list()), 527 | ets:delete_all_objects(ecron_test), 528 | ets:delete_all_objects(event_test). 529 | 530 | 531 | %% test the fun returned by MFA is rescheduled if it doesn't return ok. 532 | %% It checks that the time in test_not_ok_function is the one related 533 | %% to the scheduled time not the retry time to make sure it executes 534 | %% the fun returned by MFA and not the MFA itself 535 | insert_fun_not_ok() -> 536 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 537 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 538 | {A,B,C} = now(), 539 | UniqueKey = lists:concat([A, "-", B, "-", C]), 540 | ?assertEqual(ok, ecron:insert( 541 | DateTime, {ecron_tests,test_not_ok_function,[UniqueKey]}, 542 | 5, 1)), 543 | timer:sleep(300), 544 | JobKey = mnesia:dirty_first(?JOB_TABLE), 545 | ?assertMatch({Schedule, _}, JobKey), 546 | ?assertMatch([#job{mfa = {{ecron_tests,test_not_ok_function,[UniqueKey]}, 547 | DateTime}, 548 | schedule = DateTime, 549 | client_fun = undefined, 550 | retry = {5,1}}], 551 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 552 | timer:sleep(2100), 553 | ?assertEqual([{UniqueKey, Schedule, Schedule}], ets:lookup(ecron_test, UniqueKey)), 554 | DueSec = Schedule+1, 555 | [Job] = ecron:list(), 556 | ?assertMatch(#job{mfa = {{ecron_tests,test_not_ok_function,[UniqueKey]}, 557 | DateTime}, key = {DueSec, _}, 558 | schedule = DateTime, 559 | client_fun = _, 560 | retry = {4,1}}, Job), 561 | Events = ets:tab2list(event_test), 562 | ?assertMatch([{retry, 563 | {DateTime, {ecron_tests,test_not_ok_function,[UniqueKey]}}, 564 | _, DateTime}, 565 | {fun_result, {error, retry}, 566 | {DateTime, {ecron_tests, test_not_ok_function, [UniqueKey]}}, 567 | DateTime, DateTime}, 568 | {mfa_result, {apply, _}, 569 | {DateTime, {ecron_tests, test_not_ok_function, [UniqueKey]}}, 570 | DateTime, DateTime}], Events), 571 | [RetryEvent|_] = Events, 572 | ?assert(element(3, RetryEvent) /= undefined), 573 | timer:sleep(1100), 574 | [Job1] = ecron:list(), 575 | TableEntries = ets:lookup(ecron_test, UniqueKey), 576 | %% It checks that at least a retry has been executed 577 | ?assert(length(TableEntries) >=2), 578 | ?assert(lists:member({UniqueKey, Schedule, Schedule}, TableEntries)), 579 | ?assert(lists:member({UniqueKey, Schedule, DueSec}, TableEntries)), 580 | ?assertEqual(ok, ecron:delete(Job1#job.key)), 581 | ?assertEqual([], ecron:list()), 582 | ets:delete_all_objects(ecron_test), 583 | ets:delete_all_objects(event_test). 584 | 585 | %% test that MFA is executed at the given time and no retry is executed 586 | %% since it doesn't return {error, Reason} 587 | insert_wrong_mfa() -> 588 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 589 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 590 | {A,B,C} = now(), 591 | UniqueKey = lists:concat([A, "-", B, "-", C]), 592 | ?assertEqual(ok, ecron:insert( 593 | DateTime,{ecron_tests, wrong_mfa, [UniqueKey]}, 3, 1)), 594 | timer:sleep(300), 595 | JobKey = mnesia:dirty_first(?JOB_TABLE), 596 | ?assertMatch({Schedule, _}, JobKey), 597 | ?assertMatch([#job{mfa = {{ecron_tests, wrong_mfa, [UniqueKey]}, DateTime}, 598 | schedule = DateTime, client_fun = undefined}], 599 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 600 | timer:sleep(2500), 601 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 602 | ?assertMatch([{mfa_result, wrong, 603 | {DateTime, {ecron_tests, wrong_mfa, [UniqueKey]}}, 604 | DateTime, DateTime}], ets:tab2list(event_test)), 605 | ?assertEqual([], ecron:list()), 606 | ets:delete_all_objects(ecron_test), 607 | ets:delete_all_objects(event_test). 608 | 609 | %% test that fun is executed at the given time and no retry is executed 610 | %% since it doesn't return {error, Reason} 611 | insert_wrong_fun() -> 612 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 613 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 614 | {A,B,C} = now(), 615 | UniqueKey = lists:concat([A, "-", B, "-", C]), 616 | ?assertEqual(ok, ecron:insert( 617 | DateTime,{ecron_tests, wrong_fun, [UniqueKey]}, 3, 1)), 618 | timer:sleep(300), 619 | JobKey = mnesia:dirty_first(?JOB_TABLE), 620 | ?assertMatch({Schedule, _}, JobKey), 621 | ?assertMatch([#job{mfa = {{ecron_tests, wrong_fun, [UniqueKey]}, DateTime}, 622 | schedule = DateTime, client_fun = _}], 623 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 624 | timer:sleep(2500), 625 | ?assertEqual([{UniqueKey, Schedule}], ets:lookup(ecron_test, UniqueKey)), 626 | ?assertMatch([{fun_result, wrong, 627 | {DateTime, {ecron_tests, wrong_fun, [UniqueKey]}}, 628 | DateTime, DateTime}, 629 | {mfa_result, {apply, _}, 630 | {DateTime, {ecron_tests, wrong_fun, [UniqueKey]}}, 631 | DateTime, DateTime}], ets:tab2list(event_test)), 632 | ?assertEqual([], ecron:list()), 633 | ets:delete_all_objects(ecron_test), 634 | ets:delete_all_objects(event_test). 635 | 636 | load_from_app_file() -> 637 | application:stop(ecron), 638 | 639 | Schedule = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+2, 640 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 641 | {A,B,C} = now(), 642 | UniqueKey = lists:concat([A, "-", B, "-", C]), 643 | Schedule1 = calendar:datetime_to_gregorian_seconds(ecron_time:localtime())+3, 644 | {{Y,M,D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule1), 645 | {A1,B1,C1} = now(), 646 | UniqueKey1 = lists:concat([A1, "-", B1, "-", C1]), 647 | application:set_env(ecron, scheduled, 648 | [{DateTime,{ecron_tests, test_function, [UniqueKey]}}, 649 | {{{'*', '*', '*'}, Time}, 650 | {ecron_tests, test_function, [UniqueKey1]}}]), 651 | timer:sleep(1000), 652 | application:start(ecron), 653 | {ok, _} = ecron:add_event_handler(ecron_event_handler_test, []), 654 | ok = gen_event:delete_handler(?EVENT_MANAGER, ecron_event, []), 655 | 656 | JobKey = mnesia:dirty_first(?JOB_TABLE), 657 | ?assertMatch({Schedule, _}, JobKey), 658 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, 659 | [UniqueKey]}, DateTime}, 660 | schedule = DateTime, client_fun = undefined}], 661 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 662 | JobKey1 = mnesia:dirty_next(?JOB_TABLE, JobKey), 663 | ?assertMatch({Schedule1, _}, JobKey1), 664 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, 665 | [UniqueKey1]}, {{Y,M,D}, Time}}, 666 | schedule = {{'*', '*', '*'}, Time}, 667 | client_fun = undefined}], 668 | mnesia:dirty_read(?JOB_TABLE, JobKey1)), 669 | timer:sleep(2500), 670 | ?assertEqual([{UniqueKey, test}], ets:lookup(ecron_test, UniqueKey)), 671 | ?assertEqual([{UniqueKey1, test}], ets:lookup(ecron_test, UniqueKey1)), 672 | [Job1] = ecron:list(), 673 | DueSec = Schedule1+3600*24, 674 | DueTime = calendar:gregorian_seconds_to_datetime(DueSec), 675 | ?assertMatch(#job{mfa = {{ecron_tests, test_function, 676 | [UniqueKey1]},DueTime}, key = {DueSec, _}, 677 | schedule = {{'*', '*', '*'}, Time}, 678 | client_fun = undefined}, Job1), 679 | ?assertEqual(ok, ecron:delete(Job1#job.key)), 680 | ?assertEqual([], ecron:list()), 681 | ets:delete_all_objects(ecron_test), 682 | ets:delete_all_objects(event_test). 683 | 684 | %% Restarts the application, verify that the previously scheduled jobs are 685 | %% preserved and new ones are added into the queue from the environment variable 686 | %% Also a daily job was present both in the table and in the file, test we 687 | %% don't have a duplicated entry for it 688 | load_from_file_and_table() -> 689 | LT = ecron_time:localtime(), 690 | Schedule = calendar:datetime_to_gregorian_seconds(LT)+1, 691 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 692 | {A,B,C} = now(), 693 | UniqueKey = lists:concat([A, "-", B, "-", C]), 694 | ?assertEqual(ok, ecron:insert( 695 | DateTime, 696 | {ecron_tests, test_not_ok_function, [UniqueKey]},3,6)), 697 | Schedule1 = calendar:datetime_to_gregorian_seconds(LT)+6, 698 | {{Y,M,D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule1), 699 | {A1,B1,C1} = now(), 700 | UniqueKey1 = lists:concat([A1, "-", B1, "-", C1]), 701 | ?assertEqual(ok, ecron:insert({{'*', '*', '*'}, Time}, 702 | {ecron_tests, test_function, [UniqueKey1]})), 703 | timer:sleep(1300), 704 | ?assertEqual([{UniqueKey, Schedule, Schedule}], ets:lookup(ecron_test, UniqueKey)), 705 | ets:delete_all_objects(ecron_test), 706 | application:stop(ecron), 707 | 708 | Schedule2 = calendar:datetime_to_gregorian_seconds(LT)+5, 709 | DateTime2 = calendar:gregorian_seconds_to_datetime(Schedule2), 710 | {A2,B2,C2} = now(), 711 | UniqueKey2 = lists:concat([A2, "-", B2, "-", C2]), 712 | Schedule3 = calendar:datetime_to_gregorian_seconds(LT)+4, 713 | {{_Y,_M,_D}, Time3} = calendar:gregorian_seconds_to_datetime(Schedule3), 714 | {A3,B3,C3} = now(), 715 | UniqueKey3 = lists:concat([A3, "-", B3, "-", C3]), 716 | % ?debugFmt("DateTime=~pDateTime2=~pTime=~pTime3=~p~n",[DateTime, DateTime2, Time, Time3]), 717 | application:set_env(ecron, scheduled, 718 | [{DateTime2,{ecron_tests, test_function, [UniqueKey2]}}, 719 | {{{'*', '*', '*'}, Time3}, 720 | {ecron_tests, test_function, [UniqueKey3]}}, 721 | {{{'*', '*', '*'}, Time}, 722 | {ecron_tests, test_function, [UniqueKey1]}}]), 723 | timer:sleep(1000), 724 | application:start(ecron), 725 | {ok, _} = ecron:add_event_handler(ecron_event_handler_test, []), 726 | ok = gen_event:delete_handler(?EVENT_MANAGER, ecron_event, []), 727 | 728 | timer:sleep(300), 729 | JobKey = mnesia:dirty_first(?JOB_TABLE), 730 | ?assertMatch({Schedule3, _}, JobKey), 731 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, 732 | [UniqueKey3]}, {{Y,M,D}, Time3}}, 733 | schedule = {{'*', '*', '*'}, Time3}, 734 | client_fun = undefined}], 735 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 736 | JobKey1 = mnesia:dirty_next(?JOB_TABLE, JobKey), 737 | ?assertMatch({Schedule2, _}, JobKey1), 738 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey2]}, 739 | DateTime2}, 740 | schedule = DateTime2, client_fun = undefined}], 741 | mnesia:dirty_read(?JOB_TABLE, JobKey1)), 742 | JobKey2 = mnesia:dirty_next(?JOB_TABLE, JobKey1), 743 | ?assertMatch({Schedule1, _}, JobKey2), 744 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey1]}, 745 | {{Y,M,D}, Time}}, 746 | schedule = {{'*', '*', '*'}, Time}, 747 | client_fun = undefined}], 748 | mnesia:dirty_read(?JOB_TABLE, JobKey2)), 749 | JobKey3 = mnesia:dirty_next(?JOB_TABLE, JobKey2), 750 | ?assertMatch([#job{mfa = {{ecron_tests, test_not_ok_function, 751 | [UniqueKey]}, DateTime}, 752 | schedule = DateTime, client_fun = _}], 753 | mnesia:dirty_read(?JOB_TABLE, JobKey3)), 754 | ?assertEqual('$end_of_table', mnesia:dirty_next(?JOB_TABLE, JobKey3)), 755 | timer:sleep(6300), 756 | ?assertMatch([{UniqueKey, Schedule, _}], ets:lookup(ecron_test, UniqueKey)), 757 | ?assertEqual([{UniqueKey1, test}], ets:lookup(ecron_test, UniqueKey1)), 758 | ?assertEqual([{UniqueKey2, test}], ets:lookup(ecron_test, UniqueKey2)), 759 | ?assertEqual([{UniqueKey3, test}], ets:lookup(ecron_test, UniqueKey3)), 760 | ?assertEqual(3, length(ecron:list())), 761 | ?assertEqual(ok, ecron:delete_all()), 762 | ?assertEqual([], ecron:list()), 763 | ets:delete_all_objects(ecron_test), 764 | ets:delete_all_objects(event_test). 765 | 766 | delete_not_existent() -> 767 | ?assertEqual(ok, ecron:delete(fakekey)). 768 | 769 | 770 | refresh() -> 771 | LT = ecron_time:localtime(), 772 | Schedule = calendar:datetime_to_gregorian_seconds(LT)+3, 773 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 774 | {A,B,C} = now(), 775 | UniqueKey = lists:concat([A, "-", B, "-", C]), 776 | ?assertEqual(ok, ecron:insert( 777 | DateTime, 778 | {ecron_tests, test_not_ok_function, [UniqueKey]})), 779 | Schedule1 = calendar:datetime_to_gregorian_seconds(LT)+6, 780 | {{_Y,_M,_D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule1), 781 | {A1,B1,C1} = now(), 782 | UniqueKey1 = lists:concat([A1, "-", B1, "-", C1]), 783 | ?assertEqual(ok, ecron:insert( 784 | {{'*', '*', '*'}, Time}, 785 | {ecron_tests, test_function, [UniqueKey1]})), 786 | ?assertEqual(2, length(ecron:list())), 787 | 788 | Schedule2 = calendar:datetime_to_gregorian_seconds(LT)+5, 789 | DateTime2 = calendar:gregorian_seconds_to_datetime(Schedule2), 790 | {A2,B2,C2} = now(), 791 | UniqueKey2 = lists:concat([A2, "-", B2, "-", C2]), 792 | Schedule3 = calendar:datetime_to_gregorian_seconds(LT)+4, 793 | {{Y,M,D}, Time3} = calendar:gregorian_seconds_to_datetime(Schedule3), 794 | {A3,B3,C3} = now(), 795 | UniqueKey3 = lists:concat([A3, "-", B3, "-", C3]), 796 | application:set_env( 797 | ecron, 798 | scheduled, 799 | [{DateTime2,{ecron_tests, test_function, [UniqueKey2]}}, 800 | {{{'*', '*', '*'}, Time3},{ecron_tests, test_function, [UniqueKey3]}}, 801 | {{{'*', '*', '*'}, Time},{ecron_tests, test_function, [UniqueKey1]}}]), 802 | 803 | ?assertEqual(ok, ecron:refresh()), 804 | timer:sleep(300), 805 | ?assertEqual(3, length(ecron:list())), 806 | JobKey = mnesia:dirty_first(?JOB_TABLE), 807 | ?assertMatch({Schedule3, _}, JobKey), 808 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey3]}, 809 | {{Y,M,D}, Time3}}, 810 | schedule = {{'*', '*', '*'}, Time3}, 811 | client_fun = undefined}], 812 | mnesia:dirty_read(?JOB_TABLE, JobKey)), 813 | JobKey1 = mnesia:dirty_next(?JOB_TABLE, JobKey), 814 | ?assertMatch({Schedule2, _}, JobKey1), 815 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey2]}, 816 | DateTime2}, 817 | schedule = DateTime2, client_fun = undefined}], 818 | mnesia:dirty_read(?JOB_TABLE, JobKey1)), 819 | JobKey2 = mnesia:dirty_next(?JOB_TABLE, JobKey1), 820 | ?assertMatch({Schedule1, _}, JobKey2), 821 | ?assertMatch([#job{mfa = {{ecron_tests, test_function, [UniqueKey1]}, 822 | {{Y,M,D}, Time}}, 823 | schedule = {{'*', '*', '*'}, Time}, 824 | client_fun = undefined}], 825 | mnesia:dirty_read(?JOB_TABLE, JobKey2)), 826 | JobKey3 = mnesia:dirty_next(?JOB_TABLE, JobKey2), 827 | ?assertEqual('$end_of_table', JobKey3), 828 | ?assertEqual(ok, ecron:delete_all()), 829 | ets:delete_all_objects(ecron_test), 830 | ets:delete_all_objects(event_test). 831 | 832 | execute_all() -> 833 | LT = ecron_time:localtime(), 834 | Schedule = calendar:datetime_to_gregorian_seconds(LT)+30, 835 | DateTime = calendar:gregorian_seconds_to_datetime(Schedule), 836 | {A,B,C} = now(), 837 | UniqueKey = lists:concat([A, "-", B, "-", C]), 838 | ?assertEqual(ok, ecron:insert( 839 | DateTime, 840 | {ecron_tests, test_not_ok_function, [UniqueKey]})), 841 | 842 | Schedule1 = calendar:datetime_to_gregorian_seconds(LT)+60, 843 | {{_Y,_M,_D}, Time} = calendar:gregorian_seconds_to_datetime(Schedule1), 844 | {A1,B1,C1} = now(), 845 | UniqueKey1 = lists:concat([A1, "-", B1, "-", C1]), 846 | ?assertEqual(ok, ecron:insert( 847 | {{'*', '*', '*'}, Time}, 848 | {ecron_tests, ok_mfa, [UniqueKey1]})), 849 | 850 | Schedule2 = calendar:datetime_to_gregorian_seconds(LT)+20, 851 | DateTime2 = calendar:gregorian_seconds_to_datetime(Schedule2), 852 | {A2,B2,C2} = now(), 853 | UniqueKey2 = lists:concat([A2, "-", B2, "-", C2]), 854 | ?assertEqual(ok, ecron:insert( 855 | DateTime2,{ecron_tests, ok_mfa1, [UniqueKey2]})), 856 | 857 | Schedule3 = calendar:datetime_to_gregorian_seconds(LT)+50, 858 | DateTime3 = calendar:gregorian_seconds_to_datetime(Schedule3), 859 | {A3,B3,C3} = now(), 860 | UniqueKey3 = lists:concat([A3, "-", B3, "-", C3]), 861 | ?assertEqual(ok, ecron:insert( 862 | DateTime3,{ecron_tests, retry_mfa, [UniqueKey3]}, 4, 1)), 863 | 864 | ?assertEqual(4, length(ecron:list())), 865 | ?assertEqual(ok, ecron:execute_all()), 866 | T = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 867 | timer:sleep(300), 868 | ?assertEqual([{UniqueKey, T, T}], ets:lookup(ecron_test, UniqueKey)), 869 | ?assertEqual([{UniqueKey1, T}], ets:lookup(ecron_test, UniqueKey1)), 870 | ?assertEqual([{UniqueKey2, T}], ets:lookup(ecron_test, UniqueKey2)), 871 | ?assertEqual([{UniqueKey3, T}], ets:lookup(ecron_test, UniqueKey3)), 872 | 873 | ?assertEqual([], ecron:list()), 874 | ets:delete_all_objects(ecron_test), 875 | ets:delete_all_objects(event_test). 876 | 877 | event_handler_api() -> 878 | EventHandlers = ecron:list_event_handlers(), 879 | ?assertMatch([{_, ecron_event_handler_test}], EventHandlers), 880 | ?assertEqual([ecron_event_handler_test], gen_event:which_handlers(?EVENT_MANAGER)), 881 | [{Pid, ecron_event_handler_test}] = EventHandlers, 882 | ?assertEqual(ok, ecron:delete_event_handler(Pid)), 883 | timer:sleep(100), 884 | ?assertEqual([], ecron:list_event_handlers()), 885 | ?assertEqual([], gen_event:which_handlers(?EVENT_MANAGER)), 886 | ?assertMatch({ok, _}, ecron:add_event_handler(ecron_event_handler_test, [])), 887 | ?assertMatch([{_, ecron_event_handler_test}], ecron:list_event_handlers()), 888 | ?assertEqual([ecron_event_handler_test], gen_event:which_handlers(?EVENT_MANAGER)). 889 | 890 | test_function(Key) -> 891 | F = fun() -> 892 | ets:insert(ecron_test, {Key, test}), 893 | ok 894 | end, 895 | {apply, F}. 896 | 897 | test_function1(Key) -> 898 | F = fun() -> 899 | ets:insert(ecron_test, {Key, test}), 900 | {ok, Key} 901 | end, 902 | {apply, F}. 903 | 904 | 905 | test_not_ok_function(Key) -> 906 | Time = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 907 | F = fun() -> 908 | ExecTime = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 909 | ets:insert(ecron_test, {Key, Time, ExecTime}), 910 | {error, retry} 911 | end, 912 | {apply, F}. 913 | 914 | wrong_fun(Key) -> 915 | Time = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 916 | F = fun() -> 917 | ets:insert(ecron_test, {Key, Time}), 918 | wrong 919 | end, 920 | {apply, F}. 921 | 922 | ok_mfa(Key) -> 923 | Time = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 924 | ets:insert(ecron_test, {Key, Time}), 925 | ok. 926 | 927 | ok_mfa1(Key) -> 928 | Time = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 929 | ets:insert(ecron_test, {Key, Time}), 930 | {ok, Key}. 931 | 932 | retry_mfa(Key) -> 933 | Time = calendar:datetime_to_gregorian_seconds(ecron_time:localtime()), 934 | ets:insert(ecron_test, {Key, Time}), 935 | {error, retry}. 936 | 937 | 938 | wrong_mfa(Key) -> 939 | ets:insert(ecron_test, {Key, test}), 940 | wrong. 941 | 942 | 943 | add_month({Y, M, D}) -> 944 | case M of 945 | 12 -> {Y+1, 1, D}; 946 | M -> {Y, M+1, D} 947 | end. 948 | --------------------------------------------------------------------------------