1 package Catalyst::Plugin::Scheduler;
5 use base 'Class::Data::Inheritable';
7 use DateTime::Event::Cron;
8 use DateTime::TimeZone;
11 our $VERSION = '0.01';
13 __PACKAGE__->mk_classdata( '_events' => [] );
14 __PACKAGE__->mk_classdata( '_last_event_check' );
17 my ( $class, %args ) = @_;
20 event => $args{event},
21 trigger => $args{trigger},
22 auto_run => ( defined $args{auto_run} ) ? $args{auto_run} : 1,
26 # replace keywords that Set::Crontab doesn't support
27 $args{at} = _prepare_cron( $args{at} );
29 # parse the cron entry into a DateTime::Set
31 eval { $set = DateTime::Event::Cron->from_cron( $args{at} ) };
33 Catalyst::Exception->throw(
34 "Scheduler: Unable to parse 'at' value "
35 . $args{at} . ': ' . $@
43 push @{ $class->_events }, $event;
49 $c->NEXT::dispatch(@_);
51 # check if a minute has passed since our last check
52 if ( time - $c->_last_event_check < 60 ) {
56 my $conf = $c->config->{scheduler};
58 # check for events to execute
59 my $last_check = DateTime->from_epoch(
60 epoch => $c->_last_event_check,
61 time_zone => $conf->{time_zone}
63 my $now = DateTime->now( time_zone => $conf->{time_zone} );
64 $c->_last_event_check( $now->epoch );
67 for my $event ( @{ $c->_events } ) {
68 next EVENT unless $event->{set};
70 my $next_run = $event->{set}->next( $last_check );
71 if ( $next_run <= $now ) {
73 # do some security checking for non-auto-run events
74 if ( !$event->{auto_run} ) {
75 next EVENT unless $c->_event_authorized;
78 # update the state file to make sure we're the only process
80 next EVENT unless $c->_mark_state( $event, $next_run );
82 my $event_name = $event->{trigger} || $event->{event};
83 $c->log->info( "Scheduler: Executing $event_name" )
87 local $c->{error} = [];
91 # do not allow the event to modify the response
92 local $c->res->{body};
93 local $c->res->{cookies};
94 local $c->res->{headers};
95 local $c->res->{location};
96 local $c->res->{status};
98 if ( ref $event->{event} eq 'CODE' ) {
99 $event->{event}->( $c );
102 $c->forward( $event->{event} );
105 my @errors = @{ $c->{error} };
106 push @errors, $@ if $@;
108 $c->log->error( 'Scheduler: Error executing '
109 . "$event_name: $_" ) for @errors;
120 # initial configuration
121 $c->config->{scheduler}->{logging} ||= ( $c->debug ) ? 1 : 0;
122 $c->config->{scheduler}->{time_zone} ||= $c->_detect_timezone();
123 $c->config->{scheduler}->{state_file} ||= $c->path_to('scheduler.state');
124 $c->config->{scheduler}->{hosts_allow} ||= '127.0.0.1';
126 $c->_last_event_check(
128 time_zone => $c->config->{scheduler}->{time_zone}
133 # Detect the current time zone
134 sub _detect_timezone {
138 eval { $tz = DateTime::TimeZone->new( name => 'local' ) };
141 'Scheduler: Unable to determine local time zone, using UTC' );
149 # Check for authorized users on non-auto events
151 sub _event_authorized {
156 # Update the state file
159 my ( $c, $event, $next_run ) = @_;
163 # Set::Crontab does not support day names, or '@' shortcuts
167 return $cron unless $cron =~ /\w/;
191 'yearly' => '0 0 1 1 *',
192 'annually' => '0 0 1 1 *',
193 'monthly' => '0 0 1 * *',
194 'weekly' => '0 0 * * 0',
195 'daily' => '0 0 * * *',
196 'midnight' => '0 0 * * *',
197 'hourly' => '0 * * * *',
200 for my $name ( keys %replace ) {
201 my $value = $replace{$name};
203 if ( $cron =~ /^\@$name/ ) {
208 $cron =~ s/$name/$value/i;
209 last unless $cron =~ /\w/;
223 Catalyst::Plugin::Scheduler - Schedule events to run in a cron-like fashion
227 use Catalyst qw/Scheduler/;
229 # run remove_sessions in the Cron controller every hour
230 __PACKAGE__->schedule(
232 event => '/cron/remove_sessions'
235 # Run a subroutine at 4:05am every Sunday
236 __PACKAGE__->schedule(
243 This plugin allows you to schedule events to run at recurring intervals.
244 Events will run during the first request which meets or exceeds the specified
245 time. Depending on the level of traffic to the application, events may or may
246 not run at exactly the correct time, but it should be enough to satisfy many
247 basic scheduling needs.
251 Configuration is optional and is specified in MyApp->config->{scheduler}.
255 Set to 1 to enable logging of events as they are executed. This option is
256 enabled by default when running under -Debug mode. Errors are always logged
257 regardless of the value of this option.
261 The time zone of your system. This will be autodetected where possible, or
262 will default to UTC (GMT). You can override the detection by providing a
263 valid L<DateTime> time zone string, such as 'America/New_York'.
267 The current state of every event is stored in a file. By default this is
268 $APP_HOME/scheduler.state. If this file cannot be read or created at
269 startup, your app will die.
273 This option specifies IP addresses for trusted users. This option defaults
274 to 127.0.0.1. Multiple addresses can be specified by using an array
275 reference. This option is used for both events where auto_run is set to 0
276 and for manually-triggered events.
278 __PACKAGE__->config->{scheduler}->{hosts_allow} = '192.168.1.1';
279 __PACKAGE__->config->{scheduler}->{hosts_allow} = [
286 =head2 AUTOMATED EVENTS
288 Events are scheduled by calling the class method C<schedule>.
292 event => '/cron/remove_sessions',
295 package MyApp::Controller::Cron;
297 sub remove_sessions : Private {
298 my ( $self, $c ) = @_;
300 $c->delete_expired_sessions;
305 The time to run an event is specified using L<crontab(5)>-style syntax.
307 5 0 * * * # 5 minutes after midnight, every day
308 15 14 1 * * # run at 2:15pm on the first of every month
309 0 22 * * 1-5 # run at 10 pm on weekdays
310 5 4 * * sun # run at 4:05am every Sunday
319 month 0-12 (or names, see below)
320 day of week 0-7 (0 or 7 is Sun, or use names)
322 Instead of the first five fields, one of seven special strings may appear:
326 @yearly Run once a year, "0 0 1 1 *".
327 @annually (same as @yearly)
328 @monthly Run once a month, "0 0 1 * *".
329 @weekly Run once a week, "0 0 * * 0".
330 @daily Run once a day, "0 0 * * *".
331 @midnight (same as @daily)
332 @hourly Run once an hour, "0 * * * *".
336 The event to run at the specified time can be either a Catalyst private
337 action path or a coderef. Both types of event methods will receive the $c
338 object from the current request, but you must not rely on any request-specific
339 information present in $c as it will be from a random user request at or near
340 the event's specified run time.
342 Important: Methods used for events should be marked C<Private> so that
343 they can not be executed via the browser.
347 The auto_run parameter specifies when the event is allowed to be executed.
348 By default this option is set to 1, so the event will be executed during the
349 first request that matches the specified time in C<at>.
351 If set to 0, the event will only run when a request is made by a user from
352 an authorized address. The purpose of this option is to allow long-running
353 tasks to execute only for certain users.
357 event => '/cron/rebuild_search_index',
361 package MyApp::Controller::Cron;
363 sub rebuild_search_index : Private {
364 my ( $self, $c ) = @_;
366 # rebuild the search index, this may take a long time
369 Now, the search index will only be rebuilt when a request is made from a user
370 whose IP address matches the list in the C<hosts_allow> config option. To
371 run this event, you probably want to ping the app from a cron job.
373 0 0 * * * wget -q http://www.myapp.com/
377 To create an event that does not run on a set schedule and must be manually
378 triggered, you can specify the C<trigger> option instead of C<at>.
380 __PACKAGE__->schedule(
381 trigger => 'send_email',
382 event => '/events/send_email',
385 The event may then be triggered by a standard web request from an authorized
386 user. The trigger to run is specified by using a special GET parameter,
387 'schedule_trigger'; the path requested does not matter.
389 http://www.myapp.com/?schedule_trigger=send_email
391 By default, manual events may only be triggered by requests made from
392 localhost (127.0.0.1). To allow other addresses to run events, use the
393 configuration option C<hosts_allow>.
397 All events are run inside of an eval container. This protects the user from
398 receiving any error messages or page crashes if an event fails to run
399 properly. All event errors are logged, even if logging is disabled.
403 The time at which an event will run is determined completely by the requests
404 made to the application. Apps with heavy traffic may have events run at very
405 close to the correct time, whereas apps with low levels of traffic may see
406 events running much later than scheduled. If this is a problem, you can use
407 a real cron entry that simply hits your application at the desired time.
409 0 * * * * wget -q http://www.myapp.com/
411 Events which consume a lot of time will slow the request processing for the
412 user who triggers the event. For these types of events, you should use
413 auto_run => 0 or manual event triggering.
415 =head1 PLUGIN SUPPORT
417 Other plugins may register scheduled events if they need to perform periodic
418 maintenance. Plugin authors, B<be sure to inform your users> if you do this!
419 Events should be registered from a plugin's C<setup> method.
425 if ( $c->can('schedule') ) {
435 Support storing all scheduled events in an external YAML file. This would
436 only allow private actions, not coderefs. It would also allow changes to
437 the schedule to take effect in realtime.
445 Andy Grundman, <andy@hybridized.org>
449 This program is free software, you can redistribute it and/or modify it
450 under the same terms as Perl itself.