Changes:
[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
261sub next_run_as_dt {
262 my $self = shift;
263
264 if( $self->set ) {
265 my $dt = DateTime->from_epoch(
266 epoch => $self->_last_check_time,
267 time_zone => $Base->_config('time_zone'),
268 );
269 return $self->set->next( $dt );
270 }
271 return;
272}
273
274=head2 $bool = $event->run( ... )
275
276Run the event. Any arguments passed to C<run> will be passed along to
277the event that is being run. If C<run> returns true, running the event
278completed without fatal errors. If C<run> returns false, a fatal error
279was encountered while running the event.
280
281You can inspect the following accessors after running the event for
282output and diagnostics:
283
284 $event->output; # any output returned from the event
285 $event->error; # any fatal errors caught while running the event
286
287B<NOTE> that you can only C<run> an event during a C<Catalyst> action,
288as the events need access to the C<Catalyst> object. In short, this means
289that your C<$c> needs to be an object rather than a class name. If C<$c>
290is not an object, C<run> will throw an exception.
291
292=cut
293
294sub run {
295 my $self = shift;
296 my $c = $Base->_app;
297
298 Catalyst::Exception->throw(
299 message => "Can not run scheduled events -- $c is not an object"
300 ) unless ref $c;
301
302 ### XXX mark running
303 # make sure we're the only process running this event
304 ### XXX is 'return' the right thing to do? --kane
305 ### mark_runnign will return true if we are the ones (exclusively)
306 ### running this process.
307 return unless $self->_mark_running;
308
309 ### reset any output/errors
310 $self->_output( undef );
311 $self->_error( undef );
312
313 my $event_name = $self->label;
314 $c->log->debug("Scheduler: Executing $event_name")
315 if $Base->_config('logging');
316
317 # trap errors
318 local $c->{error} = [];
319
320 # return value/output from the event, if any
321 my $output;
322
323 # run event
324 eval {
325 # do not allow the event to modify the response
326 local $c->res->{body};
327 local $c->res->{cookies};
328 local $c->res->{headers};
329 local $c->res->{location};
330 local $c->res->{status};
331
332 ### XXX events can not set output without RETURNING
333 ### their output... is this good? --kane
334 $output = ref $self->event eq 'CODE'
335 ? $self->event->($c, @_ )
336 : $c->forward( $self->event, [@_] );
337 };
338
339 my @errors = @{ $c->{error} };
340 push @errors, $@ if $@;
341
342 if (@errors) {
343 ### we check for this error in the tests -- if you alter
344 ### it, tests will starting spewing errors
345 $c->log->error(
346 'Scheduler: Error executing ' . "$event_name: $_"
347 ) for @errors;
348
349 my $error = join '; ', @errors;
350
351 $self->_error( $error );
352 $output .= $error;
353 }
354
355 $self->_output( $output );
356
357 $self->_mark_finished( );
358
359 return if @errors;
360 return 1;
361}
362
363sub _mark_running {
364 my $self = shift;
365
366 $self->_get_event_state();
367
368 return if $self->running;
369
370 # this is a 2-step process to prevent race conditions
371 # 1. write the state file with our PID
372 $self->_running( $$ );
373 $self->_save_event_state();
374
375 # 2. re-read the state file and make sure it's got the same PID
376 $self->_get_event_state();
377 if ( $self->running == $$ ) {
378 return 1;
379 }
380
381 return;
382}
383
384sub _mark_finished {
385 my $self = shift;
386
387 $self->_running( 0 );
388 $self->_last_run( time );
389
390 $self->_save_event_state();
391
392 return 1;
393}
394
395=head1 CLASS METHODS
396
397=head2 $time = MyApp->scheduler->last_check_time
398
399Returns the C<time()> the last check was made for pending events from
400the dispatcher.
401
402=cut
403
404### dont pass arguments, so it becomes read only
405sub last_check_time {
406 my $self = shift;
407 return $self->_last_check_time;
408}
409
410=head2 $str = MyApp->scheduler->last_check_time_as_string
411
412Returns a pretty-printable version of C<last_check_time>.
413
414=cut
415
416sub last_check_time_as_string {
417 my $self = shift;
418
419 if( my $last = $self->last_check_time ) {
420
421 my $dt = DateTime->from_epoch(
422 epoch => $last,
423 time_zone => $Base->_config('time_zone'),
424 );
425 return join ' ', $dt->ymd, $dt->hms, $dt->time_zone_short_name;
426 }
427
428 return;
429}
430
431sub _last_check_time {
432 my $self = shift;
433
434 $self->_get_event_state();
435
436 if( @_ ) {
437 $self->_event_state->{'last_check'} = $_[0];
438 $self->_save_event_state;
439 }
440
441 return $self->_event_state->{'last_check'};
442}
443
444{ my $key = 'state_file';
445
446 # get the state from the state file
447 sub _get_event_state {
448 my $self = shift;
449 my $file = $Base->_config($key);
450
451 if ( -e $file ) {
452 $self->_event_state( lock_retrieve $file );
453
454 } else {
455 # initialize the state file
456 $self->_event_state(
457 { last_check => time,
458 events => {},
459 yaml_mtime => {},
460 }
461 );
462 $self->_save_event_state();
463 }
464 }
465
466 # update the state file on disk
467 sub _save_event_state {
468 my $self = shift;
469 lock_store( $self->_event_state, $Base->_config($key) );
470 }
471}
472
4731;
474
475__END__
476
477=head1 SEE ALSO
478
479C<Catalyst::Plugin::Scheduler>, C<Catalyst::Plugin::Scheduler::Base>,
480
481=cut