Commit | Line | Data |
ba2735b6 |
1 | package Catalyst::Plugin::Scheduler::Event; |
2 | |
3 | use strict; |
4 | use warnings; |
5 | use DateTime; |
6 | use Storable qw/lock_store lock_retrieve/; |
7 | use base qw[Class::Accessor::Fast Class::Data::Inheritable]; |
8 | |
9 | =head1 NAME |
10 | |
11 | Catalyst::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 | |
33 | These are accessors on the event objects that give you information on or |
34 | let you change the behaviour of the scheduled event objects. |
35 | |
36 | All these accessors can be provided as arguments to the C<new> function |
37 | directly, or via the C<< MyApp->schedule >> method indirectly. |
38 | |
39 | =head2 $event->at |
40 | |
41 | Time the event should be triggered, in C<crontab> notation. |
42 | |
43 | If empty, $event->trigger should be filled. |
44 | |
45 | =head2 $event->trigger |
46 | |
47 | C<GET> parameter that will trigger this event. |
48 | |
49 | If empty, $event->at should be filled. |
50 | |
51 | =head2 $event->auto_run |
52 | |
53 | Boolean indicating if the event should be automatically run at the end |
54 | of a dispatch cycle when the event is due, or not. |
55 | |
56 | See C<Catalyst::Plugin::Scheduler> documentation on method C<schedule> |
57 | for an example of C<auto_run> use. |
58 | |
59 | =head2 $event->event |
60 | |
61 | The event that will be triggered by this event object. This would be |
62 | either a C<CODE> ref, or an absolute path in your application. |
63 | |
64 | =head2 $event->label |
65 | |
66 | A pretty print name for your event. Defaults to the C<trigger> or |
67 | C<event> accessors if not explicitly defined. |
68 | |
69 | =head2 $event->priority |
70 | |
71 | Pending events are executed in order of priority, where the highest |
72 | priority comes first. The priority defaults to C<0> but can be set |
73 | explicitly when scheduling an event to make it run earlier or later |
74 | in the dispatch cycle. |
75 | |
76 | =head2 $event->active |
77 | |
78 | Marks 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 |
80 | events are never present in the C<< MyApp->scheduler->list_pending_events >> |
81 | call. |
82 | |
83 | =head2 $event->scheduled_by |
84 | |
85 | A text string containing details on the code that scheduled this event. |
86 | Useful for debugging purposes or for accountability. |
87 | |
88 | =cut |
89 | |
90 | my $Base = 'Catalyst::Plugin::Scheduler::Base'; |
91 | my @EventAcc = qw[output error last_run running]; |
92 | my @ExtraAcc = qw[ |
93 | next_run |
94 | next_run_as_dt |
95 | next_run_as_string |
96 | last_run_as_string |
97 | ]; |
98 | my @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 | |
115 | sub ls_accessors { return ( @Acc, @EventAcc, @ExtraAcc ) }; |
116 | |
117 | =head1 METHODS |
118 | |
119 | =head2 $event = Catalyst::Plugin::Scheduler::Event->new( ... ); |
120 | |
121 | Creates a new C<Event> object from it's arguments. All possible arguments |
122 | and their meaning are listed in the C<ACCESSORS> section above. |
123 | |
124 | C<new> should usually not be called directly, but via the interface |
125 | provided via the scheduler object: |
126 | |
127 | MyApp->scheduler->schedule( |
128 | at => ... |
129 | event => ... |
130 | ... |
131 | ); |
132 | |
133 | =cut |
134 | |
135 | sub 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 | |
154 | Output of the event the B<last time> it was run. This is the return |
155 | value of the executed event. |
156 | |
157 | =head2 $str = $event->error |
158 | |
159 | Fatal errors encountered the B<last time> this event was run. |
160 | |
161 | If $event->error returns a string, you can be sure that the event |
162 | did not complete succesfully. |
163 | |
164 | =head2 $pid = $event->running |
165 | |
166 | Returns the pid of the process that is running the event currently. |
167 | |
168 | If a pid is returned, the event is running B<right now>. If the |
169 | pid is different from your pid, another process is running the event. |
170 | |
171 | =head2 $time = $event->last_run |
172 | |
173 | The 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 |
183 | for 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 | |
207 | A pretty-printable version of the C<last_run> |
208 | |
209 | =cut |
210 | |
211 | sub 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 | |
228 | The output of C<time()> the next time the event is due to be run. |
229 | |
230 | =cut |
231 | |
232 | sub 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 | |
240 | A pretty-printable version of the C<next_run> |
241 | |
242 | =cut |
243 | |
244 | sub 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 | |
257 | Returns 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 | |
298 | Run the event. Any arguments passed to C<run> will be passed along to |
299 | the event that is being run. If C<run> returns true, running the event |
300 | completed without fatal errors. If C<run> returns false, a fatal error |
301 | was encountered while running the event. |
302 | |
303 | You can inspect the following accessors after running the event for |
304 | output and diagnostics: |
305 | |
306 | $event->output; # any output returned from the event |
307 | $event->error; # any fatal errors caught while running the event |
308 | |
309 | B<NOTE> that you can only C<run> an event during a C<Catalyst> action, |
310 | as the events need access to the C<Catalyst> object. In short, this means |
311 | that your C<$c> needs to be an object rather than a class name. If C<$c> |
312 | is not an object, C<run> will throw an exception. |
313 | |
314 | =cut |
315 | |
316 | sub 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 | |
385 | sub _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 | |
406 | sub _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 | |
421 | Returns the C<time()> the last check was made for pending events from |
422 | the dispatcher. |
423 | |
424 | =cut |
425 | |
426 | ### dont pass arguments, so it becomes read only |
427 | sub 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 | |
434 | Returns a pretty-printable version of C<last_check_time>. |
435 | |
436 | =cut |
437 | |
438 | sub 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 | |
453 | sub _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 | |
495 | 1; |
496 | |
497 | __END__ |
498 | |
499 | =head1 SEE ALSO |
500 | |
501 | C<Catalyst::Plugin::Scheduler>, C<Catalyst::Plugin::Scheduler::Base>, |
502 | |
503 | =cut |