├── .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 |
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 |
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 |
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 |
208 |
209 |
210 |
211 | add_event_handler/2 | Adds a new event handler. |
delete/1 | Deletes a cron job from the list. |
delete_all/0 | Delete all the scheduled jobs. |
delete_event_handler/1 | Deletes an event handler. |
execute_all/0 | Executes all cron jobs in the queue, irrespective of the time they are
212 | scheduled to run. |
insert/2 | Schedules the MFA at the given Date and Time. |
insert/4 | Schedules the MFA at the given Date and Time and retry if it fails. |
install/0 | Create mnesia tables on those nodes where disc_copies resides according
213 | to the schema. |
install/1 | Create mnesia tables on Nodes. |
list/0 | Returns a list of job records defined in ecron.hrl. |
list_event_handlers/0 | Returns 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/0 | Prints a pretty list of records sorted by Job ID. |
refresh/0 | Deletes all jobs and recreates the table from the environment variables. |
216 |
217 |
218 |
219 |
220 |
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 | DateTime = {Date, Time}
Date = {Year, Month, Day} | '*'
Time = {Hours, Minutes, Seconds}
Year = integer() | '*'
Month = integer() | '*'
Day = integer() | '*' | last
Hours = integer()
Minutes = integer()
Seconds = integer()
MFA = {Module, Function, Args}
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 | DateTime = {Date, Time}
Date = {Year, Month, Day} | '*'
Time = {Hours, Minutes, Seconds}
Year = integer() | '*'
Month = integer() | '*'
Day = integer() | '*' | last
Hours = integer()
Minutes = integer()
Seconds = integer()
Retry = integer() | infinity
RetrySeconds = integer()
MFA = {Module, Function, Args}
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 |
19 |
20 |
21 |
22 | start/2 | Start the ecron application. |
stop/1 | Stop the ecron application. |
23 |
24 |
25 |
26 |
27 |
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 |
21 |
22 |
23 |
24 | code_change/3 | Convert process state when code is changed. |
handle_call/2 | Whenever 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/2 | This 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/2 | Whenever an event handler is deleted from an event manager,
29 | this function is called. |
30 |
31 |
32 |
33 |
34 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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 |
20 |
21 |
22 |
23 | init/1 | Whenever 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/0 | Returns a list of all event handlers installed by the
26 | start_handler/2 API. |
start_handler/2 | Starts a child that will add a new event handler. |
start_link/0 | Starts the supervisor. |
stop_handler/1 | Stop the child with the given Pid. |
27 |
28 |
29 |
30 |
31 |
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 |
19 |
20 |
21 |
22 | init/1 | Whenever 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/0 | Starts the supervisor. |
25 |
26 |
27 |
28 |
29 |
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 |
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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
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 |
--------------------------------------------------------------------------------