├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── THANKS ├── rebar ├── rebar.config ├── src ├── leader_cron.app.src ├── leader_cron.erl ├── leader_cron_app.erl ├── leader_cron_sup.erl └── leader_cron_task.erl └── test └── leader_cron_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | erl_crash.dump 2 | log 3 | logs 4 | deps 5 | ebin 6 | .eunit 7 | .leader_cron_plt 8 | doc 9 | *.beam 10 | xref_warnings 11 | dialyzer_warnings 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=./rebar 2 | PLT=.leader_cron_plt 3 | DEPS=$(wildcard deps/*/ebin) 4 | 5 | .PHONY: doc 6 | 7 | all: get-deps 8 | $(REBAR) compile 9 | 10 | clean: clean-doc 11 | $(REBAR) clean 12 | rm -rf log logs 13 | 14 | get-deps: 15 | $(REBAR) get-deps 16 | 17 | check: all eunit ct 18 | 19 | eunit: all 20 | @echo These tests take a few minutes... 21 | $(REBAR) eunit skip_deps=true 22 | 23 | ct: all 24 | $(REBAR) ct skip_deps=true 25 | 26 | xref: all 27 | $(REBAR) xref 28 | 29 | dialyzer: all 30 | dialyzer -q --plt $(PLT) -Wno_undefined_callbacks ebin 31 | 32 | 33 | build_plt: all 34 | dialyzer --build_plt --output_plt $(PLT) \ 35 | --apps erts kernel stdlib $(DEPS) 36 | 37 | doc: 38 | $(REBAR) doc 39 | 40 | clean-doc: 41 | rm -rf doc 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leader_cron 2 | 3 | leader_cron provides a distributed task scheduler for executing task 4 | periodically in an Erlang cluster 5 | 6 | Participating members of the cluster elect a leader node. The leader node 7 | schedules and executes given tasks as per their schedules. Should the leader 8 | node become unavailable a new leader is elected who resumes task execution 9 | responsibilities. 10 | 11 | Tasks are defined by specifying a function in a particular module with given 12 | arguments to run according to a schedule. The schedule types are: 13 | 14 | * sleeper - sleep a specified number of milliseconds between task executions 15 | * one shot - execute task once at a given date and time or after a number of 16 | milliseconds 17 | * cron - define a schedule very similar to Unix cron 18 | 19 | ## Usage 20 | 21 | Startup leader_cron on each participating node (do this on all nodes): 22 | 23 | ```erlang 24 | leader_cron:start_link(['node1@127.0.0.1', 'node2@127.0.0.1']). 25 | ``` 26 | 27 | Schedule tasks from any node. Here a cron style schedule is defined. 28 | 29 | ```erlang 30 | leader_cron:schedule_task({cron, {[5], all, all, all, all}}, 31 | {io, format, [user, "It is 5 past the hour", []]}). 32 | ``` 33 | 34 | That's it. In this example the task prints, "It is 5 past the hour" on the 35 | leader node at 5 minutes past every hour. 36 | 37 | You can also schedule anonymous functions: 38 | ```erlang 39 | F = fun(Device, Format, Args) -> io:format(Device, Format, Args) end, 40 | leader_cron:schedule_task({cron, {[5], all, all, all, all}}, 41 | {F, [user, "It is 5 past the hour", []]}). 42 | ``` 43 | 44 | See the `leader_cron_task` module for full scheduling details (or `make doc`). 45 | 46 | ## Building 47 | 48 | Run `make` or include as a [rebar](https://github.com/basho/rebar) dependency 49 | in your project. 50 | 51 | ## Testing 52 | 53 | To run all tests run `make check`. This runs both the eunit tests as well as the 54 | common tests. These can also be run individually via `make eunit` and `make ct`. 55 | 56 | ## Static Analysis 57 | 58 | 59 | ### Dialyzer 60 | 61 | To perform the [Dialyzer](http://www.erlang.org/doc/man/dialyzer.html) 62 | static analysis of the code run `make dialyzer`. Run `make build_plt` 63 | once before performing the static analysis to build the plt file required 64 | by dialyzer. 65 | 66 | ### Xref 67 | 68 | To run the [Xref](http://www.erlang.org/doc/apps/tools/xref_chapter.html) 69 | cross reference tool run `make xref`. 70 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | The following people have contributed to leader_cron: 2 | 3 | Jeremy Raymond 4 | Taybin Rutkin 5 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeraymond/leader_cron/2a367426d2d1ea4a3ba914a9832f8ef71e2554e1/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, fail_on_warning]}. 2 | {deps, [{gen_leader, ".*", 3 | {git, "git://github.com/jeraymond/gen_leader_revival.git", 4 | {branch, "master"}}} 5 | ]}. 6 | {eunit_opts, [{report, {eunit_surefire, [{dir, "."}]}}]}. 7 | {xref_checks, [undefined_function_calls]}. 8 | {cover_enabled, true}. 9 | {cover_print_enabled, true}. 10 | -------------------------------------------------------------------------------- /src/leader_cron.app.src: -------------------------------------------------------------------------------- 1 | {application, leader_cron, 2 | [ 3 | {description, ""}, 4 | {vsn, "0.2.1"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {mod, { leader_cron_app, []}}, 11 | {env, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/leader_cron.erl: -------------------------------------------------------------------------------- 1 | %%============================================================================== 2 | %% Copyright 2012 Jeremy Raymond 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%============================================================================== 16 | 17 | %%%------------------------------------------------------------------- 18 | %%% @author Jeremy Raymond 19 | %%% @copyright (C) 2012, Jeremy Raymond 20 | %%% @doc 21 | %%% The leader_cron module provides a distrubuted task scheduler for 22 | %%% executing tasks periodically. The connected nodes elect a leader 23 | %%% to manage task scheduling and execution. Should the current leader 24 | %%% become unavailable a new leader node is elected who resumes task 25 | %%% execution responsibilities. 26 | %%% 27 | %%% There are several different ways to specify the schedule for a task. 28 | %%% See {@link leader_cron_task} for details. 29 | %%% 30 | %%% Each node that is part of the scheduling cluster must be working 31 | %%% with the same list of nodes as given to {@link start_link/1}. If 32 | %%% the node list needs to change leader_cron must be 33 | %%% stopped on all nodes. Once stopped everywhere restart 34 | %%% leader_cron with the new node list. Rolling updates 35 | %%% currently are not supported. 36 | %%% 37 | %%% @see leader_cron_task 38 | %%% 39 | %%% @end 40 | %%% Created : 31 Jan 2012 by Jeremy Raymond 41 | %%%------------------------------------------------------------------- 42 | -module(leader_cron). 43 | 44 | -behaviour(gen_leader). 45 | 46 | %% API 47 | -export([start_link/1, 48 | start_link/2, 49 | status/0, 50 | schedule_task/2, 51 | schedule_task/3, 52 | cancel_task/1, 53 | task_status/1, 54 | task_list/0, 55 | remove_done_tasks/0 56 | ]). 57 | 58 | %% gen_leader callbacks 59 | -export([init/1, 60 | handle_cast/3, 61 | handle_call/4, 62 | handle_info/2, 63 | handle_leader_call/4, 64 | handle_leader_cast/3, 65 | handle_DOWN/3, 66 | elected/3, 67 | surrendered/3, 68 | from_leader/3, 69 | code_change/4, 70 | terminate/2]). 71 | 72 | -define(SERVER, ?MODULE). 73 | 74 | -type name() :: atom() | binary(). 75 | %% Name for a named task. 76 | 77 | -type ident() :: name() | pid(). 78 | %% The task pid() or name. 79 | 80 | -type task() :: {ident(), 81 | leader_cron_task:schedule(), 82 | leader_cron_task:execargs()}. 83 | %% Task definition. 84 | 85 | %% The gen_leader options 86 | -type bcast_type() :: 'all' | 'sender'. 87 | 88 | -type option() :: {'workers', Workers::[node()]} 89 | | {'vardir', Dir::string()} 90 | | {'bcast_type', Type::bcast_type()} 91 | | {'heartbeat', Seconds::integer()}. 92 | 93 | -type options() :: [option()]. 94 | 95 | 96 | -record(state, {tasks = [], is_leader = false}). 97 | 98 | %%%=================================================================== 99 | %%% API 100 | %%%=================================================================== 101 | 102 | %%-------------------------------------------------------------------- 103 | %% @doc 104 | %% Creates a linked process to manage scheduled tasks in coordination 105 | %% with the given nodes. The current node must be part of the node 106 | %% list. Each leader_cron node must be working with the same list of 107 | %% nodes to coordinate correctly. 108 | %% 109 | %% @end 110 | %%-------------------------------------------------------------------- 111 | 112 | -spec start_link(Nodes) -> {ok, pid()} | {error, Reason} when 113 | Nodes :: [node()], 114 | Reason :: term(). 115 | 116 | start_link(Nodes) -> 117 | Opts = [], 118 | start_link(Nodes, Opts). 119 | 120 | %%-------------------------------------------------------------------- 121 | %% @doc 122 | %% Creates a linked process to manage scheduled tasks in coordination 123 | %% with the given nodes. The current node must be part of the node 124 | %% list. Each leader_cron node must be working with the same list of 125 | %% nodes to coordinate correctly. 126 | %% The Opts argument allows to configure gen_leader options. 127 | %% 128 | %% @end 129 | %%-------------------------------------------------------------------- 130 | -spec start_link(Nodes, Opts) -> {ok, pid()} | {error, Reason} when 131 | Nodes :: [node()], 132 | Opts :: [options()], 133 | Reason :: term(). 134 | 135 | start_link(Nodes, Opts) -> 136 | gen_leader:start_link(?SERVER, Nodes, Opts, ?MODULE, [], []). 137 | 138 | %%-------------------------------------------------------------------- 139 | %% @doc 140 | %% Gets the status of this scheduler. 141 | %% 142 | %% @end 143 | %%-------------------------------------------------------------------- 144 | 145 | -spec status() -> Status when 146 | Status :: {[term()]}. 147 | 148 | status() -> 149 | gen_leader:call(?SERVER, status). 150 | 151 | %%-------------------------------------------------------------------- 152 | %% @doc 153 | %% Schedules a task. See {@link leader_cron_task} for scheduling 154 | %% details. 155 | %% 156 | %% @end 157 | %%-------------------------------------------------------------------- 158 | 159 | -spec schedule_task(Schedule, Exec) -> {ok, pid()} | {error, term()} when 160 | Schedule :: leader_cron_task:schedule(), 161 | Exec :: leader_cron_task:execargs(). 162 | 163 | schedule_task(Schedule, Exec) -> 164 | gen_leader:leader_call(?SERVER, {schedule, {undefined, Schedule, Exec}}). 165 | 166 | %%-------------------------------------------------------------------- 167 | %% @doc 168 | %% Schedules a named task. There cannot be more than one task with 169 | %% a given name at any one time. See {@link leader_cron_task} for 170 | %% scheduling details. 171 | %% 172 | %% The name 'undefined' is reserved for all unnamed tasks and cannot 173 | %% be used. 174 | %% 175 | %% @end 176 | %%-------------------------------------------------------------------- 177 | 178 | -spec schedule_task(ident(), Schedule, Exec) -> 179 | {ok, pid()} | {error, term()} when 180 | Schedule :: leader_cron_task:schedule(), 181 | Exec :: leader_cron_task:execargs(). 182 | 183 | schedule_task(Name, Schedule, Exec) when 184 | is_binary(Name); is_atom(Name), Name /= undefined -> 185 | gen_leader:leader_call(?SERVER, {schedule, {Name, Schedule, Exec}}). 186 | 187 | %%-------------------------------------------------------------------- 188 | %% @doc 189 | %% Cancels a task. 190 | %% 191 | %% @end 192 | %%-------------------------------------------------------------------- 193 | 194 | -spec cancel_task(ident()) -> ok | {error, Reason} when 195 | Reason :: term(). 196 | 197 | cancel_task(Ident) -> 198 | gen_leader:leader_call(?SERVER, {cancel, Ident}). 199 | 200 | %%-------------------------------------------------------------------- 201 | %% @doc 202 | %% Gets the status of a task. 203 | %% 204 | %% @end 205 | %%-------------------------------------------------------------------- 206 | 207 | -spec task_status(ident()) -> {Status, ScheduleTime, TaskPid} when 208 | Status :: leader_cron_task:status(), 209 | ScheduleTime :: leader_cron_task:datetime(), 210 | TaskPid :: pid(). 211 | 212 | task_status(Ident) -> 213 | gen_leader:leader_call(?SERVER, {task_status, Ident}). 214 | 215 | %%-------------------------------------------------------------------- 216 | %% @doc 217 | %% Gets the list of tasks. 218 | %% 219 | %% @end 220 | %%-------------------------------------------------------------------- 221 | 222 | -spec task_list() -> [task()]. 223 | 224 | task_list() -> 225 | gen_leader:leader_call(?SERVER, task_list). 226 | 227 | %%-------------------------------------------------------------------- 228 | %% @doc 229 | %% Remove tasks with a status of done. 230 | %% 231 | %% @end 232 | %%-------------------------------------------------------------------- 233 | 234 | -spec remove_done_tasks() -> ok. 235 | 236 | remove_done_tasks() -> 237 | gen_leader:leader_call(?SERVER, remove_done_tasks). 238 | 239 | %%%=================================================================== 240 | %%% gen_leader callbacks 241 | %%%=================================================================== 242 | 243 | %% @private 244 | init([]) -> 245 | {ok, #state{}}. 246 | 247 | %% @private 248 | elected(State, _Election, undefined) -> 249 | Sync = State#state.tasks, 250 | State1 = case State#state.is_leader of 251 | false -> 252 | start_tasks(State); 253 | true -> 254 | State 255 | end, 256 | State2 = State1#state{is_leader = true}, 257 | {ok, Sync, State2}; 258 | elected(State, _Election, _Node) -> 259 | Sync = State#state.tasks, 260 | State1 = case State#state.is_leader of 261 | false -> 262 | start_tasks(State); 263 | true -> 264 | State 265 | end, 266 | State2 = State1#state{is_leader = true}, 267 | {reply, Sync, State2}. 268 | 269 | %% @private 270 | surrendered(State, Sync, _Election) -> 271 | State1 = stop_tasks(State), 272 | State2 = save_tasks(State1, Sync), 273 | State3 = State2#state{is_leader = false}, 274 | {ok, State3}. 275 | 276 | %% @private 277 | handle_leader_call({cancel, Name}, From, State, Election) when 278 | is_binary(Name); is_atom(Name) -> 279 | case pid_for_name(Name, State#state.tasks) of 280 | {error, Reason} -> 281 | {reply, {error, Reason}, State}; 282 | Pid -> 283 | handle_leader_call({cancel, Pid}, From, State, Election) 284 | end; 285 | handle_leader_call({cancel, Pid}, _From, State, Election) -> 286 | Tasks = State#state.tasks, 287 | {Reply, State1} = case lists:keyfind(Pid, 2, Tasks) of 288 | false -> 289 | {{error, no_such_pid}, State}; 290 | {_, Pid, _, _} -> 291 | ok = leader_cron_task:stop(Pid), 292 | Tasks1 = lists:keydelete(Pid, 2, Tasks), 293 | send_tasks(Tasks1, Election), 294 | {ok, State#state{tasks = Tasks1}} 295 | end, 296 | {reply, Reply, State1}; 297 | handle_leader_call({schedule, {Name, Schedule, Exec}}, _From, State, Election) -> 298 | case not (Name == undefined) 299 | andalso lists:keymember(Name, 1, State#state.tasks) of 300 | true -> 301 | {reply, {error, already_exists}, State}; 302 | false -> 303 | case leader_cron_task:start_link(Schedule, Exec) of 304 | {ok, Pid} -> 305 | Task = {Name, Pid, Schedule, Exec}, 306 | TaskList = [Task|State#state.tasks], 307 | State1 = State#state{tasks = TaskList}, 308 | ok = send_tasks(TaskList, Election), 309 | {reply, {ok, Pid}, State1}; 310 | {error, Reason} -> 311 | {reply, {error, Reason}, State} 312 | end 313 | end; 314 | handle_leader_call({task_status, Name}, From, State, Election) when 315 | is_binary(Name); is_atom(Name) -> 316 | case pid_for_name(Name, State#state.tasks) of 317 | {error, Reason} -> 318 | {reply, {error, Reason}, State}; 319 | Pid -> 320 | handle_leader_call({task_status, Pid}, From, State, Election) 321 | end; 322 | handle_leader_call({task_status, Pid}, _From, State, _Election) -> 323 | Status = leader_cron_task:status(Pid), 324 | {reply, Status, State}; 325 | handle_leader_call(task_list, _From, State, _Election) -> 326 | Tasks = State#state.tasks, 327 | {reply, Tasks, State}; 328 | handle_leader_call(remove_done_tasks, _From, State, Election) -> 329 | Tasks = State#state.tasks, 330 | Tasks1 = lists:foldl(fun remove_task_if_done/2, [], Tasks), 331 | State1 = State#state{tasks = Tasks1}, 332 | ok = send_tasks(Tasks1, Election), 333 | {reply, ok, State1}. 334 | 335 | %% @private 336 | handle_leader_cast(_Request, State, _Election) -> 337 | {noreply, State}. 338 | 339 | %% @private 340 | from_leader({tasks, Tasks}, State, _Election) -> 341 | State1 = save_tasks(State, Tasks), 342 | {ok, State1}. 343 | 344 | %% @private 345 | handle_DOWN(_Node, State, _Election) -> 346 | {ok, State}. 347 | 348 | %% @private 349 | handle_call(status, _From, State, Election) -> 350 | Reply = [{leader, gen_leader:leader_node(Election)}, 351 | {alive, gen_leader:alive(Election)}, 352 | {down, gen_leader:down(Election)}, 353 | {candidates, gen_leader:candidates(Election)}, 354 | {workers, gen_leader:workers(Election)}, 355 | {me, node()} 356 | ], 357 | {reply, Reply, State}; 358 | handle_call(_Request, _From, State, _Election) -> 359 | Reply = ok, 360 | {reply, Reply, State}. 361 | 362 | %% @private 363 | handle_cast(_Msg, State, _Election) -> 364 | {noreply, State}. 365 | 366 | %% @private 367 | handle_info(_Info, State) -> 368 | {noreply, State}. 369 | 370 | %% @private 371 | terminate(_Reason, _State) -> 372 | ok. 373 | 374 | %% @private 375 | code_change(_OldVsn, State, _Election, _Extra) -> 376 | {ok, State}. 377 | 378 | %%%=================================================================== 379 | %%% Internal functions 380 | %%%=================================================================== 381 | 382 | save_tasks(State, Tasks) -> 383 | State#state{tasks = Tasks}. 384 | 385 | -spec send_tasks(Tasks, Election) -> ok when 386 | Tasks :: [task()], 387 | Election :: term(). 388 | 389 | send_tasks(Tasks, Election) -> 390 | case gen_leader:alive(Election) -- [node()] of 391 | [] -> 392 | ok; 393 | Alive -> 394 | Election = gen_leader:broadcast({from_leader, {tasks, Tasks}}, 395 | Alive, 396 | Election), 397 | ok 398 | end. 399 | 400 | -spec stop_tasks(State :: #state{}) -> #state{}. 401 | 402 | stop_tasks(State) -> 403 | Tasks = State#state.tasks, 404 | Tasks1 = lists:foldl(fun({Name, Pid, Schedule, Exec}, Acc) -> 405 | ok = leader_cron_task:stop(Pid), 406 | [{Name, undefined, Schedule, Exec}|Acc] 407 | end, [], Tasks), 408 | State#state{tasks = Tasks1}. 409 | 410 | -spec start_tasks(#state{}) -> #state{}. 411 | 412 | start_tasks(State) -> 413 | TaskList = State#state.tasks, 414 | TaskList1 = lists:foldl( 415 | fun(Task, Acc) -> 416 | {Name, _, Schedule, Exec} = Task, 417 | case leader_cron_task:start_link(Schedule, Exec) of 418 | {ok, Pid} -> 419 | [{Name, Pid, Schedule, Exec}|Acc]; 420 | {error, Reason} -> 421 | Format = "Could not start task ~p ~p, name: ~p", 422 | Message = io_lib:format(Format, 423 | [Exec, Reason, Name]), 424 | error_logger:error_report(Message), 425 | [{Name, undefined, Schedule, Exec}|Acc] 426 | end 427 | end, [], TaskList), 428 | State#state{tasks = TaskList1}. 429 | 430 | remove_task_if_done(Task, Acc) -> 431 | {_, Pid, _, _} = Task, 432 | case leader_cron_task:status(Pid) of 433 | {done, _, _} -> 434 | ok = leader_cron_task:stop(Pid), 435 | Acc; 436 | _ -> 437 | [Task|Acc] 438 | end. 439 | 440 | pid_for_name(Name, Tasks) -> 441 | case lists:keyfind(Name, 1, Tasks) of 442 | false -> 443 | {error, no_such_name}; 444 | {_, Pid, _, _} -> 445 | Pid 446 | end. 447 | 448 | %%%=================================================================== 449 | %%% Unit Tests 450 | %%%=================================================================== 451 | 452 | -ifdef(TEST). 453 | 454 | -compile(export_all). 455 | 456 | -include_lib("eunit/include/eunit.hrl"). 457 | 458 | simple_task() -> 459 | receive 460 | go -> 461 | ok 462 | after 463 | 1000 -> 464 | ok 465 | end. 466 | 467 | dying_task(TimeToLiveMillis) -> 468 | timer:sleep(TimeToLiveMillis), 469 | throw(time_to_go). 470 | 471 | all_test_() -> 472 | {foreach, 473 | fun() -> 474 | leader_cron:start_link([node()]), 475 | Tasks = leader_cron:task_list(), 476 | lists:foreach(fun({_, Pid, _, _}) -> 477 | ok = leader_cron:cancel_task(Pid) 478 | end, Tasks) 479 | end, 480 | [ 481 | fun test_single_node_task/0, 482 | fun test_dying_task/0, 483 | fun test_done_task_removal/0, 484 | fun test_single_named_task_with_atom_name/0, 485 | fun test_single_named_task_with_atom_name_undefined/0, 486 | fun test_single_named_task_with_binary_name/0 487 | ]}. 488 | 489 | test_single_node_task() -> 490 | Schedule = {sleeper, 100}, 491 | Exec = {leader_cron, simple_task, []}, 492 | {ok, SchedulerPid} = leader_cron:schedule_task(Schedule, Exec), 493 | {running, _, TaskPid} = leader_cron:task_status(SchedulerPid), 494 | TaskPid ! go, 495 | ?assertMatch({waiting, _, TaskPid}, leader_cron:task_status(SchedulerPid)), 496 | ?assertEqual([{undefined, SchedulerPid, Schedule, Exec}], 497 | leader_cron:task_list()), 498 | ?assertEqual(true, is_process_alive(TaskPid)), 499 | ?assertEqual(ok, leader_cron:cancel_task(SchedulerPid)), 500 | ?assertEqual([], leader_cron:task_list()), 501 | ?assertEqual(false, is_process_alive(TaskPid)). 502 | 503 | test_dying_task() -> 504 | Schedule = {sleeper, 100000}, 505 | {ok, SchedulerPid} = leader_cron:schedule_task( 506 | Schedule, {leader_cron, dying_task, [100]}), 507 | ?assertMatch({running, _, _TPid}, leader_cron:task_status(SchedulerPid)), 508 | timer:sleep(200), 509 | ?assertMatch({waiting, _, _TPid}, leader_cron:task_status(SchedulerPid)). 510 | 511 | test_done_task_removal() -> 512 | Schedule = {oneshot, 1}, 513 | Exec = {timer, sleep, [1]}, 514 | {ok, Pid} = leader_cron:schedule_task(Schedule, Exec), 515 | timer:sleep(5), 516 | ?assertMatch([_], leader_cron:task_list()), 517 | ?assertMatch({done, _, _}, leader_cron:task_status(Pid)), 518 | ?assertEqual(ok, leader_cron:remove_done_tasks()), 519 | ?assertEqual([], leader_cron:task_list()). 520 | 521 | test_single_named_task_with_atom_name() -> 522 | test_single_named_task(test_task). 523 | 524 | test_single_named_task_with_binary_name() -> 525 | test_single_named_task(<<"test task">>). 526 | 527 | test_single_named_task_with_atom_name_undefined() -> 528 | ?assertError(function_clause, leader_cron:schedule_task(undefined, ok, ok)). 529 | 530 | test_single_named_task(Name) -> 531 | Schedule = {sleeper, 100}, 532 | Exec = {leader_cron, simple_task, []}, 533 | {ok, SchedulerPid} = leader_cron:schedule_task(Name, Schedule, Exec), 534 | {error, already_exists} = leader_cron:schedule_task(Name, Schedule, Exec), 535 | {running, _, TaskPid} = leader_cron:task_status(Name), 536 | TaskPid ! go, 537 | ?assertMatch({waiting, _, TaskPid}, leader_cron:task_status(Name)), 538 | ?assertEqual([{Name, SchedulerPid, Schedule, Exec}], 539 | leader_cron:task_list()), 540 | ?assertEqual(true, is_process_alive(TaskPid)), 541 | ?assertEqual(ok, leader_cron:cancel_task(Name)), 542 | ?assertEqual([], leader_cron:task_list()), 543 | ?assertEqual(false, is_process_alive(TaskPid)). 544 | 545 | -endif. 546 | -------------------------------------------------------------------------------- /src/leader_cron_app.erl: -------------------------------------------------------------------------------- 1 | %%============================================================================== 2 | %% Copyright 2012 Jeremy Raymond 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%============================================================================== 16 | 17 | %%%------------------------------------------------------------------- 18 | %%% @author Jeremy Raymond 19 | %%% @copyright (C) 2012, Jeremy Raymond 20 | %%% @doc 21 | %%% Starts the leader_cron application using the currently connected 22 | %%% nodes as the node list (see {@link leader_cron}). In general it 23 | %%% is probably more useful to add {@link leader_cron} or 24 | %%% {@link leader_cron_sup} to your own supervision tree where you 25 | %%% can more reasonably control the node list. 26 | %%% 27 | %%% @see leader_cron 28 | %%% @see leader_cron_sup 29 | %%% 30 | %%% @end 31 | %%% Created : 31 Jan 2012 by Jeremy Raymond 32 | %%%------------------------------------------------------------------- 33 | -module(leader_cron_app). 34 | 35 | -behaviour(application). 36 | 37 | %% Application callbacks 38 | -export([start/2, stop/1]). 39 | 40 | %%%=================================================================== 41 | %%% Application callbacks 42 | %%%=================================================================== 43 | 44 | %%-------------------------------------------------------------------- 45 | %% @private 46 | %% @doc 47 | %% 48 | %% @end 49 | %%-------------------------------------------------------------------- 50 | 51 | start(_StartType, _StartArgs) -> 52 | case leader_cron_sup:start_link([node()|nodes()]) of 53 | {ok, Pid} -> 54 | {ok, Pid}; 55 | Error -> 56 | Error 57 | end. 58 | 59 | %%-------------------------------------------------------------------- 60 | %% @private 61 | %% @doc 62 | %% 63 | %% @end 64 | %%-------------------------------------------------------------------- 65 | 66 | stop(_State) -> 67 | ok. 68 | 69 | %%%=================================================================== 70 | %%% Internal functions 71 | %%%=================================================================== 72 | 73 | -------------------------------------------------------------------------------- /src/leader_cron_sup.erl: -------------------------------------------------------------------------------- 1 | %%============================================================================== 2 | %% Copyright 2012 Jeremy Raymond 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%============================================================================== 16 | 17 | %%%------------------------------------------------------------------- 18 | %%% @author Jeremy Raymond 19 | %%% @copyright (C) 2012, Jeremy Raymond 20 | %%% @doc 21 | %%% The {@link leader_cron} supervisor. 22 | %%% @see leader_cron 23 | %%% 24 | %%% @end 25 | %%% Created : 31 Jan 2012 by Jeremy Raymond 26 | %%%------------------------------------------------------------------- 27 | -module(leader_cron_sup). 28 | 29 | -behaviour(supervisor). 30 | 31 | %% API 32 | -export([start_link/1, 33 | start_link/2]). 34 | 35 | %% Supervisor callbacks 36 | -export([init/1]). 37 | 38 | -define(SERVER, ?MODULE). 39 | 40 | %%%=================================================================== 41 | %%% API functions 42 | %%%=================================================================== 43 | 44 | %%-------------------------------------------------------------------- 45 | %% @doc 46 | %% Starts the leader_cron supervisor with the given node list. See 47 | %% {@link leader_cron:start_link/1}. 48 | %% 49 | %% @end 50 | %%-------------------------------------------------------------------- 51 | 52 | -spec start_link([node()]) -> ignore | {error, term()} | {ok, pid()}. 53 | 54 | start_link(Nodes) -> 55 | supervisor:start_link({local, ?SERVER}, ?MODULE, [Nodes]). 56 | 57 | %%-------------------------------------------------------------------- 58 | %% @doc 59 | %% Starts the leader_cron supervisor with the given node list and 60 | %% gen_leader options. See {@link leader_cron:start_link/2}. 61 | %% 62 | %% @end 63 | %%-------------------------------------------------------------------- 64 | -spec start_link([node()], [term()]) -> ignore | {error, term()} | {ok, pid()}. 65 | 66 | start_link(Nodes, Opts) -> 67 | supervisor:start_link({local, ?SERVER}, ?MODULE, [Nodes, Opts]). 68 | 69 | %%%=================================================================== 70 | %%% Supervisor callbacks 71 | %%%=================================================================== 72 | 73 | %%-------------------------------------------------------------------- 74 | %% @private 75 | %% @doc 76 | %% 77 | %% @end 78 | %%-------------------------------------------------------------------- 79 | 80 | init([]) -> 81 | {error, no_node_list}; 82 | init([Nodes]) -> 83 | init([Nodes, []]); 84 | init([Nodes, Opts]) -> 85 | RestartStrategy = one_for_one, 86 | MaxRestarts = 2, 87 | MaxSecondsBetweenRestarts = 3600, 88 | 89 | SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, 90 | 91 | Restart = permanent, 92 | Shutdown = 2000, 93 | Type = worker, 94 | 95 | LeaderCron = {leader_cron, {leader_cron, start_link, [Nodes, Opts]}, 96 | Restart, Shutdown, Type, [leader_cron]}, 97 | 98 | {ok, {SupFlags, [LeaderCron]}}. 99 | 100 | %%%=================================================================== 101 | %%% Internal functions 102 | %%%=================================================================== 103 | -------------------------------------------------------------------------------- /src/leader_cron_task.erl: -------------------------------------------------------------------------------- 1 | %%============================================================================== 2 | %% Copyright 2012 Jeremy Raymond 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%============================================================================== 16 | 17 | %%%------------------------------------------------------------------- 18 | %%% @author Jeremy Raymond 19 | %%% @copyright (C) 2012, Jeremy Raymond 20 | %%% @doc 21 | %%% The leader_cron_task module provides different methods for scheduling 22 | %%% a task to be executed periodically in the future. The supported methods 23 | %%% are one shot, sleeper, and cron mode. 24 | %%% 25 | %%% A oneshot schedule executes a task once after sleeping a specified 26 | %%% number of milliseconds or at a given datetime. 27 | %%% 28 | %%% 29 | %%% {oneshot, 60000} % execute task once after waiting a minute
30 | %%% {oneshot, {{2012, 2, 23}, {1, 0, 0}}} % execute task on Feb 23, 2012 at 1 am 31 | %%%
32 | %%% 33 | %%% A sleeper mode schedule repeatedly executes a task then sleeps for a 34 | %%% specified number of milliseconds before repeating the task. 35 | %%% 36 | %%% {sleeper, 5000} % execute task then wait 5 seconds before the 37 | %%% next execution 38 | %%% 39 | %%% A cron mode schedule acts similarly to Unix cron. The schedule is 40 | %%% defined by the cron tuple 41 | %%% 42 | %%% {cron, {Minute, Hour, DayOfMonth, Month, DayOfWeek}} 43 | %%% 44 | %%% The valid range of values for these fields are 45 | %%% 46 | %%%
 47 | %%% Field         Valid Range
 48 | %%% ------------  -------------------
 49 | %%% minute        0 - 59
 50 | %%% hour          0 - 23
 51 | %%% day of month  1 - 31
 52 | %%% month         1 - 12
 53 | %%% day of week   0 - 6 (Sunday is 0) 
54 | %%% 55 | %%% The semantics of these fields align with Unix cron. Each field 56 | %%% specifies which values in the range are valid for task execution. The 57 | %%% values can be given as a range, a list or the atom 'all'. 58 | %%% 59 | %%%
 60 | %%% Field Spec                     Example            Unix Cron
 61 | %%% -----------------------------  -----------------  ---------
 62 | %%% all                            all                *
 63 | %%% {integer(), integer{}}         {1, 5}             1-5
 64 | %%% [integer()]                    [1, 3, 7]          1,3,7
 65 | %%%
 66 | %%% # old range and list format is also supported
 67 | %%% {range, integer(), integer()}  {range, 1, 5}      1-5
 68 | %%% {list, [integer()]}            {list, [1, 3, 7]}  1,3,7
69 | %%% 70 | %%% If the day of month is set to a day which does not exist in the current 71 | %%% month (such as 31 for February) the day is skipped. Setting day of month 72 | %%% to 31 does _not_ mean the last day of the month. This aligns with Unix 73 | %%% cron. 74 | %%% 75 | %%% Specified dates and times are all handled in UTC. 76 | %%% 77 | %%% When a task takes longer than the time to the next valid period (or 78 | %%% periods) the overlapped periods are skipped. 79 | %%% 80 | %%% @end 81 | %%% Created : 1 Feb 2012 by Jeremy Raymond 82 | %%%------------------------------------------------------------------- 83 | 84 | -module(leader_cron_task). 85 | 86 | -behaviour(gen_server). 87 | 88 | %% API 89 | -export([start_link/2, status/1, stop/1]). 90 | 91 | %% gen_server callbacks 92 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 93 | terminate/2, code_change/3]). 94 | 95 | -export_type([sleeper/0, cron/0, execargs/0, datetime/0, status/0, schedule/0]). 96 | 97 | -define(SERVER, ?MODULE). 98 | 99 | -record(state, { 100 | schedule :: schedule(), 101 | exec :: execargs(), 102 | task_pid :: pid(), 103 | status :: status(), 104 | next}). 105 | 106 | -define(DAY_IN_SECONDS, 86400). 107 | -define(HOUR_IN_SECONDS, 3600). 108 | -define(MINUTE_IN_SECONDS, 60). 109 | 110 | -type schedule() :: oneshot() | sleeper() | cron(). 111 | %% A cron schedule. 112 | 113 | -type oneshot() :: {oneshot, Millis::pos_integer() | datetime()}. 114 | %% Schedule a task once after a delay or on a particular date. 115 | 116 | -type sleeper() :: {sleeper, Millis::pos_integer()}. 117 | %% Repeating schedule sleeping between executions. 118 | 119 | -type cron() :: {cron, {Minute :: cronspec(), 120 | Hour :: cronspec(), 121 | DayOfMonth :: cronspec(), 122 | Month :: cronspec(), 123 | DayOfWeek :: cronspec()}}. 124 | %% Unix like cron schedule representing the five cron fields: 125 | %% minute, hour, day of month, month, day of week. 126 | 127 | -type cronspec() :: all | [rangespec() | listspec()]. 128 | %% Cron field value. Atom all for all values (e.g. *) or one of rangespec() 129 | %% or listspec(). 130 | 131 | -type rangespec() :: {range, Min :: integer(), Max :: integer()} 132 | | {Min :: integer(), Max :: integer()}. 133 | %% Represents a cron range (e.g. 1-5). 134 | 135 | -type listspec() :: {list, Values :: [integer()]} | [integer()] | integer(). 136 | %% Represents a cron list (e.g. 1,3,7) 137 | 138 | -type status() :: waiting | running | done | error. 139 | %% Task execution status. 140 | 141 | -type execargs() :: mfargs() | funcargs(). 142 | %% Task execution type. 143 | 144 | -type mfargs() :: {Module :: atom(), Function :: atom(), Args :: [term()]}. 145 | %% Function execution definition. 146 | 147 | -type funcargs() :: {Function :: fun(), Args :: [term()]}. 148 | %% Anonymous function execution definition. 149 | 150 | -type datetime() :: calendar:datetime(). 151 | %% Date and time. 152 | 153 | %%%=================================================================== 154 | %%% API 155 | %%%=================================================================== 156 | 157 | %%-------------------------------------------------------------------- 158 | %% @doc 159 | %% Creates a linked process which schedules the function in the 160 | %% specified module with the given arguments to be run according 161 | %% to the given schedule. 162 | %% 163 | %% @end 164 | %%-------------------------------------------------------------------- 165 | 166 | -spec start_link(Schedule, Exec) -> {ok, pid()} | {error, Reason} when 167 | Schedule :: schedule(), 168 | Exec :: execargs(), 169 | Reason :: term(). 170 | 171 | start_link(Schedule, Exec) -> 172 | gen_server:start_link(?MODULE, [{Schedule, Exec}], []). 173 | 174 | %%-------------------------------------------------------------------- 175 | %% @doc 176 | %% Gets the current status of the task and the trigger time. If running 177 | %% the trigger time denotes the time the task started. If waiting the 178 | %% time denotes the next time the task will run. If done the time the 179 | %% task ran. If error the cause of the error. 180 | %% 181 | %% @end 182 | %%-------------------------------------------------------------------- 183 | 184 | -spec status(pid()) -> {Status, ScheduleTime, TaskPid} when 185 | Status :: status(), 186 | ScheduleTime :: datetime() | pos_integer() | {error, Reason}, 187 | Reason :: term(), 188 | TaskPid :: pid(). 189 | 190 | status(Pid) -> 191 | gen_server:call(Pid, status). 192 | 193 | %%-------------------------------------------------------------------- 194 | %% @doc 195 | %% Stops the task. 196 | %% 197 | %% @end 198 | %%-------------------------------------------------------------------- 199 | 200 | -spec stop(pid()) -> ok. 201 | 202 | stop(Pid) -> 203 | gen_server:cast(Pid, stop). 204 | 205 | %%%=================================================================== 206 | %%% gen_server callbacks 207 | %%%=================================================================== 208 | 209 | %%-------------------------------------------------------------------- 210 | %% @private 211 | %% @doc 212 | %% Initializes the server 213 | %% 214 | %% @end 215 | %%-------------------------------------------------------------------- 216 | 217 | -spec init([{schedule(), execargs()}]) -> {ok, #state{}}. 218 | 219 | init([{Schedule, Exec}]) -> 220 | Self = self(), 221 | Pid = spawn_link(fun() -> 222 | case Schedule of 223 | {oneshot, _} -> 224 | oneshot(Schedule, Exec, Self); 225 | _ -> 226 | run_task(Schedule, Exec, Self) 227 | end 228 | end), 229 | {ok, #state{schedule = Schedule, 230 | exec = Exec, 231 | task_pid = Pid}}. 232 | 233 | %%-------------------------------------------------------------------- 234 | %% @private 235 | %% @doc 236 | %% Handling call messages 237 | %% 238 | %% @end 239 | %%-------------------------------------------------------------------- 240 | 241 | handle_call(status, _From, State) -> 242 | Status = State#state.status, 243 | Next = State#state.next, 244 | TaskPid = State#state.task_pid, 245 | {reply, {Status, Next, TaskPid}, State}. 246 | 247 | %%-------------------------------------------------------------------- 248 | %% @private 249 | %% @doc 250 | %% Handling cast messages 251 | %% 252 | %% @end 253 | %%-------------------------------------------------------------------- 254 | 255 | handle_cast({error, Message}, State) -> 256 | {noreply, State#state{status = error, next = Message}}; 257 | handle_cast({done, Schedule}, State) -> 258 | {noreply, State#state{status = done, next = Schedule}}; 259 | handle_cast({waiting, NextValidDateTime}, State) -> 260 | {noreply, State#state{status = waiting, next = NextValidDateTime}}; 261 | handle_cast({running, NextValidDateTime}, State) -> 262 | {noreply, State#state{status = running, next = NextValidDateTime}}; 263 | handle_cast(stop, State) -> 264 | {stop, normal, State}. 265 | 266 | %%-------------------------------------------------------------------- 267 | %% @private 268 | %% @doc 269 | %% Handling all non call/cast messages 270 | %% 271 | %% @end 272 | %%-------------------------------------------------------------------- 273 | 274 | handle_info(_Info, State) -> 275 | {noreply, State}. 276 | 277 | %%-------------------------------------------------------------------- 278 | %% @private 279 | %% @doc 280 | %% 281 | %% @end 282 | %%-------------------------------------------------------------------- 283 | 284 | terminate(_Reason, State) -> 285 | exit(State#state.task_pid, kill), 286 | ok. 287 | 288 | %%-------------------------------------------------------------------- 289 | %% @private 290 | %% @doc 291 | %% Convert process state when code is changed 292 | %% 293 | %% @end 294 | %%-------------------------------------------------------------------- 295 | 296 | code_change(_OldVsn, State, _Extra) -> 297 | {ok, State}. 298 | 299 | %%%=================================================================== 300 | %%% Internal functions 301 | %%%=================================================================== 302 | 303 | oneshot({oneshot, Millis}, Exec, ParentPid) when is_integer(Millis) -> 304 | gen_server:cast(ParentPid, {waiting, Millis}), 305 | sleep_accounting_for_max(Millis), 306 | gen_server:cast(ParentPid, {running, Millis}), 307 | apply_task(Exec), 308 | gen_server:cast(ParentPid, {done, Millis}); 309 | oneshot({oneshot, DateTime}, Exec, ParentPid) -> 310 | CurrentDateTime = calendar:universal_time(), 311 | CurrentSeconds = calendar:datetime_to_gregorian_seconds(CurrentDateTime), 312 | ScheduleSeconds = calendar:datetime_to_gregorian_seconds(DateTime), 313 | WaitSeconds = ScheduleSeconds - CurrentSeconds, 314 | case WaitSeconds > 0 of 315 | true -> 316 | gen_server:cast(ParentPid, {waiting, DateTime}), 317 | sleep_accounting_for_max(WaitSeconds * 1000), 318 | gen_server:cast(ParentPid, {running, DateTime}), 319 | apply_task(Exec), 320 | gen_server:cast(ParentPid, {done, DateTime}); 321 | false -> 322 | Format = "Schedule datetime ~p is in the past", 323 | Message = lists:flatten(io_lib:format(Format, [DateTime])), 324 | error_logger:error_report(Message), 325 | gen_server:cast(ParentPid, {error, Message}) 326 | end. 327 | 328 | run_task({sleeper, Millis}, Exec, ParentPid) -> 329 | gen_server:cast(ParentPid, {running, Millis}), 330 | apply_task(Exec), 331 | gen_server:cast(ParentPid, {waiting, Millis}), 332 | sleep_accounting_for_max(Millis), 333 | run_task({sleeper, Millis}, Exec, ParentPid); 334 | run_task(Schedule, Exec, ParentPid) -> 335 | CurrentDateTime = calendar:universal_time(), 336 | NextValidDateTime = next_valid_datetime(Schedule, CurrentDateTime), 337 | SleepFor = time_to_wait_millis(CurrentDateTime, NextValidDateTime), 338 | gen_server:cast(ParentPid, {waiting, NextValidDateTime}), 339 | sleep_accounting_for_max(SleepFor), 340 | gen_server:cast(ParentPid, {running, NextValidDateTime}), 341 | apply_task(Exec), 342 | run_task(Schedule, Exec, ParentPid). 343 | 344 | -spec apply_task(execargs()) -> any(). 345 | 346 | apply_task(Exec) -> 347 | try 348 | case Exec of 349 | {M, F, A} -> 350 | apply(M, F, A); 351 | {F, A} -> 352 | apply(F, A) 353 | end 354 | catch 355 | Error:Reason -> 356 | Stacktrace = erlang:get_stacktrace(), 357 | Format = "Task ~p in process ~p with value:~n~p", 358 | Message = lists:flatten(io_lib:format( 359 | Format, 360 | [Error, self(), {Reason, Stacktrace}])), 361 | error_logger:error_report(Message) 362 | end. 363 | 364 | -spec time_to_wait_millis(datetime(), datetime()) -> integer(). 365 | 366 | time_to_wait_millis(CurrentDateTime, NextDateTime) -> 367 | CurrentSeconds = calendar:datetime_to_gregorian_seconds(CurrentDateTime), 368 | NextSeconds = calendar:datetime_to_gregorian_seconds(NextDateTime), 369 | SecondsToSleep = NextSeconds - CurrentSeconds, 370 | SecondsToSleep * 1000. 371 | 372 | -spec next_valid_datetime(cron(), datetime()) -> datetime(). 373 | 374 | next_valid_datetime({cron, Schedule}, DateTime) -> 375 | DateTime1 = advance_seconds(DateTime, ?MINUTE_IN_SECONDS), 376 | {{Y, Mo, D}, {H, M, _}} = DateTime1, 377 | DateTime2 = {{Y, Mo, D}, {H, M, 0}}, 378 | next_valid_datetime(not_done, {cron, Schedule}, DateTime2). 379 | 380 | -spec next_valid_datetime(done|not_done, cron(), datetime()) -> datetime(). 381 | 382 | next_valid_datetime(done, _, DateTime) -> 383 | DateTime; 384 | next_valid_datetime(not_done, {cron, Schedule}, DateTime) -> 385 | {MinuteSpec, HourSpec, DayOfMonthSpec, MonthSpec, DayOfWeekSpec} = 386 | Schedule, 387 | {{Year, Month, Day}, {Hour, Minute, _}} = DateTime, 388 | {Done, Time} = 389 | case value_valid(MonthSpec, 1, 12, Month) of 390 | false -> 391 | case Month of 392 | 12 -> 393 | {not_done, {{Year + 1, 1, 1}, {0, 0, 0}}}; 394 | Month -> 395 | {not_done, {{Year, Month + 1, 1}, {0, 0, 0}}} 396 | end; 397 | true -> 398 | DayOfWeek = case calendar:day_of_the_week(Year, Month, Day) of 399 | 7 -> 400 | 0; % we want 0 to be Sunday not 7 401 | DOW -> 402 | DOW 403 | end, 404 | DOMValid = value_valid(DayOfMonthSpec, 1, 31, Day), 405 | DOWValid = value_valid(DayOfWeekSpec, 0, 6, DayOfWeek), 406 | case (((DayOfMonthSpec /= all) and 407 | (DayOfWeekSpec /= all) and 408 | (DOMValid or DOWValid)) or (DOMValid and DOWValid)) of 409 | false -> 410 | Temp1 = advance_seconds(DateTime, ?DAY_IN_SECONDS), 411 | {{Y, M, D}, {_, _, _}} = Temp1, 412 | {not_done, {{Y, M, D}, {0, 0, 0}}}; 413 | true -> 414 | case value_valid(HourSpec, 0, 23, Hour) of 415 | false -> 416 | Temp3 = advance_seconds(DateTime, 417 | ?HOUR_IN_SECONDS), 418 | {{Y, M, D}, {H, _, _}} = Temp3, 419 | {not_done, {{Y, M, D}, {H, 0, 0}}}; 420 | true -> 421 | case value_valid( 422 | MinuteSpec, 0, 59, Minute) of 423 | false -> 424 | {not_done, advance_seconds( 425 | DateTime, 426 | ?MINUTE_IN_SECONDS)}; 427 | true -> 428 | {done, DateTime} 429 | end 430 | end 431 | end 432 | end, 433 | next_valid_datetime(Done, {cron, Schedule}, Time). 434 | 435 | -spec value_valid(cronspec(), integer(), integer(), integer()) -> true | false. 436 | 437 | value_valid(Spec, Min, Max, Value) when Value >= Min, Value =< Max-> 438 | case Spec of 439 | all -> 440 | true; 441 | Spec -> 442 | ValidValues = extract_integers(Spec, Min, Max), 443 | lists:any(fun(Item) -> 444 | Item == Value 445 | end, ValidValues) 446 | end. 447 | 448 | -spec advance_seconds(datetime(), integer()) -> datetime(). 449 | 450 | advance_seconds(DateTime, Seconds) -> 451 | Seconds1 = calendar:datetime_to_gregorian_seconds(DateTime) + Seconds, 452 | calendar:gregorian_seconds_to_datetime(Seconds1). 453 | 454 | -spec extract_integers([rangespec()|listspec()], integer(), integer()) -> 455 | [integer()]. 456 | 457 | extract_integers(Spec, Min, Max) when Min < Max -> 458 | extract_integers(Spec, Min, Max, []). 459 | 460 | -spec extract_integers(Spec, Min, Max, Acc) -> Integers when 461 | Spec :: [rangespec()|listspec()], 462 | Min :: integer(), 463 | Max :: integer(), 464 | Acc :: list(), 465 | Integers :: [integer()]. 466 | 467 | extract_integers([], Min, Max, Acc) -> 468 | Integers = lists:sort(sets:to_list(sets:from_list(lists:flatten(Acc)))), 469 | lists:foreach(fun(Int) -> 470 | if 471 | Int < Min -> 472 | throw({error, {out_of_range, {min, Min}, 473 | {value, Int}}}); 474 | Int > Max -> 475 | throw({error, {out_of_range, {max, Max}, 476 | {value, Int}}}); 477 | true -> 478 | ok 479 | end 480 | end, Integers), 481 | Integers; 482 | extract_integers(Spec, Min, Max, Acc) -> 483 | [H|T] = Spec, 484 | Values = case H of 485 | {range, Lower, Upper} when Lower < Upper -> 486 | lists:seq(Lower, Upper); 487 | {list, List} -> 488 | List; 489 | {Lower, Upper} when Lower < Upper -> 490 | lists:seq(Lower, Upper); 491 | List when is_list(List) -> 492 | List; 493 | Integer when is_integer(Integer) -> 494 | [Integer] 495 | end, 496 | extract_integers(T, Min, Max, [Values|Acc]). 497 | 498 | -define(LONG_SLEEP_TIME, 100000000). 499 | 500 | sleep_accounting_for_max(TimeInMillis) -> 501 | case (TimeInMillis > ?LONG_SLEEP_TIME) of 502 | true -> timer:sleep(TimeInMillis rem ?LONG_SLEEP_TIME), long_sleep(TimeInMillis div ?LONG_SLEEP_TIME); 503 | false -> timer:sleep(TimeInMillis) 504 | end. 505 | 506 | long_sleep(0) -> ok; 507 | long_sleep(Chunks) -> 508 | timer:sleep(?LONG_SLEEP_TIME), 509 | long_sleep(Chunks - 1). 510 | 511 | %%%=================================================================== 512 | %%% Unit Tests 513 | %%%=================================================================== 514 | 515 | -ifdef(TEST). 516 | 517 | -compile(export_all). 518 | 519 | -include_lib("eunit/include/eunit.hrl"). 520 | 521 | oneshot_anon_test() -> 522 | Schedule = {oneshot, 500}, 523 | Fun = fun(T) -> timer:sleep(T) end, 524 | {ok, Pid} = leader_cron_task:start_link(Schedule, {Fun, [500]}), 525 | {_, _, TaskPid} = leader_cron_task:status(Pid), 526 | ?assertMatch({waiting, 500, _}, leader_cron_task:status(Pid)), 527 | timer:sleep(550), 528 | ?assertMatch({running, 500, _}, leader_cron_task:status(Pid)), 529 | ?assertEqual(true, is_process_alive(TaskPid)), 530 | timer:sleep(550), 531 | ?assertMatch({done, 500, _}, leader_cron_task:status(Pid)), 532 | ?assertEqual(false, is_process_alive(TaskPid)). 533 | 534 | oneshot_millis_test() -> 535 | Schedule = {oneshot, 500}, 536 | {ok, Pid} = leader_cron_task:start_link(Schedule, {timer, sleep, [500]}), 537 | {_, _, TaskPid} = leader_cron_task:status(Pid), 538 | ?assertMatch({waiting, 500, _}, leader_cron_task:status(Pid)), 539 | timer:sleep(550), 540 | ?assertMatch({running, 500, _}, leader_cron_task:status(Pid)), 541 | ?assertEqual(true, is_process_alive(TaskPid)), 542 | timer:sleep(550), 543 | ?assertMatch({done, 500, _}, leader_cron_task:status(Pid)), 544 | ?assertEqual(false, is_process_alive(TaskPid)). 545 | 546 | oneshot_datetime_test() -> 547 | DateTime = advance_seconds(calendar:universal_time(), 2), 548 | Schedule = {oneshot, DateTime}, 549 | {ok, Pid} = leader_cron_task:start_link(Schedule, {timer, sleep, [500]}), 550 | {_, _, TaskPid} = leader_cron_task:status(Pid), 551 | ?assertMatch({waiting, DateTime, _}, leader_cron_task:status(Pid)), 552 | timer:sleep(2100), 553 | ?assertMatch({running, DateTime, _}, leader_cron_task:status(Pid)), 554 | ?assertEqual(true, is_process_alive(TaskPid)), 555 | timer:sleep(550), 556 | ?assertMatch({done, DateTime, _}, leader_cron_task:status(Pid)), 557 | ?assertEqual(false, is_process_alive(TaskPid)). 558 | 559 | oneshot_in_the_past_test() -> 560 | DateTime = {{1970, 1, 1}, {1, 1, 1}}, 561 | Schedule = {oneshot, DateTime}, 562 | {ok, Pid} = leader_cron_task:start_link(Schedule, {timer, sleep, [500]}), 563 | {_, _, TaskPid} = leader_cron_task:status(Pid), 564 | timer:sleep(500), 565 | ?assertMatch({error, _, _}, leader_cron_task:status(Pid)), 566 | ?assertEqual(false, is_process_alive(TaskPid)). 567 | 568 | nominal_sleeper_workflow_test() -> 569 | Schedule = {sleeper, 1000}, 570 | {ok, Pid} = leader_cron_task:start_link( 571 | Schedule, 572 | {timer, sleep, [1000]}), 573 | {_, _, TaskPid} = leader_cron_task:status(Pid), 574 | ?assertMatch({running, 1000, _}, leader_cron_task:status(Pid)), 575 | timer:sleep(1500), 576 | ?assertMatch({waiting, 1000, _}, leader_cron_task:status(Pid)), 577 | timer:sleep(1000), 578 | ?assertMatch({running, 1000, _}, leader_cron_task:status(Pid)), 579 | ?assertEqual(true, is_process_alive(TaskPid)), 580 | ?assertEqual(ok, leader_cron_task:stop(Pid)), 581 | timer:sleep(100), 582 | ?assertEqual(false, is_process_alive(TaskPid)), 583 | ?assertException(exit, 584 | {noproc,{gen_server,call,[Pid, status]}}, 585 | leader_cron_task:status(Pid)). 586 | 587 | nominal_cron_workflow_test_() -> 588 | {timeout, 90, 589 | fun() -> 590 | Schedule = {cron, {all, all, all, all, all}}, 591 | {ok, Pid} = leader_cron_task:start_link( 592 | Schedule, 593 | {timer, sleep, [5000]}), 594 | Current = calendar:universal_time(), 595 | Next = next_valid_datetime(Schedule, Current), 596 | WaitFor = time_to_wait_millis(Current, Next), 597 | ?assertMatch({waiting, Next, _}, leader_cron_task:status(Pid)), 598 | timer:sleep(WaitFor + 2000), 599 | ?assertMatch({running, Next, _}, leader_cron_task:status(Pid)), 600 | timer:sleep(4000), 601 | Next1 = next_valid_datetime(Schedule, Next), 602 | ?assertMatch({waiting, Next1, _}, leader_cron_task:status(Pid)), 603 | ?assertEqual(ok, leader_cron_task:stop(Pid)), 604 | ?assertException(exit, 605 | {normal,{gen_server,call,[Pid, status]}}, 606 | leader_cron_task:status(Pid)) 607 | end}. 608 | 609 | invalid_range_test() -> 610 | ?assertException(throw, {error, {out_of_range, {min, 2}, {value, 1}}}, 611 | extract_integers([], 2, 10, [1])), 612 | ?assertException(throw, {error, {out_of_range, {max, 2}, {value, 3}}}, 613 | extract_integers([], 1, 2, [3])). 614 | 615 | extract_integers_test() -> 616 | ?assertException(error, function_clause, extract_integers([], 5, 4)), 617 | ?assertException(error, {case_clause, bad}, extract_integers([bad], 0, 5)), 618 | ?assertEqual([1,2,3,4,5], extract_integers([{range, 1, 5}], 0, 10)), 619 | ?assertEqual([1,2,3,4,5], extract_integers([{1, 5}], 0, 10)), 620 | ?assertEqual([1,2,3,4,5], extract_integers([{list, [1,2,3,4,5]}], 0, 10)), 621 | ?assertEqual([1,2,3,4,5], extract_integers([[1,2,3,4,5]], 0, 10)), 622 | ?assertEqual([5], extract_integers([{list, [5]}], 0, 10)), 623 | ?assertEqual([5], extract_integers([5], 0, 10)). 624 | 625 | next_valid_datetime_cron_test() -> 626 | % roll year 627 | ?assertEqual({{2013, 1, 1}, {0, 0, 0}}, 628 | next_valid_datetime({cron, {all, all, all, all, all}}, 629 | {{2012, 12, 31}, {23, 59, 48}})), 630 | % last second of minute (we skip a second) 631 | ?assertEqual({{2012, 1, 1}, {0, 1, 0}}, 632 | next_valid_datetime({cron, {all, all, all, all, all}}, 633 | {{2012, 1, 1}, {0, 0, 59}})), 634 | % 12th month rolls year 635 | ?assertEqual({{2013, 2, 1}, {0, 0, 0}}, 636 | next_valid_datetime({cron, {all, all, all, 637 | [{list, [2]}], all}}, 638 | {{2012, 12, 1}, {0, 0, 0}})), 639 | % normal month advance 640 | ?assertEqual({{2012, 12, 1}, {0, 0, 0}}, 641 | next_valid_datetime( 642 | {cron, {all, all, all, [{list, [12]}], all}}, 643 | {{2012, 4, 1}, {0, 0, 0}})), 644 | % day of month (no day of week) 645 | ?assertEqual({{2012, 1, 13}, {0, 0, 0}}, 646 | next_valid_datetime( 647 | {cron, {all, all, [{list, [13]}], all, all}}, 648 | {{2012, 1, 5}, {0, 0, 0}})), 649 | % day of week (no day of month) 650 | ?assertEqual({{2012, 2, 10}, {0, 0, 0}}, 651 | next_valid_datetime( 652 | {cron, {all, all, all, all, [{list, [5]}]}}, % 5 is Friday 653 | {{2012, 2, 7}, {0, 0, 0}})), 654 | % day of week and day of month (day of month comes first and wins) 655 | ?assertEqual({{2012, 2, 8}, {0, 0, 0}}, 656 | next_valid_datetime( 657 | {cron, {all, all, [{list, [8]}], all, [{list, [5]}]}}, 658 | {{2012, 2, 7}, {0, 0, 0}})), 659 | % day of week and day of month (day of week comes first and wins) 660 | ?assertEqual({{2012, 2, 10}, {0, 0, 0}}, 661 | next_valid_datetime( 662 | {cron, {all, all, [{list, [12]}], all, [{list, [5]}]}}, 663 | {{2012, 2, 7}, {0, 0, 0}})), 664 | % hour advance 665 | ?assertEqual({{2012, 1, 1}, {22, 0, 0}}, 666 | next_valid_datetime( 667 | {cron, {all, [{list, [22]}], all, all, all}}, 668 | {{2012, 1, 1}, {0, 0, 0}})), 669 | % minute advance 670 | ?assertEqual({{2012, 1, 1}, {0, 59, 0}}, 671 | next_valid_datetime( 672 | {cron, {[{list, [59]}], all, all, all, all}}, 673 | {{2012, 1, 1}, {0, 0, 0}})). 674 | 675 | time_to_wait_millis_test() -> 676 | ?assertEqual(60000, time_to_wait_millis( 677 | {{2012, 1, 1}, {0, 0, 0}}, 678 | {{2012, 1, 1}, {0, 1, 0}})). 679 | 680 | -endif. 681 | -------------------------------------------------------------------------------- /test/leader_cron_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%============================================================================== 2 | %% Copyright 2012 Jeremy Raymond 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%============================================================================== 16 | 17 | %%%------------------------------------------------------------------- 18 | %%% @author Jeremy Raymond 19 | %%% @copyright (C) 2012, Jeremy Raymond 20 | %%% @doc 21 | %%% System tests for leader_cron 22 | %%% 23 | %%% @end 24 | %%% Created : 15 Feb 2012 by Jeremy Raymond 25 | %%%------------------------------------------------------------------- 26 | -module(leader_cron_SUITE). 27 | 28 | -compile(export_all). 29 | 30 | -include_lib("common_test/include/ct.hrl"). 31 | 32 | %%-------------------------------------------------------------------- 33 | %% @spec suite() -> Info 34 | %% Info = [tuple()] 35 | %% @end 36 | %%-------------------------------------------------------------------- 37 | suite() -> 38 | [{timetrap,{seconds,30}}]. 39 | 40 | %%-------------------------------------------------------------------- 41 | %% @spec init_per_suite(Config0) -> 42 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 43 | %% Config0 = Config1 = [tuple()] 44 | %% Reason = term() 45 | %% @end 46 | %%-------------------------------------------------------------------- 47 | init_per_suite(Config) -> 48 | CodePath = string:join(code:get_path(), " "), 49 | ErlFlags = "-pa " ++ CodePath, 50 | Nodes = lists:foldl( 51 | fun(Name, Acc) -> 52 | {ok, Node} = ct_slave:start( 53 | list_to_atom(net_adm:localhost()), 54 | Name, [{erl_flags, ErlFlags}]), 55 | [Node|Acc] 56 | end, [], [test1, test2, test3]), 57 | [{nodes, Nodes} | Config]. 58 | 59 | %%-------------------------------------------------------------------- 60 | %% @spec end_per_suite(Config0) -> void() | {save_config,Config1} 61 | %% Config0 = Config1 = [tuple()] 62 | %% @end 63 | %%-------------------------------------------------------------------- 64 | end_per_suite(_Config) -> 65 | lists:foreach(fun (Node) -> {ok, _} = ct_slave:stop(Node) end, 66 | [test1, test2, test3]), 67 | ok. 68 | 69 | %%-------------------------------------------------------------------- 70 | %% @spec init_per_group(GroupName, Config0) -> 71 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 72 | %% GroupName = atom() 73 | %% Config0 = Config1 = [tuple()] 74 | %% Reason = term() 75 | %% @end 76 | %%-------------------------------------------------------------------- 77 | init_per_group(_GroupName, Config) -> 78 | Config. 79 | 80 | %%-------------------------------------------------------------------- 81 | %% @spec end_per_group(GroupName, Config0) -> 82 | %% void() | {save_config,Config1} 83 | %% GroupName = atom() 84 | %% Config0 = Config1 = [tuple()] 85 | %% @end 86 | %%-------------------------------------------------------------------- 87 | end_per_group(_GroupName, _Config) -> 88 | ok. 89 | 90 | %%-------------------------------------------------------------------- 91 | %% @spec init_per_testcase(TestCase, Config0) -> 92 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 93 | %% TestCase = atom() 94 | %% Config0 = Config1 = [tuple()] 95 | %% Reason = term() 96 | %% @end 97 | %%-------------------------------------------------------------------- 98 | init_per_testcase(_TestCase, Config) -> 99 | Config. 100 | 101 | %%-------------------------------------------------------------------- 102 | %% @spec end_per_testcase(TestCase, Config0) -> 103 | %% void() | {save_config,Config1} | {fail,Reason} 104 | %% TestCase = atom() 105 | %% Config0 = Config1 = [tuple()] 106 | %% Reason = term() 107 | %% @end 108 | %%-------------------------------------------------------------------- 109 | end_per_testcase(_TestCase, _Config) -> 110 | ok. 111 | 112 | %%-------------------------------------------------------------------- 113 | %% @spec groups() -> [Group] 114 | %% Group = {GroupName,Properties,GroupsAndTestCases} 115 | %% GroupName = atom() 116 | %% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] 117 | %% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] 118 | %% TestCase = atom() 119 | %% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} 120 | %% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | 121 | %% repeat_until_any_ok | repeat_until_any_fail 122 | %% N = integer() | forever 123 | %% @end 124 | %%-------------------------------------------------------------------- 125 | groups() -> 126 | []. 127 | 128 | %%-------------------------------------------------------------------- 129 | %% @spec all() -> GroupsAndTestCases | {skip,Reason} 130 | %% GroupsAndTestCases = [{group,GroupName} | TestCase] 131 | %% GroupName = atom() 132 | %% TestCase = atom() 133 | %% Reason = term() 134 | %% @end 135 | %%-------------------------------------------------------------------- 136 | all() -> 137 | [basic_fail_over]. 138 | 139 | %%-------------------------------------------------------------------- 140 | %% @spec TestCase() -> Info 141 | %% Info = [tuple()] 142 | %% @end 143 | %%-------------------------------------------------------------------- 144 | basic_fail_over() -> 145 | []. 146 | 147 | %%-------------------------------------------------------------------- 148 | %% @spec TestCase(Config0) -> 149 | %% ok | exit() | {skip,Reason} | {comment,Comment} | 150 | %% {save_config,Config1} | {skip_and_save,Reason,Config1} 151 | %% Config0 = Config1 = [tuple()] 152 | %% Reason = term() 153 | %% Comment = term() 154 | %% @end 155 | %%-------------------------------------------------------------------- 156 | basic_fail_over(Config) -> 157 | Nodes = proplists:get_value(nodes, Config), 158 | 159 | % start leader_cron on all nodes 160 | lists:foreach( 161 | fun (Node) -> 162 | {ok, _} = rpc:call(Node, leader_cron, start_link, [Nodes]) 163 | end, Nodes), 164 | 165 | % wait for all nodes to join 166 | timer:sleep(6000), 167 | 168 | % pick a node to talk to 169 | [ANode|_] = Nodes, 170 | 171 | % ensure all nodes are candidate nodes 172 | SortedNodes = lists:sort(Nodes), 173 | Status = rpc:call(ANode, leader_cron, status, []), 174 | SortedNodes = lists:sort(proplists:get_value(candidates, Status)), 175 | 176 | % ensure all nodes alive 177 | SortedNodes = lists:sort(proplists:get_value(alive, Status)), 178 | 179 | % schedule a task 180 | Sched = {sleeper, 100}, 181 | Mfa = {timer, sleep, [100]}, 182 | {ok, TaskPid} = rpc:call(ANode, leader_cron, schedule_task, [Sched, Mfa]), 183 | [{undefined, TaskPid, Sched, Mfa}] = 184 | rpc:call(ANode, leader_cron, task_list, []), 185 | 186 | % ensure task is alive 187 | Leader = proplists:get_value(leader, 188 | rpc:call(ANode, leader_cron, status, [])), 189 | true = rpc:call(Leader, erlang, is_process_alive, [TaskPid]), 190 | 191 | % cause a fail over 192 | LeaderPid = rpc:call(Leader, erlang, whereis, [leader_cron]), 193 | exit(LeaderPid, kill), 194 | 195 | % verify new leader 196 | Nodes1 = [Node || Node <- Nodes, Node /= Leader], 197 | [BNode|_] = Nodes1, 198 | timer:sleep(6000), 199 | NewLeader = proplists:get_value(leader, 200 | rpc:call(BNode, leader_cron, status, [])), 201 | true = Leader /= NewLeader, 202 | 203 | % verify tasks 204 | [{_Name, TaskPid1, Sched, Mfa}] = rpc:call(BNode, leader_cron, task_list, []), 205 | true = TaskPid /= TaskPid1, 206 | 207 | % verify task running again 208 | true = rpc:call(NewLeader, erlang, is_process_alive, [TaskPid1]), 209 | 210 | % verify alive nodes 211 | Status1 = rpc:call(BNode, leader_cron, status, []), 212 | SortedNodes1 = lists:sort(Nodes1), 213 | erlang:display(SortedNodes1), 214 | SortedNodes1 = lists:sort(proplists:get_value(alive, Status1)), 215 | ok. 216 | 217 | --------------------------------------------------------------------------------