File Coverage

blib/lib/App/MultiModule.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


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