Commit | Line | Data |
74e31b02 |
1 | package Catalyst::Plugin::Scheduler; |
2 | |
3 | use strict; |
4 | use warnings; |
f9d8e3cf |
5 | use base qw/Class::Accessor::Fast Class::Data::Inheritable/; |
cbf1ecfe |
6 | use DateTime; |
7 | use DateTime::Event::Cron; |
8 | use DateTime::TimeZone; |
68f800bd |
9 | use File::stat; |
74e31b02 |
10 | use NEXT; |
d2c7c91a |
11 | use Set::Scalar; |
f9d8e3cf |
12 | use Storable qw/lock_store lock_retrieve/; |
13 | use YAML; |
74e31b02 |
14 | |
13b998ad |
15 | our $VERSION = '0.07'; |
74e31b02 |
16 | |
cbf1ecfe |
17 | __PACKAGE__->mk_classdata( '_events' => [] ); |
68f800bd |
18 | __PACKAGE__->mk_accessors('_event_state'); |
cbf1ecfe |
19 | |
20 | sub schedule { |
21 | my ( $class, %args ) = @_; |
68f800bd |
22 | |
f9d8e3cf |
23 | unless ( $args{event} ) { |
68f800bd |
24 | Catalyst::Exception->throw( |
25 | message => 'The schedule method requires an event parameter' ); |
f9d8e3cf |
26 | } |
68f800bd |
27 | |
f9d8e3cf |
28 | my $conf = $class->config->{scheduler}; |
4796c217 |
29 | |
cbf1ecfe |
30 | my $event = { |
cbf1ecfe |
31 | trigger => $args{trigger}, |
f9d8e3cf |
32 | event => $args{event}, |
cbf1ecfe |
33 | auto_run => ( defined $args{auto_run} ) ? $args{auto_run} : 1, |
34 | }; |
68f800bd |
35 | |
cbf1ecfe |
36 | if ( $args{at} ) { |
68f800bd |
37 | |
cbf1ecfe |
38 | # replace keywords that Set::Crontab doesn't support |
39 | $args{at} = _prepare_cron( $args{at} ); |
4796c217 |
40 | |
cbf1ecfe |
41 | # parse the cron entry into a DateTime::Set |
42 | my $set; |
43 | eval { $set = DateTime::Event::Cron->from_cron( $args{at} ) }; |
68f800bd |
44 | if ($@) { |
45 | Catalyst::Exception->throw( |
46 | "Scheduler: Unable to parse 'at' value " |
47 | . $args{at} . ': ' |
48 | . $@ ); |
cbf1ecfe |
49 | } |
50 | else { |
ea66b1c7 |
51 | $event->{at} = $args{at}; |
cbf1ecfe |
52 | $event->{set} = $set; |
53 | } |
54 | } |
68f800bd |
55 | |
cbf1ecfe |
56 | push @{ $class->_events }, $event; |
57 | } |
58 | |
59 | sub dispatch { |
60 | my $c = shift; |
68f800bd |
61 | |
cbf1ecfe |
62 | $c->NEXT::dispatch(@_); |
68f800bd |
63 | |
f9d8e3cf |
64 | $c->_get_event_state(); |
68f800bd |
65 | |
66 | $c->_check_yaml(); |
67 | |
cbf1ecfe |
68 | # check if a minute has passed since our last check |
f9d8e3cf |
69 | # This check is not run if the user is manually triggering an event |
70 | if ( time - $c->_event_state->{last_check} < 60 ) { |
71 | return unless $c->req->params->{schedule_trigger}; |
cbf1ecfe |
72 | } |
f9d8e3cf |
73 | my $last_check = $c->_event_state->{last_check}; |
74 | $c->_event_state->{last_check} = time; |
75 | $c->_save_event_state(); |
68f800bd |
76 | |
77 | my $conf = $c->config->{scheduler}; |
f9d8e3cf |
78 | my $last_check_dt = DateTime->from_epoch( |
79 | epoch => $last_check, |
cbf1ecfe |
80 | time_zone => $conf->{time_zone} |
81 | ); |
82 | my $now = DateTime->now( time_zone => $conf->{time_zone} ); |
68f800bd |
83 | |
48390e8e |
84 | EVENT: |
cbf1ecfe |
85 | for my $event ( @{ $c->_events } ) { |
f9d8e3cf |
86 | my $next_run; |
68f800bd |
87 | |
ecf6a062 |
88 | if ( $event->{trigger} && $c->req->params->{schedule_trigger} |
68f800bd |
89 | && $event->{trigger} eq $c->req->params->{schedule_trigger} ) |
90 | { |
91 | |
f9d8e3cf |
92 | # manual trigger, run it now |
93 | next EVENT unless $c->_event_authorized; |
94 | $next_run = $now; |
95 | } |
96 | else { |
97 | next EVENT unless $event->{set}; |
68f800bd |
98 | $next_run = $event->{set}->next($last_check_dt); |
f9d8e3cf |
99 | } |
68f800bd |
100 | |
cbf1ecfe |
101 | if ( $next_run <= $now ) { |
68f800bd |
102 | |
cbf1ecfe |
103 | # do some security checking for non-auto-run events |
104 | if ( !$event->{auto_run} ) { |
105 | next EVENT unless $c->_event_authorized; |
106 | } |
68f800bd |
107 | |
f9d8e3cf |
108 | # make sure we're the only process running this event |
68f800bd |
109 | next EVENT unless $c->_mark_running($event); |
110 | |
cbf1ecfe |
111 | my $event_name = $event->{trigger} || $event->{event}; |
68f800bd |
112 | $c->log->debug("Scheduler: Executing $event_name") |
f9d8e3cf |
113 | if $c->config->{scheduler}->{logging}; |
68f800bd |
114 | |
cbf1ecfe |
115 | # trap errors |
116 | local $c->{error} = []; |
695ab602 |
117 | |
118 | # return value/output from the event, if any |
119 | my $output; |
68f800bd |
120 | |
cbf1ecfe |
121 | # run event |
122 | eval { |
68f800bd |
123 | |
cbf1ecfe |
124 | # do not allow the event to modify the response |
125 | local $c->res->{body}; |
126 | local $c->res->{cookies}; |
127 | local $c->res->{headers}; |
128 | local $c->res->{location}; |
129 | local $c->res->{status}; |
68f800bd |
130 | |
cbf1ecfe |
131 | if ( ref $event->{event} eq 'CODE' ) { |
695ab602 |
132 | $output = $event->{event}->($c); |
cbf1ecfe |
133 | } |
134 | else { |
695ab602 |
135 | $output = $c->forward( $event->{event} ); |
cbf1ecfe |
136 | } |
137 | }; |
138 | my @errors = @{ $c->{error} }; |
139 | push @errors, $@ if $@; |
68f800bd |
140 | if (@errors) { |
141 | $c->log->error( |
142 | 'Scheduler: Error executing ' . "$event_name: $_" ) |
143 | for @errors; |
695ab602 |
144 | $output = join '; ', @errors; |
cbf1ecfe |
145 | } |
68f800bd |
146 | |
695ab602 |
147 | $c->_mark_finished( $event, $output ); |
cbf1ecfe |
148 | } |
149 | } |
150 | } |
151 | |
152 | sub setup { |
153 | my $c = shift; |
68f800bd |
154 | |
cbf1ecfe |
155 | # initial configuration |
48390e8e |
156 | $c->config->{scheduler}->{logging} ||= ( $c->debug ) ? 1 : 0; |
68f800bd |
157 | $c->config->{scheduler}->{time_zone} ||= $c->_detect_timezone(); |
cbf1ecfe |
158 | $c->config->{scheduler}->{state_file} ||= $c->path_to('scheduler.state'); |
159 | $c->config->{scheduler}->{hosts_allow} ||= '127.0.0.1'; |
8c698cac |
160 | $c->config->{scheduler}->{yaml_file} ||= $c->path_to('scheduler.yml'); |
68f800bd |
161 | |
f9d8e3cf |
162 | $c->NEXT::setup(@_); |
cbf1ecfe |
163 | } |
164 | |
ea66b1c7 |
165 | sub dump_these { |
166 | my $c = shift; |
167 | |
168 | return ( $c->NEXT::dump_these(@_) ) unless @{ $c->_events }; |
169 | |
170 | # for debugging, we dump out a list of all events with their next |
171 | # scheduled run time |
695ab602 |
172 | return ( |
173 | $c->NEXT::dump_these(@_), |
174 | [ 'Scheduled Events', $c->scheduler_state ], |
175 | ); |
176 | } |
177 | |
178 | sub scheduler_state { |
179 | my $c = shift; |
180 | |
181 | $c->_get_event_state(); |
ea66b1c7 |
182 | |
183 | my $conf = $c->config->{scheduler}; |
184 | my $now = DateTime->now( time_zone => $conf->{time_zone} ); |
185 | |
186 | my $last_check = $c->_event_state->{last_check}; |
187 | my $last_check_dt = DateTime->from_epoch( |
188 | epoch => $last_check, |
695ab602 |
189 | time_zone => $conf->{time_zone}, |
ea66b1c7 |
190 | ); |
191 | |
192 | my $event_dump = []; |
193 | for my $event ( @{ $c->_events } ) { |
194 | my $dump = {}; |
195 | for my $key ( qw/at trigger event auto_run/ ) { |
196 | $dump->{$key} = $event->{$key} if $event->{$key}; |
197 | } |
198 | |
695ab602 |
199 | # display the next run time |
ea66b1c7 |
200 | if ( $event->{set} ) { |
201 | my $next_run = $event->{set}->next($last_check_dt); |
202 | $dump->{next_run} |
203 | = $next_run->ymd |
204 | . q{ } . $next_run->hms |
205 | . q{ } . $next_run->time_zone_short_name; |
206 | } |
207 | |
695ab602 |
208 | # display the last run time |
209 | my $last_run |
210 | = $c->_event_state->{events}->{ $event->{event} }->{last_run}; |
211 | if ( $last_run ) { |
212 | $last_run = DateTime->from_epoch( |
ecf6a062 |
213 | epoch => $last_run, |
695ab602 |
214 | time_zone => $conf->{time_zone}, |
215 | ); |
216 | $dump->{last_run} |
217 | = $last_run->ymd |
218 | . q{ } . $last_run->hms |
219 | . q{ } . $last_run->time_zone_short_name; |
220 | } |
221 | |
222 | # display the result of the last run |
223 | my $output |
224 | = $c->_event_state->{events}->{ $event->{event} }->{last_output}; |
225 | if ( $output ) { |
226 | $dump->{last_output} = $output; |
227 | } |
228 | |
ea66b1c7 |
229 | push @{$event_dump}, $dump; |
230 | } |
231 | |
695ab602 |
232 | return $event_dump; |
ea66b1c7 |
233 | } |
234 | |
68f800bd |
235 | # check and reload the YAML file with schedule data |
236 | sub _check_yaml { |
237 | my ($c) = @_; |
238 | |
239 | # each process needs to load the YAML file independently |
240 | if ( $c->_event_state->{yaml_mtime}->{$$} ||= 0 ) { |
241 | return if ( time - $c->_event_state->{last_check} < 60 ); |
242 | } |
243 | |
8c698cac |
244 | return unless -e $c->config->{scheduler}->{yaml_file}; |
68f800bd |
245 | |
246 | eval { |
8c698cac |
247 | my $mtime = ( stat $c->config->{scheduler}->{yaml_file} )->mtime; |
68f800bd |
248 | if ( $mtime > $c->_event_state->{yaml_mtime}->{$$} ) { |
249 | $c->_event_state->{yaml_mtime}->{$$} = $mtime; |
68f800bd |
250 | |
ecf6a062 |
251 | # clean up old PIDs listed in yaml_mtime |
695ab602 |
252 | foreach my $pid ( keys %{ $c->_event_state->{yaml_mtime} } ) { |
253 | if ( $c->_event_state->{yaml_mtime}->{$pid} < $mtime ) { |
254 | delete $c->_event_state->{yaml_mtime}->{$pid}; |
255 | } |
256 | } |
257 | $c->_save_event_state(); |
258 | |
68f800bd |
259 | # wipe out all current events and reload from YAML |
260 | $c->_events( [] ); |
261 | |
13b998ad |
262 | my $file = $c->config->{scheduler}->{yaml_file}; |
263 | my $yaml = YAML::LoadFile( "$file" ); |
4796c217 |
264 | |
68f800bd |
265 | foreach my $event ( @{$yaml} ) { |
266 | $c->schedule( %{$event} ); |
267 | } |
268 | |
269 | $c->log->info( "Scheduler: PID $$ loaded " |
270 | . scalar @{$yaml} |
271 | . ' events from YAML file' ) |
272 | if $c->config->{scheduler}->{logging}; |
273 | } |
274 | }; |
275 | if ($@) { |
695ab602 |
276 | $c->log->error("Scheduler: Error reading YAML file: $@"); |
68f800bd |
277 | } |
278 | } |
279 | |
cbf1ecfe |
280 | # Detect the current time zone |
281 | sub _detect_timezone { |
282 | my $c = shift; |
68f800bd |
283 | |
cbf1ecfe |
284 | my $tz; |
285 | eval { $tz = DateTime::TimeZone->new( name => 'local' ) }; |
286 | if ($@) { |
68f800bd |
287 | $c->log->warn( |
07305803 |
288 | 'Scheduler: Unable to autodetect local time zone, using UTC') |
289 | if $c->config->{scheduler}->{logging}; |
cbf1ecfe |
290 | return 'UTC'; |
291 | } |
292 | else { |
f9d8e3cf |
293 | $c->log->debug( |
68f800bd |
294 | 'Scheduler: Using autodetected time zone: ' . $tz->name ) |
295 | if $c->config->{scheduler}->{logging}; |
cbf1ecfe |
296 | return $tz->name; |
297 | } |
298 | } |
299 | |
300 | # Check for authorized users on non-auto events |
cbf1ecfe |
301 | sub _event_authorized { |
302 | my $c = shift; |
68f800bd |
303 | |
f9d8e3cf |
304 | # this should never happen, but just in case... |
68f800bd |
305 | return unless $c->req->address; |
306 | |
f9d8e3cf |
307 | my $hosts_allow = $c->config->{scheduler}->{hosts_allow}; |
68f800bd |
308 | $hosts_allow = [$hosts_allow] unless ref($hosts_allow) eq 'ARRAY'; |
d2c7c91a |
309 | my $allowed = Set::Scalar->new( @{$hosts_allow} ); |
310 | return $allowed->contains( $c->req->address ); |
f9d8e3cf |
311 | } |
312 | |
313 | # get the state from the state file |
314 | sub _get_event_state { |
315 | my $c = shift; |
68f800bd |
316 | |
f9d8e3cf |
317 | if ( -e $c->config->{scheduler}->{state_file} ) { |
68f800bd |
318 | $c->_event_state( |
319 | lock_retrieve $c->config->{scheduler}->{state_file} ); |
f9d8e3cf |
320 | } |
321 | else { |
68f800bd |
322 | |
f9d8e3cf |
323 | # initialize the state file |
68f800bd |
324 | $c->_event_state( |
695ab602 |
325 | { last_check => time, |
326 | events => {}, |
327 | yaml_mtime => {}, |
68f800bd |
328 | } |
329 | ); |
f9d8e3cf |
330 | $c->_save_event_state(); |
331 | } |
332 | } |
333 | |
334 | # Check the state file to ensure we are the only process running an event |
335 | sub _mark_running { |
336 | my ( $c, $event ) = @_; |
68f800bd |
337 | |
f9d8e3cf |
338 | $c->_get_event_state(); |
68f800bd |
339 | |
695ab602 |
340 | return if |
341 | $c->_event_state->{events}->{ $event->{event} }->{running}; |
68f800bd |
342 | |
f9d8e3cf |
343 | # this is a 2-step process to prevent race conditions |
344 | # 1. write the state file with our PID |
695ab602 |
345 | $c->_event_state->{events}->{ $event->{event} }->{running} = $$; |
f9d8e3cf |
346 | $c->_save_event_state(); |
68f800bd |
347 | |
f9d8e3cf |
348 | # 2. re-read the state file and make sure it's got the same PID |
349 | $c->_get_event_state(); |
695ab602 |
350 | if ( $c->_event_state->{events}->{ $event->{event} }->{running} == $$ ) { |
f9d8e3cf |
351 | return 1; |
352 | } |
68f800bd |
353 | |
f9d8e3cf |
354 | return; |
355 | } |
356 | |
357 | # Mark an event as finished |
358 | sub _mark_finished { |
695ab602 |
359 | my ( $c, $event, $output ) = @_; |
68f800bd |
360 | |
695ab602 |
361 | $c->_event_state->{events}->{ $event->{event} }->{running} = 0; |
362 | $c->_event_state->{events}->{ $event->{event} }->{last_run} = time; |
363 | $c->_event_state->{events}->{ $event->{event} }->{last_output} = $output; |
f9d8e3cf |
364 | $c->_save_event_state(); |
cbf1ecfe |
365 | } |
366 | |
f9d8e3cf |
367 | # update the state file on disk |
368 | sub _save_event_state { |
369 | my $c = shift; |
68f800bd |
370 | |
f9d8e3cf |
371 | lock_store $c->_event_state, $c->config->{scheduler}->{state_file}; |
cbf1ecfe |
372 | } |
373 | |
374 | # Set::Crontab does not support day names, or '@' shortcuts |
375 | sub _prepare_cron { |
376 | my $cron = shift; |
68f800bd |
377 | |
cbf1ecfe |
378 | return $cron unless $cron =~ /\w/; |
68f800bd |
379 | |
cbf1ecfe |
380 | my %replace = ( |
381 | jan => 1, |
382 | feb => 2, |
383 | mar => 3, |
384 | apr => 4, |
385 | may => 5, |
386 | jun => 6, |
387 | jul => 7, |
388 | aug => 8, |
389 | sep => 9, |
390 | 'oct' => 10, |
391 | nov => 11, |
392 | dec => 12, |
68f800bd |
393 | |
cbf1ecfe |
394 | sun => 0, |
395 | mon => 1, |
396 | tue => 2, |
397 | wed => 3, |
398 | thu => 4, |
399 | fri => 5, |
400 | sat => 6, |
4796c217 |
401 | ); |
402 | |
403 | my %replace_at = ( |
cbf1ecfe |
404 | 'yearly' => '0 0 1 1 *', |
405 | 'annually' => '0 0 1 1 *', |
406 | 'monthly' => '0 0 1 * *', |
407 | 'weekly' => '0 0 * * 0', |
408 | 'daily' => '0 0 * * *', |
409 | 'midnight' => '0 0 * * *', |
410 | 'hourly' => '0 * * * *', |
411 | ); |
4796c217 |
412 | |
413 | if ( $cron =~ /^\@/ ) { |
414 | $cron =~ s/^\@//; |
415 | return $replace_at{ $cron }; |
416 | } |
68f800bd |
417 | |
cbf1ecfe |
418 | for my $name ( keys %replace ) { |
419 | my $value = $replace{$name}; |
4796c217 |
420 | $cron =~ s/$name/$value/i; |
421 | last unless $cron =~ /\w/; |
cbf1ecfe |
422 | } |
cbf1ecfe |
423 | return $cron; |
424 | } |
425 | |
74e31b02 |
426 | 1; |
427 | __END__ |
428 | |
429 | =pod |
430 | |
431 | =head1 NAME |
432 | |
433 | Catalyst::Plugin::Scheduler - Schedule events to run in a cron-like fashion |
434 | |
435 | =head1 SYNOPSIS |
436 | |
437 | use Catalyst qw/Scheduler/; |
438 | |
439 | # run remove_sessions in the Cron controller every hour |
440 | __PACKAGE__->schedule( |
441 | at => '0 * * * *', |
442 | event => '/cron/remove_sessions' |
443 | ); |
444 | |
445 | # Run a subroutine at 4:05am every Sunday |
446 | __PACKAGE__->schedule( |
447 | at => '5 4 * * sun', |
448 | event => \&do_stuff, |
449 | ); |
450 | |
68f800bd |
451 | # A long-running scheduled event that must be triggered |
452 | # manually by an authorized user |
453 | __PACKAGE__->schedule( |
454 | trigger => 'rebuild_search_index', |
455 | event => '/cron/rebuild_search_index', |
456 | ); |
457 | $ wget -q http://www.myapp.com/?schedule_trigger=rebuild_search_index |
f9d8e3cf |
458 | |
74e31b02 |
459 | =head1 DESCRIPTION |
460 | |
461 | This plugin allows you to schedule events to run at recurring intervals. |
462 | Events will run during the first request which meets or exceeds the specified |
463 | time. Depending on the level of traffic to the application, events may or may |
464 | not run at exactly the correct time, but it should be enough to satisfy many |
465 | basic scheduling needs. |
466 | |
467 | =head1 CONFIGURATION |
468 | |
469 | Configuration is optional and is specified in MyApp->config->{scheduler}. |
470 | |
471 | =head2 logging |
472 | |
473 | Set to 1 to enable logging of events as they are executed. This option is |
474 | enabled by default when running under -Debug mode. Errors are always logged |
475 | regardless of the value of this option. |
476 | |
cbf1ecfe |
477 | =head2 time_zone |
478 | |
479 | The time zone of your system. This will be autodetected where possible, or |
480 | will default to UTC (GMT). You can override the detection by providing a |
481 | valid L<DateTime> time zone string, such as 'America/New_York'. |
482 | |
74e31b02 |
483 | =head2 state_file |
484 | |
485 | The current state of every event is stored in a file. By default this is |
f9d8e3cf |
486 | $APP_HOME/scheduler.state. This file is created on the first request if it |
487 | does not already exist. |
74e31b02 |
488 | |
68f800bd |
489 | =head2 yaml_file |
490 | |
491 | The location of the optional YAML event configuration file. By default this |
492 | is $APP_HOME/scheduler.yml. |
493 | |
74e31b02 |
494 | =head2 hosts_allow |
495 | |
496 | This option specifies IP addresses for trusted users. This option defaults |
497 | to 127.0.0.1. Multiple addresses can be specified by using an array |
498 | reference. This option is used for both events where auto_run is set to 0 |
499 | and for manually-triggered events. |
500 | |
501 | __PACKAGE__->config->{scheduler}->{hosts_allow} = '192.168.1.1'; |
502 | __PACKAGE__->config->{scheduler}->{hosts_allow} = [ |
503 | '127.0.0.1', |
504 | '192.168.1.1' |
505 | ]; |
506 | |
507 | =head1 SCHEDULING |
508 | |
509 | =head2 AUTOMATED EVENTS |
510 | |
511 | Events are scheduled by calling the class method C<schedule>. |
512 | |
513 | MyApp->schedule( |
514 | at => '0 * * * *', |
515 | event => '/cron/remove_sessions', |
516 | ); |
517 | |
518 | package MyApp::Controller::Cron; |
519 | |
520 | sub remove_sessions : Private { |
521 | my ( $self, $c ) = @_; |
522 | |
523 | $c->delete_expired_sessions; |
524 | } |
525 | |
526 | =head3 at |
527 | |
528 | The time to run an event is specified using L<crontab(5)>-style syntax. |
529 | |
530 | 5 0 * * * # 5 minutes after midnight, every day |
531 | 15 14 1 * * # run at 2:15pm on the first of every month |
532 | 0 22 * * 1-5 # run at 10 pm on weekdays |
533 | 5 4 * * sun # run at 4:05am every Sunday |
534 | |
535 | From crontab(5): |
536 | |
537 | field allowed values |
538 | ----- -------------- |
539 | minute 0-59 |
540 | hour 0-23 |
541 | day of month 1-31 |
542 | month 0-12 (or names, see below) |
543 | day of week 0-7 (0 or 7 is Sun, or use names) |
544 | |
545 | Instead of the first five fields, one of seven special strings may appear: |
546 | |
547 | string meaning |
548 | ------ ------- |
549 | @yearly Run once a year, "0 0 1 1 *". |
550 | @annually (same as @yearly) |
551 | @monthly Run once a month, "0 0 1 * *". |
552 | @weekly Run once a week, "0 0 * * 0". |
553 | @daily Run once a day, "0 0 * * *". |
554 | @midnight (same as @daily) |
555 | @hourly Run once an hour, "0 * * * *". |
556 | |
557 | =head3 event |
558 | |
559 | The event to run at the specified time can be either a Catalyst private |
560 | action path or a coderef. Both types of event methods will receive the $c |
561 | object from the current request, but you must not rely on any request-specific |
562 | information present in $c as it will be from a random user request at or near |
563 | the event's specified run time. |
564 | |
565 | Important: Methods used for events should be marked C<Private> so that |
566 | they can not be executed via the browser. |
567 | |
568 | =head3 auto_run |
569 | |
570 | The auto_run parameter specifies when the event is allowed to be executed. |
571 | By default this option is set to 1, so the event will be executed during the |
572 | first request that matches the specified time in C<at>. |
573 | |
574 | If set to 0, the event will only run when a request is made by a user from |
575 | an authorized address. The purpose of this option is to allow long-running |
576 | tasks to execute only for certain users. |
577 | |
578 | MyApp->schedule( |
579 | at => '0 0 * * *', |
580 | event => '/cron/rebuild_search_index', |
581 | auto_run => 0, |
582 | ); |
583 | |
584 | package MyApp::Controller::Cron; |
585 | |
586 | sub rebuild_search_index : Private { |
587 | my ( $self, $c ) = @_; |
588 | |
589 | # rebuild the search index, this may take a long time |
590 | } |
591 | |
592 | Now, the search index will only be rebuilt when a request is made from a user |
593 | whose IP address matches the list in the C<hosts_allow> config option. To |
594 | run this event, you probably want to ping the app from a cron job. |
595 | |
f9d8e3cf |
596 | 0 0 * * * wget -q http://www.myapp.com/ |
74e31b02 |
597 | |
598 | =head2 MANUAL EVENTS |
599 | |
600 | To create an event that does not run on a set schedule and must be manually |
601 | triggered, you can specify the C<trigger> option instead of C<at>. |
602 | |
603 | __PACKAGE__->schedule( |
604 | trigger => 'send_email', |
605 | event => '/events/send_email', |
606 | ); |
607 | |
608 | The event may then be triggered by a standard web request from an authorized |
609 | user. The trigger to run is specified by using a special GET parameter, |
610 | 'schedule_trigger'; the path requested does not matter. |
611 | |
612 | http://www.myapp.com/?schedule_trigger=send_email |
613 | |
614 | By default, manual events may only be triggered by requests made from |
615 | localhost (127.0.0.1). To allow other addresses to run events, use the |
68f800bd |
616 | configuration option L</hosts_allow>. |
617 | |
618 | =head1 SCHEDULING USING A YAML FILE |
619 | |
620 | As an alternative to using the schedule() method, you may define scheduled |
621 | events in an external YAML file. By default, the plugin looks for the |
622 | existence of a file called C<schedule.yml> in your application's home |
623 | directory. You can change the filename using the configuration option |
624 | L</yaml_file>. |
625 | |
626 | Modifications to this file will be re-read once per minute during the normal |
627 | event checking process. |
628 | |
629 | Here's an example YAML configuration file with 4 events. Each event is |
630 | denoted with a '-' character, followed by the same parameters used by the |
631 | C<schedule> method. Note that coderef events are not supported by the YAML |
632 | file. |
633 | |
634 | --- |
635 | - at: '* * * * *' |
636 | event: /cron/delete_sessions |
637 | - event: /cron/send_email |
638 | trigger: send_email |
639 | - at: '@hourly' |
640 | event: /cron/hourly |
641 | - at: 0 0 * * * |
642 | auto_run: 0 |
643 | event: /cron/rebuild_search_index |
74e31b02 |
644 | |
645 | =head1 SECURITY |
646 | |
647 | All events are run inside of an eval container. This protects the user from |
648 | receiving any error messages or page crashes if an event fails to run |
649 | properly. All event errors are logged, even if logging is disabled. |
650 | |
74e31b02 |
651 | =head1 PLUGIN SUPPORT |
652 | |
653 | Other plugins may register scheduled events if they need to perform periodic |
654 | maintenance. Plugin authors, B<be sure to inform your users> if you do this! |
655 | Events should be registered from a plugin's C<setup> method. |
656 | |
657 | sub setup { |
658 | my $c = shift; |
659 | $c->NEXT::setup(@_); |
660 | |
661 | if ( $c->can('schedule') ) { |
662 | $c->schedule( |
663 | at => '0 * * * *', |
664 | event => \&cleanup, |
665 | ); |
666 | } |
667 | } |
f9d8e3cf |
668 | |
669 | =head1 CAVEATS |
670 | |
671 | The time at which an event will run is determined completely by the requests |
672 | made to the application. Apps with heavy traffic may have events run at very |
673 | close to the correct time, whereas apps with low levels of traffic may see |
674 | events running much later than scheduled. If this is a problem, you can use |
675 | a real cron entry that simply hits your application at the desired time. |
676 | |
677 | 0 * * * * wget -q http://www.myapp.com/ |
678 | |
679 | Events which consume a lot of time will slow the request processing for the |
680 | user who triggers the event. For these types of events, you should use |
681 | auto_run => 0 or manual event triggering. |
682 | |
683 | =head1 PERFORMANCE |
684 | |
685 | The plugin only checks once per minute if any events need to be run, so the |
686 | overhead on each request is minimal. On my test server, the difference |
687 | between running with Scheduler and without was only around 0.02% (0.004 |
688 | seconds). |
689 | |
68f800bd |
690 | Of course, when a scheduled event runs, performance will depend on what's |
691 | being run in the event. |
07305803 |
692 | |
693 | =head1 METHODS |
694 | |
695 | =head2 schedule |
696 | |
697 | Schedule is a class method for adding scheduled events. See the |
8c698cac |
698 | L<"/SCHEDULING"> section for more information. |
07305803 |
699 | |
695ab602 |
700 | =head2 scheduler_state |
701 | |
702 | The current state of all scheduled events is available in an easy-to-use |
703 | format by calling $c->scheduler_state. You can use this data to build an |
704 | admin view into the scheduling engine, for example. This same data is also |
705 | displayed on the Catalyst debug screen. |
706 | |
707 | This method returns an array reference containing a hash reference for each |
708 | event. |
709 | |
710 | [ |
711 | { |
712 | 'last_run' => '2005-12-29 16:29:33 EST', |
713 | 'auto_run' => 1, |
714 | 'last_output' => 1, |
715 | 'at' => '0 0 * * *', |
716 | 'next_run' => '2005-12-30 00:00:00 EST', |
717 | 'event' => '/cron/session_cleanup' |
718 | }, |
719 | { |
720 | 'auto_run' => 1, |
721 | 'at' => '0 0 * * *', |
722 | 'next_run' => '2005-12-30 00:00:00 EST', |
723 | 'event' => '/cron/build_rss' |
724 | }, |
725 | ] |
726 | |
07305803 |
727 | =head1 INTERNAL METHODS |
728 | |
729 | The following methods are extended by this plugin. |
730 | |
731 | =over 4 |
732 | |
733 | =item dispatch |
734 | |
735 | The main scheduling logic takes place during the dispatch phase. |
736 | |
ea66b1c7 |
737 | =item dump_these |
738 | |
739 | On the Catalyst debug screen, all scheduled events are displayed along with |
740 | the next time they will be executed. |
741 | |
07305803 |
742 | =item setup |
743 | |
744 | =back |
74e31b02 |
745 | |
746 | =head1 SEE ALSO |
747 | |
748 | L<crontab(5)> |
749 | |
750 | =head1 AUTHOR |
751 | |
752 | Andy Grundman, <andy@hybridized.org> |
753 | |
754 | =head1 COPYRIGHT |
755 | |
756 | This program is free software, you can redistribute it and/or modify it |
757 | under the same terms as Perl itself. |
758 | |
759 | =cut |