1 package Catalyst::Plugin::Scheduler::Base;
5 use DateTime::Event::Cron;
6 use DateTime::TimeZone;
9 use base qw/Catalyst::Plugin::Scheduler/;
10 use Catalyst::Plugin::Scheduler::Event;
12 __PACKAGE__->mk_classdata(_events => []);
13 __PACKAGE__->mk_classdata(_event_class => 'Catalyst::Plugin::Scheduler::Event');
14 __PACKAGE__->mk_classdata('_app' );
18 Catalyst::Plugin::Scheduler::Base - Base class for the Catalyst Scheduler
22 MyApp->scheduler->schedule( at => '0 0 * * *', event => '/cron/ping' );
24 ### return all scheduled events as ::Event objects
25 @events = MyApp->scheduler->list_events;
27 ### return all pending scheduled events as ::Event objects
28 @pending = MyApp->scheduler->list_pending_events;
30 ### a dump of the current scheduler state
31 $aref = MyApp->scheduler->state;
35 =head2 $bool = MyApp->scheduler->schedule
37 Allows you to schedule events. For full usage and documentation, consult
38 the C<Catalyst::Plugin::Scheduler> documentation on method C<schedule>.
47 ### XXX more input checks?
49 unless ( $args{event} ) {
50 Catalyst::Exception->throw(
51 message => 'The schedule method requires an event parameter' );
55 $args{'auto_run'} = 1 unless defined $args{'auto_run'};
59 # replace keywords that Set::Crontab doesn't support
60 $args{at} = $self->_prepare_cron( $args{at} );
62 # parse the cron entry into a DateTime::Set
63 $args{set} = eval { DateTime::Event::Cron->from_cron( $args{at} ) };
65 Catalyst::Exception->throw(
66 "Scheduler: Unable to parse 'at' value $args{at}: $@"
71 my $who = $self->_caller_string;
72 push @{ $self->_events },
73 Catalyst::Plugin::Scheduler::Event->new( scheduled_by => $who, %args );
78 ### create a caller string like: "package (file.pm:#line)"
79 sub _caller_string { return sprintf "%s (%s:%s)", @{[caller(1)]}[0,1,2]; }
81 =head2 @events = $c->scheduler->list_events;
83 Returns an array of C<Catalyst::Plugin::Scheduler::Event> objects,
84 representing all the scheduled events in this application.
86 See the C<Catalyst::Plugin::Scheduler::Event> documentation on how to use
93 return @{ $self->_events || [] };
96 =head2 @events = $c->scheduler->list_events;
98 Returns an array of C<Catalyst::Plugin::Scheduler::Event> objects,
99 representing all the pending events in this application. They are the
100 events that are due according to your cron specification, and will be run
101 at the next dispatch, or can be run by you explicitly.
103 See the C<Catalyst::Plugin::Scheduler::Event> documentation on how to use
108 sub list_pending_events {
111 my $tz = $self->_config('time_zone');
113 ### there are no events scheduled?
114 my @events = $self->list_events or return;
115 my $now = DateTime->now( time_zone => $tz );
117 ### list of pending events
120 ### XXX need NEXT RUN TIME??
122 for my $event (@events) {
124 ### this event is not active, so skip it
125 next EVENT unless $event->active;
127 ### the proper trigger is being called
128 if( $event->trigger && $c->req->params->{schedule_trigger} &&
129 $event->trigger eq $c->req->params->{schedule_trigger}
132 ### if you're not authorized to call the trigger, skip it
133 next EVENT unless $self->_event_authorized;
135 push @pending, $event;
139 ### we're due according to our cron-entry...
141 ### is the next run time now, or even before now?
142 push @pending, $event if $event->next_run_as_dt <= $now;
146 ### sort them by priority
147 return sort { $a->priority <=> $b->priority } @pending;
157 $self->_check_yaml();
159 # check if a minute has passed since our last check
160 # This check is not run if the user is manually triggering an event
161 if ( time - $self->_last_check_time < $self->_config('check_every') ) {
162 return unless $c->req->params->{schedule_trigger};
165 my @events = $self->list_pending_events;
167 ### update the 'checked' time and save the state, so no more
168 ### processes are going to be running these events
169 ### the small race condition between the 'list_pending_events' call
170 ### and the updating of the check time is resolved by checking if a
171 ### job is running before executing it, so at worst, we have several
172 ### processes sharing the load of this cron run. --kane
173 $self->_last_check_time( time );
176 for my $event ( @events ) {
178 # do some security checking for non-auto-run events
179 ### XXX move this to $event->run? --kane
180 if ( !$event->auto_run ) {
181 next EVENT unless $self->_event_authorized;
188 =head2 $aref = MyApp->scheduler->state
190 A dump of the current state of the scheudler. For full usage and
191 documentation, consult the C<Catalyst::Plugin::Scheduler> documentation on
192 method C<scheduler+state>.
201 for my $event ( $self->list_events ) {
203 for my $key ( qw/at trigger event auto_run/ ) {
204 $dump->{$key} = $event->$key if $event->$key;
207 # display the next run time
208 $dump->{next_run} = $event->next_run_as_string;
210 # display the last run time
211 $dump->{last_run} = $event->last_run_as_string;
213 # display the result of the last run
214 my $output = $event->output;
216 $dump->{last_output} = $output;
219 push @{$event_dump}, $dump;
229 my $conf = $c->config->{scheduler};
230 my $rv = $key ? $conf->{$key} : $conf;
236 sub _last_check_time {
238 return $self->_event_class->_last_check_time( @_ );
241 # check and reload the YAML file with schedule data
246 $self->_event_class->_get_event_state();
248 # each process needs to load the YAML file independently
249 if ( $self->_event_class->_event_state->{yaml_mtime}->{$$} ||= 0 ) {
250 return if ( time - $self->_last_check_time < 60 );
253 my $file = $self->_config('yaml_file');
254 return unless -e $file;
257 my $mtime = ( stat $file )->mtime;
258 if ( $mtime > $self->_event_class->_event_state->{yaml_mtime}->{$$} ) {
259 $self->_event_class->_event_state->{yaml_mtime}->{$$} = $mtime;
261 # clean up old PIDs listed in yaml_mtime
263 keys %{ $self->_event_class->_event_state->{yaml_mtime} }
265 delete $self->_event_class->_event_state->{yaml_mtime}->{$pid}
266 if $self->_event_class->_event_state->{yaml_mtime}->{$pid}
269 $self->_event_class->_save_event_state();
271 # wipe out all current events and reload from YAML
272 $self->_events( [] );
276 eval { require YAML::Syck; };
279 $yaml = YAML::LoadFile( "$file" );
282 open( my $fh, $file ) or die $!;
283 my $content = do { local $/; <$fh> };
285 $yaml = YAML::Syck::Load( $content );
288 foreach my $event ( @{$yaml} ) {
289 $self->schedule( %{$event} );
292 $c->log->info( "Scheduler: PID $$ loaded "
294 . ' events from YAML file' )
295 if $self->_config('logging');
299 $c->log->error("Scheduler: Error reading YAML file: $@") if $@;
302 # Detect the current time zone
303 sub _detect_timezone {
308 eval { $tz = DateTime::TimeZone->new( name => 'local' ) };
311 'Scheduler: Unable to autodetect local time zone, using UTC')
312 if $self->_config('logging');
317 'Scheduler: Using autodetected time zone: ' . $tz->name )
318 if $self->_config('logging');
323 # Check for authorized users on non-auto events
324 sub _event_authorized {
328 # this should never happen, but just in case...
329 return unless $c->req->address;
331 my $hosts_allow = $self->_config('hosts_allow');
332 $hosts_allow = [$hosts_allow] unless ref($hosts_allow) eq 'ARRAY';
333 my $allowed = Set::Scalar->new( @{$hosts_allow} );
335 return $allowed->contains( $c->req->address );
338 # Set::Crontab does not support day names, or '@' shortcuts
363 'yearly' => '0 0 1 1 *',
364 'annually' => '0 0 1 1 *',
365 'monthly' => '0 0 1 * *',
366 'weekly' => '0 0 * * 0',
367 'daily' => '0 0 * * *',
368 'midnight' => '0 0 * * *',
369 'hourly' => '0 * * * *',
370 'always' => '* * * * *',
378 return $cron unless $cron =~ /\w/;
380 if ( $cron =~ /^\@/ ) {
382 return $replace_at{ $cron };
385 for my $name ( keys %replace ) {
386 my $value = $replace{$name};
387 $cron =~ s/$name/$value/i;
388 last unless $cron =~ /\w/;
401 C<Catalyst::Plugin::Scheduler>, C<Catalyst::Plugin::Scheduler::Event>,