File Coverage

blib/lib/App/MultiModule.pm
Criterion Covered Total %
statement 33 303 10.8
branch 0 154 0.0
condition 0 78 0.0
subroutine 11 27 40.7
pod 2 2 100.0
total 46 564 8.1


line stmt bran cond sub pod time code
1             package App::MultiModule;
2             $App::MultiModule::VERSION = '1.143160';
3 29     29   436041 use 5.006;
  29         83  
  29         937  
4 29     29   118 use strict;
  29         37  
  29         987  
5 29     29   124 use warnings FATAL => 'all';
  29         293  
  29         1148  
6              
7 29     29   14911 use POE;
  29         713575  
  29         207  
8 29     29   1843169 use Digest::MD5;
  29         62  
  29         923  
9 29     29   758 use Storable;
  29         2658  
  29         1253  
10 29     29   2868 use App::MultiModule::API;
  29         52  
  29         553  
11 29     29   566 use IPC::Transit;
  29         25455  
  29         616  
12 29     29   13633 use Message::Transform qw(mtransform);
  29         10720  
  29         1593  
13              
14 29     29   12369 use parent 'App::MultiModule::Core';
  29         6838  
  29         149  
15              
16             =head1 NAME
17              
18             App::MultiModule - Framework to intelligently manage many parallel tasks
19              
20             =head1 WARNING
21              
22             This is a very early release. That means it has a whole pile of
23             technical debt. One clear example is that, at this point, this
24             distribution doesn't even try to function on any OS except Linux.
25              
26             =head1 SYNOPSIS
27              
28             Look at the documentation for the MultiModule program proper; it will be
29             rare to use this module directly.
30              
31             =head1 EXPORT
32              
33             none
34              
35             =head1 SUBROUTINES/METHODS
36              
37             =head2 new
38              
39             Constructor
40              
41             =over 4
42              
43             =item state_dir
44              
45             =item qname (required)
46              
47             IPC::Transit queue name that controls this module
48              
49             =item module_prefixes
50              
51             =item module
52              
53             =item debug
54              
55             =item oob
56              
57             =back
58              
59             =cut
60              
61             sub new {
62 0     0 1   my $class = shift;
63 0           my %args = @_;
64 0 0         die 'App::MultiModule::new: it is only safe to instantiate this one time per process space'
65             if $App::MultiModule::instantiated;
66 0           $App::MultiModule::instantiated = 1;
67 0 0 0       die "App::MultiModule::new failed: required argument 'state_dir' must be a scalar"
68             if not $args{state_dir} or
69             ref $args{state_dir};
70 0           my @module_prefixes = ('App::MultiModule::Tasks::');
71 0 0         if($args{module_prefixes}) {
72 0 0 0       if( ref $args{module_prefixes} and
73             ref $args{module_prefixes} eq 'ARRAY') {
74 0           push @module_prefixes, $_ for @{$args{module_prefixes}};
  0            
75             } else {
76 0           die "App::MultiModule::new failed: passed argument module_prefixes must either be a scalar or ARRAY ref";
77             }
78             }
79              
80 0           my $debug = $args{debug};
81 0 0         $debug = 0 unless defined $debug;
82 0           my $self = {
83             module_prefixes => \@module_prefixes,
84             api => App::MultiModule::API->new(state_dir => $args{state_dir}),
85             my_qname => $args{qname},
86             module => $args{module},
87             tasks => {},
88             message_counts => {},
89             debug => $debug,
90             oob_opts => $args{oob},
91             hold_events_for => {}, #when we issue a 'shutdown' event in POE,
92             #it may or may not stop the next, scheduled event to fire.
93             #it's important for some of the task migration 'stuff' that
94             #save_task_state() not be called in the per-task state save recur
95             #after we want to deallocate.
96             #When we deallocate an internal task, we force a state save, but
97             #with a special flag, no_save_pid, to cause the written state
98             #file to not have a PID. This is important so _manage_tasks()
99             #in the MultiModule task will not think the task is running.
100             pristine_opts => $args{pristine_opts},
101             task_name => 'main',
102             };
103 0 0         $self->{config_file} = $args{config_file} if $args{config_file};
104 0           bless ($self, $class);
105 0           POE::Kernel->run(); #silence warning about run not being called
106 0 0         if($args{config_file}) {
107             $self->recur(repeat_interval => 1, work => sub {
108 0     0     eval {
109 0 0 0       die "App::MultiModule::new failed: optional passed argument config_file($args{config_file}) must either be a scalar and exist and be readable"
110             if ref $args{config_file} or not -r $args{config_file};
111 0           my $ctime = (stat($args{config_file}))[9];
112 0 0         $self->{last_config_stat} = 0
113             unless defined $self->{last_config_stat};
114 0 0         die "all good\n" if $ctime == $self->{last_config_stat};
115 0           $self->{last_config_stat} = $ctime;
116 0           $self->log("reading config from $args{config_file}");
117 0           local $SIG{ALRM} = sub { die "timed out\n"; };
  0            
118 0           alarm 2;
119 0 0         my $conf = do $args{config_file} or die "failed to deserialize $args{config_file}: $@";
120             #handle config 'either way'
121 0 0         if(not $conf->{'.multimodule'}) {
122 0           $conf = {
123             '.multimodule' => {
124             config => $conf
125             }
126             };
127             }
128 0           IPC::Transit::local_queue(qname => $args{qname});
129 0           IPC::Transit::send(qname => $args{qname}, message => $conf);
130             };
131 0           alarm 0;
132 0 0 0       if($@ and $@ ne "all good\n") {
133 0           $self->error("failed to read config file $args{config_file}: $@");
134             }
135 0           });
136             }
137              
138 0           $self->{all_modules_info} = $self->get_multimodules_info();
139              
140             $self->recur(repeat_interval => 60, work => sub {
141 0     0     $self->{message_counts} = {};
142 0           $App::MultiModule::Task::emit_counts = {};
143 0           });
144             $self->recur(repeat_interval => 10, work => sub {
145             =head1 cut
146             if($args{module} and $args{module} eq 'main') {
147             $self->{my_counter} = 0 unless $self->{my_counter};
148             $self->{my_counter}++;
149             open my $fh, '>>', '/tmp/my_logf';
150             print $fh $args{module} . ':' . $self->{my_counter}, "\n";
151             close $fh;
152             exit if $self->{my_counter} > 60;
153             }
154             =cut
155 0     0     $self->{all_modules_info} = $self->get_multimodules_info();
156 0           });
157             $self->recur(repeat_interval => 1, work => sub {
158 0     0     $self->_receive_messages;
159 0           });
160             $SIG{TERM} = sub {
161 0     0     print STDERR "caught SIGTERM. starting orderly exit\n";
162 0           $self->log('caught term');
163 0           _cleanly_exit($self);
164 0           };
165             $SIG{INT} = sub {
166 0     0     print STDERR "caught SIGINT. starting orderly exit\n";
167 0           $self->log('caught int');
168 0           IPC::Transit::send(qname => $args{qname}, message => {
169             '.multimodule' => {
170             control => [
171             { type => 'cleanly_exit',
172             exit_externals => 1,
173             }
174             ],
175             }
176             });
177             #_cleanly_exit($self, exit_external => 1);
178 0           };
179 0           $App::MultiModule::Task::emit_counts = {};
180 0           return $self;
181             }
182              
183             sub _control {
184 0     0     my $self = shift;my $message = shift;
  0            
185 0           my %args = @_;
186 0           my $control = $message->{'.multimodule'};
187 0 0         if($control->{config}) {
188 0           foreach my $task_name (keys %{$control->{config}}) {
  0            
189 0           my $config = $control->{config}->{$task_name};
190 0           $self->{api}->save_task_config($task_name, $config);
191 0           $self->{all_modules_info}->{$task_name}->{config} = $config;
192 0           eval {
193 0           my $task = $self->get_task($task_name);
194             };
195 0 0         if($@) {
196 0 0         $self->debug("_control: failed to get_task($task_name): $@\n") if $self->{debug} > 1;
197             }
198             }
199             }
200 0 0         if($control->{control}) {
201 0 0 0       $self->debug('_control: passed control structure must be ARRAY reference') if $self->{debug} > 1 and ref $control->{control} ne 'ARRAY';
202 0           foreach my $control (@{$control->{control}}) {
  0            
203 0 0         if($control->{type} eq 'cleanly_exit') {
204 0 0         $self->debug('control cleanly exit') if $self->{debug} > 1;
205 0           $self->_cleanly_exit(%$control);
206             }
207             }
208             }
209             }
210              
211             sub _cleanly_exit {
212 0     0     my $self = shift;
213 0           my %args = @_;
214 0           $self->debug('beginning cleanly_exit');
215             #how to exit cleanly:
216             #call save_task_state on all internal stateful tasks
217             #if exit_externals is set:
218             ##send TERM to all external tasks if exit_externals is set
219             ##wait a few seconds
220             ##send KILL to all external tasks and all of their children and children
221              
222 0           my @all_tasks;
223 0           foreach my $task_name (keys %{$self->{all_modules_info}}) {
  0            
224 0           push @all_tasks, $task_name;
225             }
226             #first: 'flush' all of the internal queues
227 0           for(1..5) { #lolwut
228 0           foreach my $task_name (@all_tasks) {
229 0 0         next unless $self->{tasks}->{$task_name};
230 0           IPC::Transit::local_queue(qname => $task_name);
231 0           my $stats = IPC::Transit::stat(
232             qname => $task_name,
233             override_local => _receive_mode_translate('local'));
234 0 0         next unless $stats->{qnum}; #nothing to receive
235 0           while( my $message = IPC::Transit::receive(
236             qname => $task_name,
237             override_local => _receive_mode_translate('local'))) {
238 0           eval {
239 0           $self->{tasks}->{$task_name}->message(
240             $message,
241             root_object => $self
242             );
243             };
244 0 0         if($@) {
245 0           $self->error("_cleanly_exit($task_name) threw: $@");
246             }
247             }
248             }
249             }
250             #second: save state and send signals, as appropriate
251 0           foreach my $task_name (@all_tasks) {
252 0           eval {
253 0           my $task_info = $self->{all_modules_info}->{$task_name};
254 0           my $task_is_stateful = $task_info->{is_stateful};
255 0   0       my $task_config = $task_info->{config} || {};
256 0           my $task_state = $self->{api}->get_task_state($task_name);
257 0           my $task_status = $self->{api}->get_task_status($task_name);
258 0           my $is_loaded = $self->{tasks}->{$task_name};
259 0           my $is_running = 0;
260 0 0 0       if( $task_status and
261             $task_status->{is_running}) {
262 0           $is_running = $task_status->{is_running};
263             }
264 0           my $is_my_pid = 0;
265 0 0 0       if( $task_status and
266             $task_status->{is_my_pid}) {
267 0           $is_my_pid = $task_status->{is_my_pid};
268             }
269             #first case: internal, stateful task
270 0 0 0       if( $is_loaded and
271             $task_is_stateful) {
272 0           $self->{api}->save_task_state($task_name, $self->{tasks}->{$task_name}->{'state'});
273 0           my $status = Storable::dclone($self->{tasks}->{$task_name}->{'status'});
274 0           $status->{is_internal} = 1;
275 0           $self->{api}->save_task_status($task_name, $status);
276             }
277              
278             #second case: external task
279 0 0 0       if( not $is_loaded and
      0        
      0        
280             $is_running and
281             not $is_my_pid and
282             $args{exit_externals}) {
283 0           my $sig = $self->{api}->send_signal($task_name, 15);
284 0           sleep 2;
285 0           $self->log("cleanly_exit: exit_internals: sending signal 9 to $task_name");
286 0   0       $sig = $self->{api}->send_signal($task_name, 9) || 'undef';
287             }
288             };
289             }
290 0           $self->log('exit');
291 0           exit;
292             }
293              
294             sub _receive_messages {
295 0     0     my $self = shift;
296              
297              
298             { #handle messages directed at MultiModule proper
299             #first, we do local queue reads for the management queue
300 0           IPC::Transit::local_queue(qname => $self->{my_qname});
  0            
301 0           while( my $message = IPC::Transit::receive(
302             qname => $self->{my_qname},
303             nonblock => 1,
304             )
305             ) {
306 0           $self->_control($message);
307             }
308             #only the parent MultiModule process reads non-local for itself
309 0 0         if($self->{module} eq 'main') {
310 0           while( my $message = IPC::Transit::receive(
311             qname => $self->{my_qname},
312             nonblock => 1,
313             override_local => 1,
314             )
315             ) {
316 0           $self->_control($message);
317             }
318             }
319             }
320              
321             #we always do local queue reads for all possible local queues
322 0           foreach my $module_name (keys %{$self->{all_modules_info}}) {
  0            
323 0           $self->_receive_messages_from($module_name, 'local');
324             }
325              
326 0 0         if($self->{module} ne 'main') {
327 0           $self->_receive_messages_from($self->{module}, 'non-local');
328             } else { #main process
329             #non-local queue reads for every task that is not external
330 0           while(my($module_name, $module_info) = each %{$self->{all_modules_info}}) {
  0            
331 0 0 0       if( $module_info->{config} and
332             $module_info->{config}->{is_external}) {
333             #external; do not receive
334 0           next;
335             }
336 0           $self->_receive_messages_from($module_name, 'non-local');
337             }
338             }
339             }
340              
341             sub _receive_mode_translate {
342 0     0     my $mode = shift;
343 0 0         return 0 if $mode eq 'local';
344 0 0         return 1 if $mode eq 'non-local';
345 0           die "unknown mode: $mode\n";
346             }
347              
348             sub _receive_messages_from {
349 0     0     my $self = shift;
350 0           my $qname = shift; my $receive_mode = shift;
  0            
351 0           my %args = @_;
352 0           IPC::Transit::local_queue(qname => $qname);
353 0           my $stats = IPC::Transit::stat(
354             qname => $qname,
355             override_local => _receive_mode_translate($receive_mode));
356 0 0         return unless $stats->{qnum}; #nothing to receive
357             #at this point, there are one or more messages for us to receive
358             #we can only deliver messages to tasks that are loaded AND configured
359              
360 0 0 0       if( $self->{tasks}->{$qname} and
    0 0        
361             $self->{tasks}->{$qname}->{config_is_set}) {
362 0           while( my $message = IPC::Transit::receive(
363             qname => $qname,
364             nonblock => 1,
365             override_local => _receive_mode_translate($receive_mode),
366             )
367             ) {
368             #handle dynamic state transforms
369 0 0 0       if( $message->{'.multimodule'} and
370             $message->{'.multimodule'}->{transform}) {
371 0 0         $self->debug("_receive_messages_from($qname, _receive_mode_translate($receive_mode): in transform")
372             if $self->{debug} > 1;
373 0           eval {
374 0           mtransform( $self->{tasks}->{$qname}->{'state'},
375             $message->{'.multimodule'}->{transform}
376             );
377             };
378 0 0         $self->error("_receive_messages_from: transform failed: $@")
379             if $@;
380 0 0         $self->debug('post-transform state',
381             'state' => $self->{tasks}->{$qname}->{'state'})
382             if $self->{debug} > 5;
383              
384 0           return;
385             }
386             #actually deliver the message
387 0           eval {
388 0 0         $self->{message_counts}->{$qname} = 0 unless
389             $self->{message_counts}->{$qname};
390 0           $self->{message_counts}->{$qname}++;
391 0           $self->{tasks}->{$qname}->message($message, root_object => $self);
392             };
393 0 0         if($@) {
394 0           my $err = $@;
395 0           $self->error("_receive_messages_from: handle_message failed: $@");
396 0           $self->bucket({
397             task_name => $qname,
398             check_type => 'admin',
399             cutoff_age => 300,
400             min_points => 1,
401             min_bucket_span => 0.01,
402             bucket_name => "$qname:local.admin.task_message_failure",
403             bucket_metric => 'local.admin.task_message_failure',
404             bucket_type => 'sum',
405             value => 1,
406             });
407             }
408             }
409             } elsif( $self->{tasks}->{$qname} and
410             not $self->{tasks}->{$qname}->{config_is_set}) {
411             #in this case, the task is loaded but not configured
412             #we just wait for the configure to happen
413 0 0         $self->debug("_receive_messages_from($qname): config_is_set is false")
414             if $self->{debug} > 5;
415             } else {
416             #in this case, the task is not loaded; we need to load it,
417             #but not deliver the message to it
418 0 0         $self->debug("_receive_messages_from($qname): task is not loaded")
419             if $self->{debug} > 5;
420 0           eval {
421 0           my $task = $self->get_task($qname);
422             };
423 0 0         if($@) {
424 0           $self->error("_receive_messages_from($qname): failed to get_task($qname): $@");
425 0           return;
426             }
427             }
428             }
429              
430             { #close over get_task() and its helper function
431             #http://stackoverflow.com/questions/433752/how-can-i-determine-if-a-perl-function-exists-at-runtime
432             my $function_exists = sub {
433 29     29   52059 no strict 'refs';
  29         50  
  29         32842  
434             my $funcname = shift;
435             return \&{$funcname} if defined &{$funcname};
436             return;
437             };
438              
439             =head2 get_task
440             =cut
441             sub get_task {
442 0     0 1   my $self = shift; my $task_name = shift;
  0            
443 0           my %args = @_;
444 0 0         $self->debug("in get_task($task_name)") if $self->{debug} > 5;
445 0 0         $self->debug("get_task($task_name)", tasks => $self->{tasks})
446             if $self->{debug} > 5;
447 0 0         return $self->{tasks}->{$task_name} if $self->{tasks}->{$task_name};
448 0 0         $self->debug("get_task:($task_name)",
449             module_prefixes => $self->{module_prefixes})
450             if $self->{debug} > 5;
451              
452             #first let's find out if this thing is running externally
453 0           my $task_status = $self->{api}->get_task_status($task_name);
454             # $self->debug('get_task: ', task_state => $task_state, task_status => $task_status) if $self->{debug} > 5;
455 0 0         $self->debug('get_task: ', task_status => $task_status) if $self->{debug} > 5;
456 0 0 0       if( $task_status and
      0        
457             $task_status->{is_running} and
458             not $task_status->{is_my_pid}) {
459             #this thing is running and it is NOT our PID. That means it's
460             #running externally, so we just leave it alone
461 0           $self->error("($task_name): get_task: already running externally");
462 0           return undef;
463             #we do not consider what SHOULD be here; that's left to another function
464             }
465              
466             #at this point, we need to consider loading a task, either internal or
467             #external so we need to concern ourselves with what should be
468 0           my $module_info = $self->{all_modules_info}->{$task_name};
469 0   0       my $module_config = $module_info->{config} || {};
470 0           my $wants_external = $module_config->{is_external};
471 0           my $task_is_stateful = $module_info->{is_stateful};
472              
473             #find some reasons we should not load this module
474             #all program instances may load any non-stateful module.
475             #The main program instance may load any module (if it's not already loaded)
476             #the only stateful module external program instances may load is themselves
477 0 0         if($self->{module} ne 'main') {
478             #I am some external program instance
479 0 0         if($task_name ne $self->{module}) {
480             #I am trying to load a module besides myself
481 0 0         if($task_is_stateful) {
482             #and the task is stateful; not allowed
483 0           $self->error("get_task: external($self->{module}) tried to load stateful task $task_name");
484 0           return undef;
485             }
486             }
487             }
488              
489 0 0 0       if($wants_external and not $task_is_stateful) {
490             #this is currently not allowed, since non-stateful tasks don't have
491             #any way of communicating their PID back
492 0           $self->error("task_name $task_name marked as external but is not stateful; this is not allowed");
493 0           return undef;
494             }
495              
496              
497 0 0 0       if($wants_external and $self->{module} eq 'main') {
498             #in this brave new world, we double fork then exec with the proper
499             #arguments to run an external
500             #fork..exec...
501 0           $self->bucket({
502             task_name => $task_name,
503             check_type => 'admin',
504             cutoff_age => 300,
505             min_points => 3,
506             min_bucket_span => 0.5,
507             bucket_name => "$task_name:local.admin.start.external",
508             bucket_metric => 'local.admin.start.external',
509             bucket_type => 'sum',
510             value => 1,
511             });
512 0           my $pid = fork(); #first fork
513 0 0         die "first fork failed: $!" if not defined $pid;
514 0 0         if(not $pid) { #first child
515 0           my $pid = fork(); #second (final) fork
516 0 0         die "second fork failed: $!" if not defined $pid;
517 0 0         if($pid) { #middle parent; just exit
518 0           exit;
519             }
520             #technically, 'grand-child' of the program, but it is init parented
521 0           my $pristine_opts = $self->{pristine_opts};
522 0           my $main_prog = $0;
523 0           my @args = split ' ', $pristine_opts;
524 0           push @args, '-m';
525 0           push @args, $task_name;
526 0 0         $self->debug("preparing to exec: $main_prog " . (join ' ', @args))
527             if $self->{debug} > 1;
528 0           exec $main_prog, @args;
529 0           die "exec failed: $!";
530             }
531 0           return undef;
532             }
533              
534             #at this point, we are loading a module into our process space.
535             #we could be in module 'main' and loading our 5th stateful task,
536             #or we could be an external loading our single allowed stateful task
537             #I want to claim that there is no difference at this point
538             #I believe the only conditional should be on $task_is_stateful
539              
540 0           my $module;
541 0           foreach my $module_prefix (@{$self->{module_prefixes}}) {
  0            
542 0           my $class_name = $module_prefix . $task_name;
543 0 0         $self->debug("get_task $task_name - $class_name\n") if $self->{debug} > 5;
544 0           my $eval = "require $class_name;";
545 0 0         $self->debug("get_task:($task_name): \$eval=$eval")
546             if $self->{debug} > 5;
547 0           eval $eval;
548 0           my $err = $@;
549 0 0 0       $self->debug("get_task:($task_name): \$err = $err")
550             if $err and $self->{debug} > 4;
551 0 0         if($err) {
552 0 0         if($err !~ /Can't locate /) {
553 0           $self->error("get_task:($task_name) threw trying to load module: $@");
554 0           my $type = 'internal';
555 0 0         $type = 'external' if $wants_external;
556 0           print STDERR "bucket: $task_name:local.admin.task_compile_failure.$type\n";
557 0           $self->bucket({
558             task_name => $task_name,
559             check_type => 'admin',
560             cutoff_age => 300,
561             min_points => 1,
562             min_bucket_span => 0.01,
563             bucket_name => "$task_name:local.admin.task_compile_failure.$type",
564             bucket_metric => "local.admin.task_compile_failure.$type",
565             bucket_type => 'sum',
566             value => 1,
567             });
568             }
569 0           next;
570             }
571 0           for ('message') {
572 0           my $function_path = $class_name . '::' . $_;
573 0 0         if(not $function_exists->($function_path)) {
574 0           die "required function $function_path not found in loaded task";
575             }
576             }
577             #make the module right here
578 0           my $task_state = $self->{api}->get_task_state($task_name);
579 0           $module = {
580             config => undef,
581             'state' => $task_state,
582             status => undef,
583             config_is_set => undef,
584             debug => $self->{debug},
585             root_object => $self,
586             task_name => $task_name,
587             };
588 0           bless ($module, $class_name);
589 0 0         $self->debug("get_task:($task_name): made module", module => $module)
590             if $self->{debug} > 5;
591 0           last;
592             }
593 0 0         if(not $module) {
594 0           $self->error("get_task:($task_name) failed to load module");
595 0           return undef;
596             }
597 0 0         $self->debug("get_task:($task_name): loaded module", module => $module)
598             if $self->{debug} > 5;
599              
600 0           $self->{tasks}->{$task_name} = $module;
601              
602             #stateful or not gets the get_task_config() recur
603             $self->recur(
604             repeat_interval => 1,
605             tags => ['get_task_config'],
606             work => sub {
607 0     0     $module->{config_is_set} = 1;
608 0           my $config = $self->{api}->get_task_config($task_name);
609 0 0         if($config) {
610 0           local $Storable::canonical = 1;
611 0           my $config = Storable::dclone($config);
612 0           my $config_hash = Digest::MD5::md5_base64(Storable::freeze($config));
613 0 0         $module->{config_hash} = 'none' unless $module->{config_hash};
614 0 0         if($module->{config_hash} ne $config_hash) {
615 0           $module->{config_hash} = $config_hash;
616 0           $module->set_config($config);
617             }
618             }
619             }
620 0           );
621              
622 0 0         if($task_is_stateful) {
623 0           delete $self->{hold_events_for}->{$task_name};
624             $self->recur(
625             repeat_interval => 1,
626             tags => ['save_task_state'],
627             override_repeat_interval => sub {
628             # print STDERR "$task_name: " . Data::Dumper::Dumper $self->{all_modules_info}->{$task_name}->{config}->{intervals};
629 0 0 0 0     if( $self->{all_modules_info} and
      0        
      0        
      0        
630             $self->{all_modules_info}->{$task_name} and
631             $self->{all_modules_info}->{$task_name}->{config} and
632             $self->{all_modules_info}->{$task_name}->{config}->{intervals} and
633             $self->{all_modules_info}->{$task_name}->{config}->{intervals}->{save_state}) {
634             # print STDERR 'override_repeat_interval returned ' . $self->{all_modules_info}->{$task_name}->{config}->{intervals}->{save_state} . "\n";
635 0           return $self->{all_modules_info}->{$task_name}->{config}->{intervals}->{save_state};
636             } else {
637             # print STDERR "override_repeat_interval returned undef\n";
638 0           return undef;
639             }
640             },
641             work => sub {
642             #see comments in the App::MultiModule constructor
643 0 0   0     return if $self->{hold_events_for}->{$task_name};
644 0 0         $self->debug("saving state and status for $task_name") if $self->{debug} > 2;
645 0           eval {
646 0           $self->{api}->save_task_status($task_name, $module->{'status'});
647             };
648 0           eval {
649 0           $self->{api}->save_task_state($task_name, $module->{'state'});
650             };
651             }
652 0           );
653             }
654             }
655             }
656             =head1 AUTHOR
657              
658             Dana M. Diederich, C<diederich@gmail.com>
659              
660             =head1 BUGS
661              
662             Please report any bugs or feature requests at
663             https://github.com/dana/perl-App-MultiModule/issues
664              
665              
666             =head1 SUPPORT
667              
668             You can find documentation for this module with the perldoc command.
669              
670             perldoc App::MultiModule
671              
672              
673             You can also look for information at:
674              
675             =over 4
676              
677             =item * Github bug tracker:
678              
679             L<https://github.com/dana/perl-App-MultiModule/issues>
680              
681             =item * AnnoCPAN: Annotated CPAN documentation
682              
683             L<http://annocpan.org/dist/App-MultiModule>
684              
685             =item * CPAN Ratings
686              
687             L<http://cpanratings.perl.org/d/App-MultiModule>
688              
689             =item * Search CPAN
690              
691             L<http://search.cpan.org/dist/App-MultiModule/>
692              
693             =back
694              
695              
696             =head1 ACKNOWLEDGEMENTS
697              
698              
699             =head1 LICENSE AND COPYRIGHT
700              
701             Copyright 2013 Dana M. Diederich.
702              
703             This program is free software; you can redistribute it and/or modify it
704             under the terms of the the Artistic License (2.0). You may obtain a
705             copy of the full license at:
706              
707             L<http://www.perlfoundation.org/artistic_license_2_0>
708              
709             Any use, modification, and distribution of the Standard or Modified
710             Versions is governed by this Artistic License. By using, modifying or
711             distributing the Package, you accept this license. Do not use, modify,
712             or distribute the Package, if you do not accept this license.
713              
714             If your Modified Version has been derived from a Modified Version made
715             by someone other than you, you are nevertheless required to ensure that
716             your Modified Version complies with the requirements of this license.
717              
718             This license does not grant you the right to use any trademark, service
719             mark, tradename, or logo of the Copyright Holder.
720              
721             This license includes the non-exclusive, worldwide, free-of-charge
722             patent license to make, have made, use, offer to sell, sell, import and
723             otherwise transfer the Package with respect to any patent claims
724             licensable by the Copyright Holder that are necessarily infringed by the
725             Package. If you institute patent litigation (including a cross-claim or
726             counterclaim) against any party alleging that the Package constitutes
727             direct or contributory patent infringement, then this Artistic License
728             to you shall terminate on the date that such litigation is filed.
729              
730             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
731             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
732             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
733             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
734             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
735             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
736             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
737             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
738              
739              
740             =cut
741              
742             1; # End of App::MultiModule