Add tab test to all my modules
[catagits/Catalyst-Plugin-Scheduler.git] / lib / Catalyst / Plugin / Scheduler / Event.pm
CommitLineData
ba2735b6 1package Catalyst::Plugin::Scheduler::Event;
2
3use strict;
4use warnings;
5use DateTime;
6use Storable qw/lock_store lock_retrieve/;
7use base qw[Class::Accessor::Fast Class::Data::Inheritable];
8
9=head1 NAME
10
11Catalyst::Plugin::Scheduler::Event - Event objects for the scheduler
12
13=head1 SYNOPSIS
14
15 ### retrieve all events
16 @events = MyApp->scheduler->events;
17
18 ### access event information
19 print $event->at;
20 print $event->output;
21 print $event->error;
22
23 ### run an event explicitly
24 $event->run or die $event->error;
25
26 MyApp->scheduler->last_check_time;
27 MyApp->scheduler->last_check_time_as_string;
28
29=cut
30
31=head1 ACCESSORS
32
33These are accessors on the event objects that give you information on or
34let you change the behaviour of the scheduled event objects.
35
36All these accessors can be provided as arguments to the C<new> function
37directly, or via the C<< MyApp->schedule >> method indirectly.
38
39=head2 $event->at
40
41Time the event should be triggered, in C<crontab> notation.
42
43If empty, $event->trigger should be filled.
44
45=head2 $event->trigger
46
47C<GET> parameter that will trigger this event.
48
49If empty, $event->at should be filled.
50
51=head2 $event->auto_run
52
53Boolean indicating if the event should be automatically run at the end
54of a dispatch cycle when the event is due, or not.
55
56See C<Catalyst::Plugin::Scheduler> documentation on method C<schedule>
57for an example of C<auto_run> use.
58
59=head2 $event->event
60
61The event that will be triggered by this event object. This would be
62either a C<CODE> ref, or an absolute path in your application.
63
64=head2 $event->label
65
66A pretty print name for your event. Defaults to the C<trigger> or
67C<event> accessors if not explicitly defined.
68
69=head2 $event->priority
70
71Pending events are executed in order of priority, where the highest
72priority comes first. The priority defaults to C<0> but can be set
73explicitly when scheduling an event to make it run earlier or later
74in the dispatch cycle.
75
76=head2 $event->active
77
78Marks whether the event is active or not. Defaults to C<1> but you can
79(temporarily) disable an event by setting active to C<0>. Inactive
80events are never present in the C<< MyApp->scheduler->list_pending_events >>
81call.
82
83=head2 $event->scheduled_by
84
85A text string containing details on the code that scheduled this event.
86Useful for debugging purposes or for accountability.
87
88=cut
89
90my $Base = 'Catalyst::Plugin::Scheduler::Base';
91my @EventAcc = qw[output error last_run running];
92my @ExtraAcc = qw[
93 next_run
94 next_run_as_dt
95 next_run_as_string
96 last_run_as_string
97];
98my @Acc = qw[
99 auto_run
100 at
101 event
102 trigger
103 set
104 priority
105 label
106 active
107 scheduled_by
108];
109
110
111
112__PACKAGE__->mk_accessors( @Acc );
113__PACKAGE__->mk_classdata( _event_state => {} );
114
115sub ls_accessors { return ( @Acc, @EventAcc, @ExtraAcc ) };
116
117=head1 METHODS
118
119=head2 $event = Catalyst::Plugin::Scheduler::Event->new( ... );
120
121Creates a new C<Event> object from it's arguments. All possible arguments
122and their meaning are listed in the C<ACCESSORS> section above.
123
124C<new> should usually not be called directly, but via the interface
125provided via the scheduler object:
126
127 MyApp->scheduler->schedule(
128 at => ...
129 event => ...
130 ...
131 );
132
133=cut
134
135sub new {
136 my $self = shift;
137 my %args = @_;
138
139 my $obj = $self->SUPER::new({
140 active => 1,
141 prioirity => 0,
142 scheduled_by => $Base->_caller_string,
143 %args
144 });
145
146 ### make sure it has a name
147 $obj->label( $obj->trigger || $obj->event ) unless $obj->label;
148
149 return $obj;
150}
151
152=head2 $str = $event->output
153
154Output of the event the B<last time> it was run. This is the return
155value of the executed event.
156
157=head2 $str = $event->error
158
159Fatal errors encountered the B<last time> this event was run.
160
161If $event->error returns a string, you can be sure that the event
162did not complete succesfully.
163
164=head2 $pid = $event->running
165
166Returns the pid of the process that is running the event currently.
167
168If a pid is returned, the event is running B<right now>. If the
169pid is different from your pid, another process is running the event.
170
171=head2 $time = $event->last_run
172
173The output of C<time()> the last time the event was run.
174
175=cut
176
177### we want these items available via the event objects,
178### however, they need to be stored in the state file.. so we
179### are basically passing thru from here to the state file..
180### this provides a nicer api, but also means our state is guaranteed
181### to be correct, even if the YAML file is being reloaded or
182### schedules are being altered
183for my $acc ( @EventAcc ) {
184 no strict 'refs';
185
186 my $priv_method = "_$acc";
187 *$acc = sub { shift->$priv_method };
188 *$priv_method = sub {
189 my $self = shift;
190
191 ### make sure the event state is initialized
192 $self->_get_event_state;
193
194 ### access like this, so we're sure we're using the right hashref
195 ### to write into, even if it's not defined yet
196 if( @_ ) {
197 $self->_event_state->{'events'}->{ $self->event }->{$acc} = $_[0];
198 $self->_save_event_state;
199 }
200
201 return $self->_event_state->{'events'}->{ $self->event }->{$acc};
202 }
203}
204
205=head2 $str = $event->last_run_as_string
206
207A pretty-printable version of the C<last_run>
208
209=cut
210
211sub last_run_as_string {
212 my $self = shift;
213
214 if( my $last_run = $self->last_run ) {
215
216 my $dt = DateTime->from_epoch(
217 epoch => $last_run,
218 time_zone => $Base->_config('time_zone'),
219 );
220 return join ' ', $dt->ymd, $dt->hms, $dt->time_zone_short_name;
221 }
222
223 return;
224}
225
226=head2 $time = $event->next_run
227
228The output of C<time()> the next time the event is due to be run.
229
230=cut
231
232sub next_run {
233 my $self = shift;
234 my $dt = $self->next_run_as_dt or return;
235 return $dt->epoch;
236}
237
238=head2 $str = $event->next_run_as_string
239
240A pretty-printable version of the C<next_run>
241
242=cut
243
244sub next_run_as_string {
245 my $self = shift;
246
247 if( $self->set ) {
248 my $next = $self->next_run_as_dt;
249 return join ' ', $next->ymd, $next->hms, $next->time_zone_short_name;
250 }
251
252 return;
253}
254
255=head2 $dt = $event->next_run_as_dt
256
257Returns the time that this event is due to be run as a C<DateTime> object
258
259=cut
260
247e43e2 261{ my %cache = ();
262
263 sub next_run_as_dt {
264 my $self = shift;
265
266 ### the time for the next run is EITHER based on the last_run time
267 ### ie, if the last run time is 61 seconds ago, and it's a per minute
268 ### schedule, the 'next run' should return a time of 1 second ago.
269 ### similarly, if the last run time is 2 hours ago, the 'next run'
270 ### should return a time of 1h59mins ago, which again means it's due.
271 ### The 'last_run' may be 0 (or better yet, undef), in that case,
272 ### we assume the last run time was NOW. The problem is that if we
273 ### keep asking the 'next_run' time on subsequent requests, it will
274 ### always be based on 'NOW' + 1 minute (for events running every
275 ### minute), which will always be in the future. Hence we cache
276 ### the answer the first time 'last_run' returns false (ie, never
277 ### run) and use that answer as a fallback, meaning that if we come
278 ### back later (say, 65 seconds later), the 'next run' will be based
279 ### on NOW - 65 + 1 minute, which is 5 seconds ago, and hence the
280 ### event will trigger...
281 my $epoch = $self->last_run ||
282 $cache{$self} ||
283 do { $cache{$self} = time };
284
285 if( $self->set ) {
286 my $dt = DateTime->from_epoch(
287 epoch => $epoch,
288 time_zone => $Base->_config('time_zone'),
289 );
290 return $self->set->next( $dt );
291 }
292 return;
293 }
ba2735b6 294}
295
296=head2 $bool = $event->run( ... )
297
298Run the event. Any arguments passed to C<run> will be passed along to
299the event that is being run. If C<run> returns true, running the event
300completed without fatal errors. If C<run> returns false, a fatal error
301was encountered while running the event.
302
303You can inspect the following accessors after running the event for
304output and diagnostics:
305
306 $event->output; # any output returned from the event
307 $event->error; # any fatal errors caught while running the event
308
309B<NOTE> that you can only C<run> an event during a C<Catalyst> action,
310as the events need access to the C<Catalyst> object. In short, this means
311that your C<$c> needs to be an object rather than a class name. If C<$c>
312is not an object, C<run> will throw an exception.
313
314=cut
315
316sub run {
317 my $self = shift;
318 my $c = $Base->_app;
319
320 Catalyst::Exception->throw(
321 message => "Can not run scheduled events -- $c is not an object"
322 ) unless ref $c;
323
324 ### XXX mark running
325 # make sure we're the only process running this event
326 ### XXX is 'return' the right thing to do? --kane
327 ### mark_runnign will return true if we are the ones (exclusively)
328 ### running this process.
329 return unless $self->_mark_running;
330
331 ### reset any output/errors
332 $self->_output( undef );
333 $self->_error( undef );
334
335 my $event_name = $self->label;
336 $c->log->debug("Scheduler: Executing $event_name")
337 if $Base->_config('logging');
338
339 # trap errors
340 local $c->{error} = [];
341
342 # return value/output from the event, if any
343 my $output;
344
345 # run event
346 eval {
347 # do not allow the event to modify the response
348 local $c->res->{body};
349 local $c->res->{cookies};
350 local $c->res->{headers};
351 local $c->res->{location};
352 local $c->res->{status};
353
354 ### XXX events can not set output without RETURNING
355 ### their output... is this good? --kane
356 $output = ref $self->event eq 'CODE'
357 ? $self->event->($c, @_ )
358 : $c->forward( $self->event, [@_] );
359 };
360
361 my @errors = @{ $c->{error} };
362 push @errors, $@ if $@;
363
364 if (@errors) {
365 ### we check for this error in the tests -- if you alter
366 ### it, tests will starting spewing errors
367 $c->log->error(
368 'Scheduler: Error executing ' . "$event_name: $_"
369 ) for @errors;
370
371 my $error = join '; ', @errors;
372
373 $self->_error( $error );
374 $output .= $error;
375 }
376
377 $self->_output( $output );
378
379 $self->_mark_finished( );
380
381 return if @errors;
382 return 1;
383}
384
385sub _mark_running {
386 my $self = shift;
387
388 $self->_get_event_state();
389
390 return if $self->running;
391
392 # this is a 2-step process to prevent race conditions
393 # 1. write the state file with our PID
394 $self->_running( $$ );
395 $self->_save_event_state();
396
397 # 2. re-read the state file and make sure it's got the same PID
398 $self->_get_event_state();
399 if ( $self->running == $$ ) {
400 return 1;
401 }
402
403 return;
404}
405
406sub _mark_finished {
407 my $self = shift;
408
409 $self->_running( 0 );
410 $self->_last_run( time );
411
412 $self->_save_event_state();
413
414 return 1;
415}
416
417=head1 CLASS METHODS
418
419=head2 $time = MyApp->scheduler->last_check_time
420
421Returns the C<time()> the last check was made for pending events from
422the dispatcher.
423
424=cut
425
426### dont pass arguments, so it becomes read only
427sub last_check_time {
428 my $self = shift;
429 return $self->_last_check_time;
430}
431
432=head2 $str = MyApp->scheduler->last_check_time_as_string
433
434Returns a pretty-printable version of C<last_check_time>.
435
436=cut
437
438sub last_check_time_as_string {
439 my $self = shift;
440
441 if( my $last = $self->last_check_time ) {
442
443 my $dt = DateTime->from_epoch(
444 epoch => $last,
445 time_zone => $Base->_config('time_zone'),
446 );
447 return join ' ', $dt->ymd, $dt->hms, $dt->time_zone_short_name;
448 }
449
450 return;
451}
452
453sub _last_check_time {
454 my $self = shift;
455
456 $self->_get_event_state();
457
458 if( @_ ) {
459 $self->_event_state->{'last_check'} = $_[0];
460 $self->_save_event_state;
461 }
462
463 return $self->_event_state->{'last_check'};
464}
465
466{ my $key = 'state_file';
467
468 # get the state from the state file
469 sub _get_event_state {
470 my $self = shift;
471 my $file = $Base->_config($key);
472
473 if ( -e $file ) {
474 $self->_event_state( lock_retrieve $file );
475
476 } else {
477 # initialize the state file
478 $self->_event_state(
479 { last_check => time,
480 events => {},
481 yaml_mtime => {},
482 }
483 );
484 $self->_save_event_state();
485 }
486 }
487
488 # update the state file on disk
489 sub _save_event_state {
490 my $self = shift;
491 lock_store( $self->_event_state, $Base->_config($key) );
492 }
493}
494
4951;
496
497__END__
498
499=head1 SEE ALSO
500
501C<Catalyst::Plugin::Scheduler>, C<Catalyst::Plugin::Scheduler::Base>,
502
503=cut