Commit | Line | Data |
74e31b02 |
1 | package Catalyst::Plugin::Scheduler; |
2 | |
3 | use strict; |
4 | use warnings; |
5 | use NEXT; |
ba2735b6 |
6 | use base qw/Class::Accessor::Fast Class::Data::Inheritable Catalyst::Base/; |
7 | use Catalyst::Plugin::Scheduler::Base; |
74e31b02 |
8 | |
ba2735b6 |
9 | our $VERSION = '0.07_01'; |
74e31b02 |
10 | |
74e31b02 |
11 | |
12 | =pod |
13 | |
14 | =head1 NAME |
15 | |
16 | Catalyst::Plugin::Scheduler - Schedule events to run in a cron-like fashion |
17 | |
18 | =head1 SYNOPSIS |
19 | |
20 | use Catalyst qw/Scheduler/; |
21 | |
22 | # run remove_sessions in the Cron controller every hour |
23 | __PACKAGE__->schedule( |
24 | at => '0 * * * *', |
25 | event => '/cron/remove_sessions' |
26 | ); |
27 | |
28 | # Run a subroutine at 4:05am every Sunday |
29 | __PACKAGE__->schedule( |
30 | at => '5 4 * * sun', |
31 | event => \&do_stuff, |
32 | ); |
33 | |
68f800bd |
34 | # A long-running scheduled event that must be triggered |
35 | # manually by an authorized user |
36 | __PACKAGE__->schedule( |
37 | trigger => 'rebuild_search_index', |
38 | event => '/cron/rebuild_search_index', |
39 | ); |
ba2735b6 |
40 | |
68f800bd |
41 | $ wget -q http://www.myapp.com/?schedule_trigger=rebuild_search_index |
f9d8e3cf |
42 | |
ba2735b6 |
43 | |
74e31b02 |
44 | =head1 DESCRIPTION |
45 | |
46 | This plugin allows you to schedule events to run at recurring intervals. |
47 | Events will run during the first request which meets or exceeds the specified |
48 | time. Depending on the level of traffic to the application, events may or may |
49 | not run at exactly the correct time, but it should be enough to satisfy many |
50 | basic scheduling needs. |
51 | |
ba2735b6 |
52 | |
74e31b02 |
53 | =head1 CONFIGURATION |
54 | |
55 | Configuration is optional and is specified in MyApp->config->{scheduler}. |
56 | |
57 | =head2 logging |
58 | |
59 | Set to 1 to enable logging of events as they are executed. This option is |
60 | enabled by default when running under -Debug mode. Errors are always logged |
61 | regardless of the value of this option. |
62 | |
cbf1ecfe |
63 | =head2 time_zone |
64 | |
65 | The time zone of your system. This will be autodetected where possible, or |
66 | will default to UTC (GMT). You can override the detection by providing a |
67 | valid L<DateTime> time zone string, such as 'America/New_York'. |
68 | |
74e31b02 |
69 | =head2 state_file |
70 | |
71 | The current state of every event is stored in a file. By default this is |
f9d8e3cf |
72 | $APP_HOME/scheduler.state. This file is created on the first request if it |
73 | does not already exist. |
74e31b02 |
74 | |
68f800bd |
75 | =head2 yaml_file |
76 | |
77 | The location of the optional YAML event configuration file. By default this |
78 | is $APP_HOME/scheduler.yml. |
79 | |
74e31b02 |
80 | =head2 hosts_allow |
81 | |
82 | This option specifies IP addresses for trusted users. This option defaults |
83 | to 127.0.0.1. Multiple addresses can be specified by using an array |
84 | reference. This option is used for both events where auto_run is set to 0 |
85 | and for manually-triggered events. |
86 | |
87 | __PACKAGE__->config->{scheduler}->{hosts_allow} = '192.168.1.1'; |
88 | __PACKAGE__->config->{scheduler}->{hosts_allow} = [ |
89 | '127.0.0.1', |
90 | '192.168.1.1' |
91 | ]; |
92 | |
ba2735b6 |
93 | =head2 check_every |
94 | |
95 | This option allows you to configure how often the scheduler should check |
96 | for pending events. By default this is set to C<60> which means C<no more> |
97 | than once per 60 seconds. |
98 | |
99 | =cut |
100 | |
101 | ### set some defaults at start up time |
102 | sub setup { |
103 | my $c = shift; |
104 | |
105 | # store the app, for usage in the base class |
106 | $c->scheduler->_app( $c ); |
107 | |
108 | # initial configuration |
109 | $c->config->{scheduler}->{logging} ||= ( $c->debug ) ? 1 : 0; |
110 | $c->config->{scheduler}->{time_zone} ||= $c->scheduler->_detect_timezone; |
111 | $c->config->{scheduler}->{state_file} ||= $c->path_to('scheduler.state'); |
112 | $c->config->{scheduler}->{yaml_file} ||= $c->path_to('scheduler.yml'); |
113 | $c->config->{scheduler}->{hosts_allow} ||= '127.0.0.1'; |
114 | $c->config->{scheduler}->{check_every} ||= 60; |
115 | |
116 | ### make sure we run our own setup FIRST, so other plugins /could/ |
117 | ### schedule things in /their/ setup |
118 | $c->NEXT::setup(@_); |
119 | } |
120 | |
121 | ### for debugging purposes |
122 | sub dump_these { |
123 | my $c = shift; |
124 | |
125 | return ( $c->NEXT::dump_these(@_) ) unless @{ $c->scheduler->_events }; |
126 | |
127 | # for debugging, we dump out a list of all events with their next |
128 | # scheduled run time |
129 | return ( |
130 | $c->NEXT::dump_these(@_), |
131 | [ 'Scheduled Events', $c->scheduler_state ], |
132 | ); |
133 | } |
134 | |
135 | =head1 METHODS |
136 | |
137 | =head2 $scheduler = MyApp->scheduler; |
138 | |
139 | This the actual C<Scheduler> object that you can query for a lot of |
140 | information. See C<Catalyst::Plugin::Scheduler::Base> for usage information. |
141 | |
142 | The below methods are shorthand methods on this object. |
143 | |
144 | =head2 $aref = MyApp->scheduler_state |
145 | |
146 | The current state of all scheduled events is available in an easy-to-use |
147 | format by calling $c->scheduler_state. You can use this data to build an |
148 | admin view into the scheduling engine, for example. This same data is also |
149 | displayed on the Catalyst debug screen. |
150 | |
151 | This method returns an array reference containing a hash reference for each |
152 | event. |
153 | |
154 | [ |
155 | { |
156 | 'last_run' => '2005-12-29 16:29:33 EST', |
157 | 'auto_run' => 1, |
158 | 'last_output' => 1, |
159 | 'at' => '0 0 * * *', |
160 | 'next_run' => '2005-12-30 00:00:00 EST', |
161 | 'event' => '/cron/session_cleanup' |
162 | }, |
163 | { |
164 | 'auto_run' => 1, |
165 | 'at' => '0 0 * * *', |
166 | 'next_run' => '2005-12-30 00:00:00 EST', |
167 | 'event' => '/cron/build_rss' |
168 | }, |
169 | ] |
170 | |
171 | =head2 MyApp->schedule( event => CODE|/path, (at => CRONTIME, auto_run => BOOL) | (trigger => GET_PARAMETER) ) |
172 | |
173 | Schedule is a class method for adding scheduled events. You can schedule |
174 | both automated and manual events, which are discussed below. For extended |
175 | options to C<shedule>, consult the C<Catalyst::Plugin::Scheduler::Event> |
176 | documentation on the C<new> method. |
74e31b02 |
177 | |
ba2735b6 |
178 | =head3 SCHEDULING AUTOMATED EVENTS |
74e31b02 |
179 | |
180 | Events are scheduled by calling the class method C<schedule>. |
181 | |
182 | MyApp->schedule( |
183 | at => '0 * * * *', |
184 | event => '/cron/remove_sessions', |
185 | ); |
186 | |
187 | package MyApp::Controller::Cron; |
188 | |
189 | sub remove_sessions : Private { |
190 | my ( $self, $c ) = @_; |
191 | |
192 | $c->delete_expired_sessions; |
193 | } |
194 | |
ba2735b6 |
195 | =head4 at |
74e31b02 |
196 | |
197 | The time to run an event is specified using L<crontab(5)>-style syntax. |
198 | |
199 | 5 0 * * * # 5 minutes after midnight, every day |
200 | 15 14 1 * * # run at 2:15pm on the first of every month |
201 | 0 22 * * 1-5 # run at 10 pm on weekdays |
202 | 5 4 * * sun # run at 4:05am every Sunday |
203 | |
204 | From crontab(5): |
205 | |
206 | field allowed values |
207 | ----- -------------- |
208 | minute 0-59 |
209 | hour 0-23 |
210 | day of month 1-31 |
211 | month 0-12 (or names, see below) |
212 | day of week 0-7 (0 or 7 is Sun, or use names) |
213 | |
ba2735b6 |
214 | Instead of the first five fields, one of the following special strings |
215 | may appear: |
74e31b02 |
216 | |
217 | string meaning |
218 | ------ ------- |
219 | @yearly Run once a year, "0 0 1 1 *". |
220 | @annually (same as @yearly) |
221 | @monthly Run once a month, "0 0 1 * *". |
222 | @weekly Run once a week, "0 0 * * 0". |
223 | @daily Run once a day, "0 0 * * *". |
224 | @midnight (same as @daily) |
225 | @hourly Run once an hour, "0 * * * *". |
ba2735b6 |
226 | @always Run every minute, "* * * * *". |
74e31b02 |
227 | |
ba2735b6 |
228 | =head4 event |
74e31b02 |
229 | |
230 | The event to run at the specified time can be either a Catalyst private |
231 | action path or a coderef. Both types of event methods will receive the $c |
232 | object from the current request, but you must not rely on any request-specific |
233 | information present in $c as it will be from a random user request at or near |
234 | the event's specified run time. |
235 | |
236 | Important: Methods used for events should be marked C<Private> so that |
237 | they can not be executed via the browser. |
238 | |
ba2735b6 |
239 | =head4 auto_run |
74e31b02 |
240 | |
241 | The auto_run parameter specifies when the event is allowed to be executed. |
242 | By default this option is set to 1, so the event will be executed during the |
243 | first request that matches the specified time in C<at>. |
244 | |
245 | If set to 0, the event will only run when a request is made by a user from |
246 | an authorized address. The purpose of this option is to allow long-running |
247 | tasks to execute only for certain users. |
248 | |
249 | MyApp->schedule( |
250 | at => '0 0 * * *', |
251 | event => '/cron/rebuild_search_index', |
252 | auto_run => 0, |
253 | ); |
254 | |
255 | package MyApp::Controller::Cron; |
256 | |
257 | sub rebuild_search_index : Private { |
258 | my ( $self, $c ) = @_; |
259 | |
260 | # rebuild the search index, this may take a long time |
261 | } |
262 | |
263 | Now, the search index will only be rebuilt when a request is made from a user |
264 | whose IP address matches the list in the C<hosts_allow> config option. To |
265 | run this event, you probably want to ping the app from a cron job. |
266 | |
f9d8e3cf |
267 | 0 0 * * * wget -q http://www.myapp.com/ |
74e31b02 |
268 | |
ba2735b6 |
269 | =head3 SCHEDULING MANUAL EVENTS |
74e31b02 |
270 | |
271 | To create an event that does not run on a set schedule and must be manually |
272 | triggered, you can specify the C<trigger> option instead of C<at>. |
273 | |
274 | __PACKAGE__->schedule( |
275 | trigger => 'send_email', |
276 | event => '/events/send_email', |
277 | ); |
278 | |
279 | The event may then be triggered by a standard web request from an authorized |
280 | user. The trigger to run is specified by using a special GET parameter, |
281 | 'schedule_trigger'; the path requested does not matter. |
282 | |
283 | http://www.myapp.com/?schedule_trigger=send_email |
284 | |
285 | By default, manual events may only be triggered by requests made from |
286 | localhost (127.0.0.1). To allow other addresses to run events, use the |
68f800bd |
287 | configuration option L</hosts_allow>. |
288 | |
ba2735b6 |
289 | |
290 | =cut |
291 | |
292 | __PACKAGE__->mk_classdata( scheduler => do { bless {}, __PACKAGE__ .'::Base'} ); |
293 | |
294 | { ### convenience wrapper |
295 | sub schedule { |
296 | my $c = shift; |
297 | return $c->scheduler->schedule( |
298 | scheduled_by => $c->scheduler->_caller_string, @_ ); |
299 | } |
300 | |
301 | sub scheduler_state { |
302 | my $c = shift; |
303 | return $c->scheduler->state( @_ ); |
304 | } |
305 | } |
306 | |
307 | =head1 INTERNAL METHODS |
308 | |
309 | The following methods are extended by this plugin. |
310 | |
311 | =over 4 |
312 | |
313 | =item dispatch |
314 | |
315 | The main scheduling logic takes place during the dispatch phase. |
316 | |
317 | =item dump_these |
318 | |
319 | On the Catalyst debug screen, all scheduled events are displayed along with |
320 | the next time they will be executed. |
321 | |
322 | =item setup |
323 | |
324 | Configuration is initialized during setup time. |
325 | |
326 | =back |
327 | |
328 | =cut |
329 | |
330 | ### run stuff at dispatch time |
331 | sub dispatch { |
332 | my $c = shift; |
333 | |
334 | $c->NEXT::dispatch(@_); |
335 | |
336 | $c->scheduler->_run_events; |
337 | } |
338 | |
339 | ### store the BLESSED $c for us to work with, at the begining of every |
340 | ### request... otherwise, we just have a class name, and no request info. |
341 | sub prepare_action { |
342 | my $c = shift; |
343 | |
344 | $c->scheduler->_app( $c ); |
345 | |
346 | $c->NEXT::prepare_action( @_ ); |
347 | } |
348 | |
349 | 1; |
350 | |
351 | __END__ |
352 | |
68f800bd |
353 | =head1 SCHEDULING USING A YAML FILE |
354 | |
355 | As an alternative to using the schedule() method, you may define scheduled |
356 | events in an external YAML file. By default, the plugin looks for the |
357 | existence of a file called C<schedule.yml> in your application's home |
358 | directory. You can change the filename using the configuration option |
359 | L</yaml_file>. |
360 | |
ba2735b6 |
361 | Modifications to this file will be re-read during the normal event checking |
362 | process, which occurs once per minute (or whatever you set C<check_every> |
363 | to in your configuration). |
68f800bd |
364 | |
365 | Here's an example YAML configuration file with 4 events. Each event is |
366 | denoted with a '-' character, followed by the same parameters used by the |
367 | C<schedule> method. Note that coderef events are not supported by the YAML |
368 | file. |
369 | |
370 | --- |
371 | - at: '* * * * *' |
372 | event: /cron/delete_sessions |
373 | - event: /cron/send_email |
374 | trigger: send_email |
375 | - at: '@hourly' |
376 | event: /cron/hourly |
377 | - at: 0 0 * * * |
378 | auto_run: 0 |
379 | event: /cron/rebuild_search_index |
74e31b02 |
380 | |
381 | =head1 SECURITY |
382 | |
383 | All events are run inside of an eval container. This protects the user from |
384 | receiving any error messages or page crashes if an event fails to run |
385 | properly. All event errors are logged, even if logging is disabled. |
386 | |
74e31b02 |
387 | =head1 PLUGIN SUPPORT |
388 | |
389 | Other plugins may register scheduled events if they need to perform periodic |
390 | maintenance. Plugin authors, B<be sure to inform your users> if you do this! |
391 | Events should be registered from a plugin's C<setup> method. |
392 | |
393 | sub setup { |
394 | my $c = shift; |
395 | $c->NEXT::setup(@_); |
396 | |
397 | if ( $c->can('schedule') ) { |
398 | $c->schedule( |
399 | at => '0 * * * *', |
400 | event => \&cleanup, |
401 | ); |
402 | } |
403 | } |
ba2735b6 |
404 | |
405 | |
406 | |
f9d8e3cf |
407 | =head1 CAVEATS |
408 | |
409 | The time at which an event will run is determined completely by the requests |
410 | made to the application. Apps with heavy traffic may have events run at very |
411 | close to the correct time, whereas apps with low levels of traffic may see |
412 | events running much later than scheduled. If this is a problem, you can use |
413 | a real cron entry that simply hits your application at the desired time. |
414 | |
415 | 0 * * * * wget -q http://www.myapp.com/ |
416 | |
417 | Events which consume a lot of time will slow the request processing for the |
418 | user who triggers the event. For these types of events, you should use |
419 | auto_run => 0 or manual event triggering. |
420 | |
421 | =head1 PERFORMANCE |
422 | |
ba2735b6 |
423 | The plugin only checks once per minute (or whatever you set C<check_every> |
424 | to in your configuration) if any events need to be run, so the overhead on |
425 | each request is minimal. On my test server, the difference between running |
426 | with Scheduler and without was only around 0.02% (0.004 seconds). |
f9d8e3cf |
427 | |
68f800bd |
428 | Of course, when a scheduled event runs, performance will depend on what's |
429 | being run in the event. |
74e31b02 |
430 | |
431 | =head1 SEE ALSO |
432 | |
ba2735b6 |
433 | L<Catalyst::Plugin::Scheduler::Base>, L<Catalyst::Plugin::Scheduler::Event>, L<crontab(5)> |
74e31b02 |
434 | |
4a4fbd48 |
435 | =head1 AUTHORS |
74e31b02 |
436 | |
437 | Andy Grundman, <andy@hybridized.org> |
4a4fbd48 |
438 | Jos I. Boumans, <kane@cpan.org> |
74e31b02 |
439 | |
440 | =head1 COPYRIGHT |
441 | |
442 | This program is free software, you can redistribute it and/or modify it |
443 | under the same terms as Perl itself. |
444 | |
445 | =cut |
ba2735b6 |
446 | |
447 | Changes: |
448 | * split out C::P::Scheduler to ::Base and ::Event |
449 | * Implement all core functionality in ::Base |
450 | * C::P::Scheduler provides convenience functions to ::Base |
451 | and the hooks into catalyst to do the scheduling |
452 | * Pollute $c less |
453 | * Introduce event objects |
454 | * No longer hash based |
455 | * ->next_run and ->last_run are now accessors |
456 | * running events goes via $event->run, called from the dispatch hook |
457 | * Use $self->_config to retrieve config, rather than accessing $c directly |
458 | * Add tests for schedule_state(); |
459 | * Add '@always' as a cron shorcut |
460 | * made _event_state class data rather than instance data, so it is |
461 | accessible from the ::Event objects |
462 | * made 'once every 60 seconds' check configurable using 'check_every' |
463 | XXX add to docs |
464 | * made tests no longer need to hack the state file, but provide |
465 | $BASE->_last_check_time( 0 ) to reset the last checked time |
466 | * Moved _event_state toe ::Event from ::Base, as it's the _event_ state |
467 | * All tested & documented |
468 | |
469 | TODO: |
470 | * fix t/09long.t to use time::warp or somesuch |
471 | |
472 | |