File Coverage

blib/lib/App/MultiModule.pm
Criterion Covered Total %
statement 21 23 91.3
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 29 31 93.5


line stmt bran cond sub pod time code
1             package App::MultiModule;
2             $App::MultiModule::VERSION = '1.171870';
3 1     1   78064 use 5.006;
  1         4  
4 1     1   4 use strict;
  1         2  
  1         29  
5 1     1   4 use warnings FATAL => 'all';
  1         5  
  1         64  
6              
7 1     1   500 use POE;
  1         37989  
  1         5  
8 1     1   52868 use Digest::MD5;
  1         3  
  1         30  
9 1     1   559 use Storable;
  1         2650  
  1         52  
10 1     1   385 use App::MultiModule::API;
  1         7  
  1         34  
11 1     1   569 use IPC::Transit;
  0            
  0            
12             use Message::Transform qw(mtransform);
13              
14             use parent 'App::MultiModule::Core';
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             my $class = shift;
63             my %args = @_;
64             die 'App::MultiModule::new: it is only safe to instantiate this one time per process space'
65             if $App::MultiModule::instantiated;
66             $App::MultiModule::instantiated = 1;
67             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             my @module_prefixes = ('App::MultiModule::Tasks::');
71             if($args{module_prefixes}) {
72             if( ref $args{module_prefixes} and
73             ref $args{module_prefixes} eq 'ARRAY') {
74             push @module_prefixes, $_ for @{$args{module_prefixes}};
75             } else {
76             die "App::MultiModule::new failed: passed argument module_prefixes must either be a scalar or ARRAY ref";
77             }
78             }
79              
80             my $debug = $args{debug};
81             $debug = 0 unless defined $debug;
82             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             $self->{config_file} = $args{config_file} if $args{config_file};
104             bless ($self, $class);
105             POE::Kernel->run(); #silence warning about run not being called
106             if($args{config_file}) {
107             $self->recur(repeat_interval => 1, work => sub {
108             eval {
109             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             my $ctime = (stat($args{config_file}))[9];
112             $self->{last_config_stat} = 0
113             unless defined $self->{last_config_stat};
114             die "all good\n" if $ctime == $self->{last_config_stat};
115             $self->{last_config_stat} = $ctime;
116             $self->log("reading config from $args{config_file}");
117             local $SIG{ALRM} = sub { die "timed out\n"; };
118             alarm 2;
119             my $conf = do $args{config_file} or die "failed to deserialize $args{config_file}: $@";
120             #handle config 'either way'
121             if(not $conf->{'.multimodule'}) {
122             $conf = {
123             '.multimodule' => {
124             config => $conf
125             }
126             };
127             }
128             IPC::Transit::local_queue(qname => $args{qname});
129             IPC::Transit::send(qname => $args{qname}, message => $conf);
130             };
131             alarm 0;
132             if($@ and $@ ne "all good\n") {
133             $self->error("failed to read config file $args{config_file}: $@");
134             }
135             });
136             }
137              
138             $self->{all_modules_info} = $self->get_multimodules_info();
139              
140             $self->recur(repeat_interval => 60, work => sub {
141             $self->{message_counts} = {};
142             $App::MultiModule::Task::emit_counts = {};
143             });
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             $self->{all_modules_info} = $self->get_multimodules_info();
156             });
157             $self->recur(repeat_interval => 1, work => sub {
158             $self->_receive_messages;
159             });
160             $SIG{TERM} = sub {
161             print STDERR "caught SIGTERM. starting orderly exit\n";
162             $self->log('caught term');
163             _cleanly_exit($self);
164             };
165             $SIG{INT} = sub {
166             print STDERR "caught SIGINT. starting orderly exit\n";
167             $self->log('caught int');
168             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             };
179             $App::MultiModule::Task::emit_counts = {};
180             return $self;
181             }
182              
183             sub _control {
184             my $self = shift;my $message = shift;
185             my %args = @_;
186             my $control = $message->{'.multimodule'};
187             if($control->{config}) {
188             foreach my $task_name (keys %{$control->{config}}) {
189             my $config = $control->{config}->{$task_name};
190             $self->{api}->save_task_config($task_name, $config);
191             $self->{all_modules_info}->{$task_name}->{config} = $config;
192             eval {
193             my $task = $self->get_task($task_name);
194             };
195             if($@) {
196             $self->debug("_control: failed to get_task($task_name): $@\n") if $self->{debug} > 1;
197             }
198             }
199             }
200             if($control->{control}) {
201             $self->debug('_control: passed control structure must be ARRAY reference') if $self->{debug} > 1 and ref $control->{control} ne 'ARRAY';
202             foreach my $control (@{$control->{control}}) {
203             if($control->{type} eq 'cleanly_exit') {
204             $self->debug('control cleanly exit') if $self->{debug} > 1;
205             $self->_cleanly_exit(%$control);
206             }
207             }
208             }
209             }
210              
211             sub _cleanly_exit {
212             my $self = shift;
213             my %args = @_;
214             $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             my @all_tasks;
223             foreach my $task_name (keys %{$self->{all_modules_info}}) {
224             push @all_tasks, $task_name;
225             }
226             #first: 'flush' all of the internal queues
227             for(1..5) { #lolwut
228             foreach my $task_name (@all_tasks) {
229             next unless $self->{tasks}->{$task_name};
230             IPC::Transit::local_queue(qname => $task_name);
231             my $stats = IPC::Transit::stat(
232             qname => $task_name,
233             override_local => _receive_mode_translate('local'));
234             next unless $stats->{qnum}; #nothing to receive
235             while( my $message = IPC::Transit::receive(
236             qname => $task_name,
237             override_local => _receive_mode_translate('local'))) {
238             eval {
239             $self->{tasks}->{$task_name}->message(
240             $message,
241             root_object => $self
242             );
243             };
244             if($@) {
245             $self->error("_cleanly_exit($task_name) threw: $@");
246             }
247             }
248             }
249             }
250             #second: save state and send signals, as appropriate
251             foreach my $task_name (@all_tasks) {
252             eval {
253             my $task_info = $self->{all_modules_info}->{$task_name};
254             my $task_is_stateful = $task_info->{is_stateful};
255             my $task_config = $task_info->{config} || {};
256             my $task_state = $self->{api}->get_task_state($task_name);
257             my $task_status = $self->{api}->get_task_status($task_name);
258             my $is_loaded = $self->{tasks}->{$task_name};
259             my $is_running = 0;
260             if( $task_status and
261             $task_status->{is_running}) {
262             $is_running = $task_status->{is_running};
263             }
264             my $is_my_pid = 0;
265             if( $task_status and
266             $task_status->{is_my_pid}) {
267             $is_my_pid = $task_status->{is_my_pid};
268             }
269             #first case: internal, stateful task
270             if( $is_loaded and
271             $task_is_stateful) {
272             $self->{api}->save_task_state($task_name, $self->{tasks}->{$task_name}->{'state'});
273             my $status = Storable::dclone($self->{tasks}->{$task_name}->{'status'});
274             $status->{is_internal} = 1;
275             $self->{api}->save_task_status($task_name, $status);
276             }
277              
278             #second case: external task
279             if( not $is_loaded and
280             $is_running and
281             not $is_my_pid and
282             $args{exit_externals}) {
283             my $sig = $self->{api}->send_signal($task_name, 15);
284             sleep 2;
285             $self->log("cleanly_exit: exit_internals: sending signal 9 to $task_name");
286             $sig = $self->{api}->send_signal($task_name, 9) || 'undef';
287             }
288             };
289             }
290             $self->log('exit');
291             exit;
292             }
293              
294             sub _receive_messages {
295             my $self = shift;
296              
297              
298             { #handle messages directed at MultiModule proper
299             #first, we do local queue reads for the management queue
300             IPC::Transit::local_queue(qname => $self->{my_qname});
301             while( my $message = IPC::Transit::receive(
302             qname => $self->{my_qname},
303             nonblock => 1,
304             )
305             ) {
306             $self->_control($message);
307             }
308             #only the parent MultiModule process reads non-local for itself
309             if($self->{module} eq 'main') {
310             while( my $message = IPC::Transit::receive(
311             qname => $self->{my_qname},
312             nonblock => 1,
313             override_local => 1,
314             )
315             ) {
316             $self->_control($message);
317             }
318             }
319             }
320              
321             #we always do local queue reads for all possible local queues
322             foreach my $module_name (keys %{$self->{all_modules_info}}) {
323             $self->_receive_messages_from($module_name, 'local');
324             }
325              
326             if($self->{module} ne 'main') {
327             $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             while(my($module_name, $module_info) = each %{$self->{all_modules_info}}) {
331             if( $module_info->{config} and
332             $module_info->{config}->{is_external}) {
333             #external; do not receive
334             next;
335             }
336             $self->_receive_messages_from($module_name, 'non-local');
337             }
338             }
339             }
340              
341             sub _receive_mode_translate {
342             my $mode = shift;
343             return 0 if $mode eq 'local';
344             return 1 if $mode eq 'non-local';
345             die "unknown mode: $mode\n";
346             }
347              
348             sub _receive_messages_from {
349             my $self = shift;
350             my $qname = shift; my $receive_mode = shift;
351             my %args = @_;
352             IPC::Transit::local_queue(qname => $qname);
353             my $stats = IPC::Transit::stat(
354             qname => $qname,
355             override_local => _receive_mode_translate($receive_mode));
356             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             if( $self->{tasks}->{$qname} and
361             $self->{tasks}->{$qname}->{config_is_set}) {
362             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             if( $message->{'.multimodule'} and
370             $message->{'.multimodule'}->{transform}) {
371             $self->debug("_receive_messages_from($qname, _receive_mode_translate($receive_mode): in transform")
372             if $self->{debug} > 1;
373             eval {
374             mtransform( $self->{tasks}->{$qname}->{'state'},
375             $message->{'.multimodule'}->{transform}
376             );
377             };
378             $self->error("_receive_messages_from: transform failed: $@")
379             if $@;
380             $self->debug('post-transform state',
381             'state' => $self->{tasks}->{$qname}->{'state'})
382             if $self->{debug} > 5;
383              
384             return;
385             }
386             #actually deliver the message
387             eval {
388             $self->{message_counts}->{$qname} = 0 unless
389             $self->{message_counts}->{$qname};
390             $self->{message_counts}->{$qname}++;
391             $self->{tasks}->{$qname}->message($message, root_object => $self);
392             };
393             if($@) {
394             my $err = $@;
395             $self->error("_receive_messages_from: handle_message failed: $@");
396             $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             $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             $self->debug("_receive_messages_from($qname): task is not loaded")
419             if $self->{debug} > 5;
420             eval {
421             my $task = $self->get_task($qname);
422             };
423             if($@) {
424             $self->error("_receive_messages_from($qname): failed to get_task($qname): $@");
425             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             no strict 'refs';
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             my $self = shift; my $task_name = shift;
443             my %args = @_;
444             $self->debug("in get_task($task_name)") if $self->{debug} > 5;
445             $self->debug("get_task($task_name)", tasks => $self->{tasks})
446             if $self->{debug} > 5;
447             return $self->{tasks}->{$task_name} if $self->{tasks}->{$task_name};
448             $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             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             $self->debug('get_task: ', task_status => $task_status) if $self->{debug} > 5;
456             if( $task_status and
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             $self->error("($task_name): get_task: already running externally");
462             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             my $module_info = $self->{all_modules_info}->{$task_name};
469             my $module_config = $module_info->{config} || {};
470             my $wants_external = $module_config->{is_external};
471             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             if($self->{module} ne 'main') {
478             #I am some external program instance
479             if($task_name ne $self->{module}) {
480             #I am trying to load a module besides myself
481             if($task_is_stateful) {
482             #and the task is stateful; not allowed
483             $self->error("get_task: external($self->{module}) tried to load stateful task $task_name");
484             return undef;
485             }
486             }
487             }
488              
489             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             $self->error("task_name $task_name marked as external but is not stateful; this is not allowed");
493             return undef;
494             }
495              
496              
497             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             $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             my $pid = fork(); #first fork
513             die "first fork failed: $!" if not defined $pid;
514             if(not $pid) { #first child
515             my $pid = fork(); #second (final) fork
516             die "second fork failed: $!" if not defined $pid;
517             if($pid) { #middle parent; just exit
518             exit;
519             }
520             #technically, 'grand-child' of the program, but it is init parented
521             my $pristine_opts = $self->{pristine_opts};
522             my $main_prog = $0;
523             my @args = split ' ', $pristine_opts;
524             push @args, '-m';
525             push @args, $task_name;
526             $self->debug("preparing to exec: $main_prog " . (join ' ', @args))
527             if $self->{debug} > 1;
528             exec $main_prog, @args;
529             die "exec failed: $!";
530             }
531             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             my $module;
541             foreach my $module_prefix (@{$self->{module_prefixes}}) {
542             my $class_name = $module_prefix . $task_name;
543             $self->debug("get_task $task_name - $class_name\n") if $self->{debug} > 5;
544             my $eval = "require $class_name;";
545             $self->debug("get_task:($task_name): \$eval=$eval")
546             if $self->{debug} > 5;
547             eval $eval;
548             my $err = $@;
549             $self->debug("get_task:($task_name): \$err = $err")
550             if $err and $self->{debug} > 4;
551             if($err) {
552             if($err !~ /Can't locate /) {
553             $self->error("get_task:($task_name) threw trying to load module: $@");
554             my $type = 'internal';
555             $type = 'external' if $wants_external;
556             print STDERR "bucket: $task_name:local.admin.task_compile_failure.$type\n";
557             $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             next;
570             }
571             for ('message') {
572             my $function_path = $class_name . '::' . $_;
573             if(not $function_exists->($function_path)) {
574             die "required function $function_path not found in loaded task";
575             }
576             }
577             #make the module right here
578             my $task_state = $self->{api}->get_task_state($task_name);
579             $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             bless ($module, $class_name);
589             $self->debug("get_task:($task_name): made module", module => $module)
590             if $self->{debug} > 5;
591             last;
592             }
593             if(not $module) {
594             $self->error("get_task:($task_name) failed to load module");
595             return undef;
596             }
597             $self->debug("get_task:($task_name): loaded module", module => $module)
598             if $self->{debug} > 5;
599              
600             $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             $module->{config_is_set} = 1;
608             my $config = $self->{api}->get_task_config($task_name);
609             if($config) {
610             local $Storable::canonical = 1;
611             my $config = Storable::dclone($config);
612             my $config_hash = Digest::MD5::md5_base64(Storable::freeze($config));
613             $module->{config_hash} = 'none' unless $module->{config_hash};
614             if($module->{config_hash} ne $config_hash) {
615             $module->{config_hash} = $config_hash;
616             $module->set_config($config);
617             }
618             }
619             }
620             );
621              
622             if($task_is_stateful) {
623             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             if( $self->{all_modules_info} and
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             return $self->{all_modules_info}->{$task_name}->{config}->{intervals}->{save_state};
636             } else {
637             # print STDERR "override_repeat_interval returned undef\n";
638             return undef;
639             }
640             },
641             work => sub {
642             #see comments in the App::MultiModule constructor
643             return if $self->{hold_events_for}->{$task_name};
644             $self->debug("saving state and status for $task_name") if $self->{debug} > 2;
645             eval {
646             $self->{api}->save_task_status($task_name, $module->{'status'});
647             };
648             eval {
649             $self->{api}->save_task_state($task_name, $module->{'state'});
650             };
651             }
652             );
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