File Coverage

blib/lib/HPC/Runner.pm
Criterion Covered Total %
statement 39 145 26.9
branch 0 32 0.0
condition 0 3 0.0
subroutine 13 27 48.1
pod 5 11 45.4
total 57 218 26.1


line stmt bran cond sub pod time code
1             package HPC::Runner;
2              
3             #use Carp::Always;
4 1     1   13915 use Data::Dumper;
  1         6413  
  1         50  
5 1     1   406 use IPC::Open3;
  1         2824  
  1         39  
6 1     1   439 use IO::Select;
  1         1094  
  1         34  
7 1     1   5 use Symbol;
  1         2  
  1         39  
8 1     1   715 use Log::Log4perl qw(:easy);
  1         33941  
  1         4  
9 1     1   1139 use DateTime;
  1         88351  
  1         39  
10 1     1   496 use DateTime::Format::Duration;
  1         4029  
  1         39  
11 1     1   4 use Cwd;
  1         1  
  1         47  
12 1     1   4 use File::Path qw(make_path);
  1         1  
  1         47  
13 1     1   11 use File::Spec;
  1         1  
  1         14  
14              
15 1     1   522 use Moose;
  1         284326  
  1         4  
16 1     1   4561 use namespace::autoclean;
  1         2  
  1         7  
17              
18 1     1   57 use Moose::Util::TypeConstraints;
  1         2  
  1         7  
19             #with 'MooseX::Getopt';
20             with 'MooseX::Getopt::Usage';
21             with 'MooseX::Getopt::Usage::Role::Man';
22             with 'MooseX::Object::Pluggable';
23              
24             # For pretty man pages!
25             $ENV{TERM}='xterm-256color';
26              
27             =head1 NAME
28              
29             HPC::Runner - HPC Runner::Slurm, Runner::MCE and Runner::Threads base class
30              
31             =head1 VERSION
32              
33             Version 2.4.2
34              
35             =cut
36              
37             our $VERSION = '2.48';
38              
39             =head1 SYNOPSIS
40              
41             This is a base class for HPC::Runner::MCE and HPC::Runner:Threads. You should not need to call this module directly.
42              
43             =head1 EXPORT
44              
45             =cut
46              
47             =head1 VARIABLES
48              
49             =cut
50              
51             =head2 infile
52              
53             File of commands separated by newline. The command 'wait' indicates all previous commands should finish before starting the next one.
54              
55             =cut
56              
57             has 'infile' => (
58             is => 'rw',
59             isa => 'Str',
60             required => 1,
61             documentation => q{File of commands separated by newline. The command 'wait' indicates all previous commands should finish before starting the next one.},
62             trigger => \&_set_infile,
63             );
64              
65             =head2 _set_infile
66              
67             Internal variable
68              
69             =cut
70              
71             sub _set_infile{
72 0     0     my($self, $infile) = @_;
73              
74 0           $infile = File::Spec->rel2abs($infile);
75 0           $self->{infile} = $infile;
76             }
77              
78             =head2 outdir
79              
80             Directory to write out files and logs.
81              
82             =cut
83              
84             has 'outdir' => (
85             is => 'rw',
86             isa => 'Str',
87             required => 1,
88             default => sub {return getcwd()."/logs" },
89             documentation => q{Directory to write out files.},
90             trigger => \&_set_outdir,
91             );
92              
93             =head2 _set_outdir
94              
95             Internal variable
96              
97             =cut
98              
99             sub _set_outdir{
100 0     0     my($self, $outdir) = @_;
101              
102 0 0         make_path($outdir) if -d $outdir;
103 0           $outdir = File::Spec->rel2abs($outdir);
104 0           $self->{outdir} = $outdir;
105             }
106              
107             =head2 logdir
108              
109             Pattern to use to write out logs directory. Defaults to outdir/prunner_current_date_time/log1 .. log2 .. log3.
110              
111             =cut
112              
113             has 'logdir' => (
114             is => 'rw',
115             isa => 'Str',
116             lazy => 1,
117             required => 1,
118             default => \&set_logdir,
119             documentation => q{Directory where logfiles are written. Defaults to current_working_directory/prunner_current_date_time/log1 .. log2 .. log3'},
120             );
121              
122             =head2 procs
123              
124             Total number of running children allowed at any time. Defaults to 10. The command 'wait' can be used to have a variable number of children running. It is best to wrap this script in a slurm job to not overuse resources. This isn't used within this module, but passed off to mcerunner/parallelrunner.
125              
126             =cut
127              
128             has 'procs' => (
129             is => 'rw',
130             isa => 'Int',
131             default => 4,
132             required => 0,
133             documentation => q{Total number of running jobs allowed at any time. The command 'wait' can be used to have a variable number of children running.}
134             );
135              
136             #TODO add to Log
137             has 'show_processid' => (
138             is => 'rw',
139             isa => 'Bool',
140             default => 0,
141             documentation => q{Show the process ID per logging message. This is useful when aggregating logs.}
142             );
143              
144             #TODO add to Log
145             has 'metastr' => (
146             is => 'rw',
147             isa => 'Str',
148             default => "",
149             documentation => q{Meta str passed from HPC::Runner::Scheduler},
150             required => 0,
151             );
152              
153             =head1 Internal VARIABLES
154              
155             You shouldn't be calling these directly.
156              
157             =cut
158              
159             has 'dt' => (
160             traits => ['NoGetopt'],
161             is => 'rw',
162             isa => 'DateTime',
163             default => sub { return DateTime->now(time_zone => 'local'); },
164             lazy => 1,
165             );
166              
167             =head2 wait
168              
169             Boolean value indicates any job dependencies
170              
171             =cut
172              
173             has 'wait' => (
174             traits => ['NoGetopt'],
175             is => 'rw',
176             isa => 'Bool',
177             default => 0,
178             );
179              
180             has 'cmd' => (
181             traits => ['String', 'NoGetopt'],
182             is => 'rw',
183             isa => 'Str',
184             #lazy_build => 1,
185             required => 0,
186             #default => q{},
187             handles => {
188             add_cmd => 'append',
189             match_cmd => 'match',
190             },
191             predicate => 'has_cmd',
192             clearer => 'clear_cmd',
193             );
194              
195             has 'counter' => (
196             traits => ['Counter', 'NoGetopt'],
197             is => 'rw',
198             isa => 'Num',
199             required => 1,
200             default => 1,
201             handles => {
202             inc_counter => 'inc',
203             dec_counter => 'dec',
204             reset_counter => 'reset',
205             },
206             );
207              
208             #this needs to be called in the main app
209             has 'log' => (
210             traits => ['NoGetopt'],
211             is => 'rw',
212             );
213              
214             has 'command_log' => (
215             traits => ['NoGetopt'],
216             is => 'rw',
217             );
218              
219             has 'logfile' => (
220             traits => ['String', 'NoGetopt'],
221             is => 'rw',
222             default => \&set_logfile,
223             handles => {
224             append_logfile => 'append',
225             prepend_logfile => 'prepend',
226             clear_logfile => 'clear',
227             }
228             );
229              
230             has 'logname' => (
231             isa => 'Str',
232             is => 'rw',
233             default => 'hpcrunner_logs',
234             );
235              
236             =head2 process_table
237              
238             We also want to write all cmds and exit codes to a table
239              
240             =cut
241              
242             has 'process_table' => (
243             isa => 'Str',
244             is => 'rw',
245              
246             handles => {
247             add_process_table => 'append',
248             prepend_process_table => 'prepend',
249             clear_process_table => 'clear',
250             },
251             default => sub {
252             my $self = shift;
253             return $self->logdir."/process_table.md"
254             },
255             lazy => 1,
256             );
257              
258             =head2 plugins
259              
260             Load plugins
261              
262             =cut
263              
264             has 'plugins' => (
265             is => 'rw',
266             isa => 'ArrayRef|Str',
267             documentation => 'Add Plugins to your run',
268             );
269              
270             =head2 jobref
271              
272             Array of arrays details slurm/process/scheduler job id. Index -1 is the most recent job submissisions, and there will be an index -2 if there are any job dependencies
273              
274             =cut
275              
276             has 'jobref' => (
277             traits => ['NoGetopt'],
278             is => 'rw',
279             isa => 'ArrayRef',
280             default => sub { [ [] ] },
281             );
282              
283             =head1 Subroutines
284              
285             =cut
286              
287             sub BUILD {
288 0     0 0   my $self = shift;
289              
290 0           $self->process_plugins;
291             }
292              
293             =head2 set_logdir
294              
295             Set the log directory
296              
297             =cut
298              
299             sub set_logdir{
300 0     0 1   my $self = shift;
301              
302 0           my $logdir;
303 0           $logdir = $self->outdir."/".$self->set_logfile."-".$self->logname;
304              
305 0           $DB::single=2;
306 0           $logdir =~ s/\.log$//;
307              
308 0 0         make_path($logdir) if ! -d $logdir;
309 0           return $logdir;
310             }
311              
312             =head2 set_logfile
313              
314             Set logfile
315              
316             =cut
317              
318             sub set_logfile{
319 0     0 1   my $self = shift;
320              
321 0           my $tt = $self->dt->ymd();
322 0           return "$tt";
323             }
324              
325             =head2 init_log
326              
327             Initialize Log4perl log
328              
329             =cut
330              
331             sub init_log {
332 0     0 1   my $self = shift;
333              
334 0           Log::Log4perl->easy_init(
335             {
336             level => $TRACE,
337             utf8 => 1,
338             mode => 'append',
339             file => ">>".$self->logdir."/".$self->logfile,
340             layout => '%d: %p %m%n '
341             }
342             );
343              
344 0           my $log = get_logger();
345 0           return $log;
346             }
347              
348             =head2 process_plugins
349              
350             Split and process plugins
351              
352             =cut
353              
354             sub process_plugins{
355 0     0 1   my $self = shift;
356              
357 0 0         return unless $self->plugins;
358              
359 0 0         if(ref($self->plugins)){
360 0           my @plugins = @{$self->plugins};
  0            
361 0           foreach my $plugin (@plugins){
362 0 0         if($plugin =~ m/,/){
363 0           my @tmp = split(',', $plugin);
364 0           foreach my $tmp (@tmp){
365 0           push(@plugins, $tmp);
366             }
367             }
368             }
369 0           $self->load_plugins(@plugins);
370             }
371             else{
372 0           $self->load_plugin($self->plugins);
373             }
374             }
375              
376             =head2 run_command_threads
377              
378             Start the thread, run the command, and finish the thread
379              
380             =cut
381              
382             sub run_command_threads{
383 0     0 1   my $self = shift;
384              
385 0 0         my $pid = $self->threads->start($self->cmd) and return;
386 0           push(@{$self->jobref->[-1]}, $pid);
  0            
387              
388 0           my $exitcode = $self->_log_commands($pid);
389              
390 0           $self->threads->finish($exitcode); # pass an exit code to finish
391              
392 0           return;
393             }
394              
395             #=head2 run_command_mce
396              
397             #MCE knows which subcommand to use from Runner/MCE - object mce
398              
399             #=cut
400              
401             #sub run_command_mce{
402             #my $self = shift;
403              
404             #my $pid = $$;
405              
406             #$DB::single=2;
407              
408             ##Mce doesn't take exitcode to end
409             #push(@{$self->jobref->[-1]}, $pid);
410             #$self->_log_commands($pid);
411              
412             #return;
413             #}
414              
415             =head2 _log_commands
416              
417             Log the commands run them. Cat stdout/err with IO::Select so we hopefully don't break things.
418              
419             This example was just about 100% from the following perlmonks discussions.
420              
421             http://www.perlmonks.org/?node_id=151886
422              
423             You can use the script at the top to test the runner. Just download it, make it executable, and put it in the infile as
424              
425             perl command.pl 1
426             perl command.pl 2
427             #so on and so forth
428              
429             =cut
430              
431             sub _log_commands {
432 0     0     my($self, $pid) = @_;
433              
434 0           my $dt1 = $self->dt;
435              
436 0           $DB::single=2;
437              
438 0           my($cmdpid, $exitcode) = $self->log_job;
439              
440 0           $self->log_cmd_messages("info", "Finishing job ".$self->counter." with ExitCode $exitcode", $cmdpid);
441              
442 0           my $dt2 = DateTime->now();
443 0           my $duration = $dt2 - $dt1;
444 0           my $format = DateTime::Format::Duration->new(
445             pattern => '%Y years, %m months, %e days, %H hours, %M minutes, %S seconds'
446             );
447              
448 0           $self->log_cmd_messages("info", "Total execution time ".$format->format_duration($duration), $cmdpid);
449              
450 0           $self->log_table($cmdpid, $exitcode, $format->format_duration($duration));
451 0           return $exitcode;
452             }
453              
454             sub name_log {
455 0     0 0   my $self = shift;
456 0           my $pid = shift;
457              
458 0           $self->logfile($self->set_logfile);
459 0           my $string = sprintf ("%03d", $self->counter);
460 0           $self->append_logfile("-CMD_".$string.".log");
461             }
462              
463             ##TODO extend this in HPC-Runner-Web for ENV tags
464             sub log_table {
465 0     0 0   my $self = shift;
466 0           my $cmdpid = shift;
467 0           my $exitcode = shift;
468 0           my $duration = shift;
469              
470 0 0         open(my $pidtablefh, ">>".$self->process_table) or die print "Couldn't open process file $!\n";
471              
472 0           print $pidtablefh "### $self->{cmd}\n";
473 0           print $pidtablefh <<EOF;
474             |$cmdpid|$exitcode|$duration|
475              
476             EOF
477             }
478              
479             #This should be run_job instead and in main app
480             sub log_job {
481 0     0 0   my $self = shift;
482              
483             #Start running job
484 0           my ($infh,$outfh,$errfh);
485 0           $errfh = gensym(); # if you uncomment this line, $errfh will
486 0           my $cmdpid;
487 0           eval{
488 0           $cmdpid = open3($infh, $outfh, $errfh, $self->cmd);
489             };
490 0 0         die $@ if $@;
491 0 0         if(! $cmdpid) {
492 0           print "There is no $cmdpid please contact your administrator with the full command given\n";
493 0           die;
494             }
495 0           $infh->autoflush();
496              
497 0           $self->name_log($cmdpid);
498 0           $self->command_log($self->init_log);
499              
500 0           $DB::single=2;
501              
502 0           $self->log_cmd_messages("info", "Starting Job: ".$self->counter." \nCmd is ".$self->cmd, $cmdpid);
503              
504 0           $DB::single=2;
505              
506 0           my $sel = new IO::Select; # create a select object
507 0           $sel->add($outfh,$errfh); # and add the fhs
508              
509 0           while(my @ready = $sel->can_read) {
510 0           foreach my $fh (@ready) { # loop through them
511 0           my $line;
512             # read up to 4096 bytes from this fh.
513 0           my $len = sysread $fh, $line, 4096;
514 0 0         if(not defined $len){
    0          
515             # There was an error reading
516 0           $self->log_cmd_messages("fatal", "Error from child: $!" , $cmdpid)
517             } elsif ($len == 0){
518             # Finished reading from this FH because we read
519             # 0 bytes. Remove this handle from $sel.
520 0           $sel->remove($fh);
521 0           next;
522             } else { # we read data alright
523 0 0         if($fh == $outfh) {
    0          
524 0           $self->log_cmd_messages("info", $line, $cmdpid)
525             } elsif($fh == $errfh) {
526 0           $self->log_cmd_messages("error", $line, $cmdpid)
527             } else {
528 0           $self->log_cmd_messages('fatal', "Shouldn't be here!\n");
529             }
530             }
531             }
532             }
533              
534 0           waitpid($cmdpid, 1);
535 0           my $exitcode = $?;
536              
537 0           return($cmdpid, $exitcode);
538             }
539              
540             sub log_cmd_messages{
541 0     0 0   my($self, $level, $message, $cmdpid) = @_;
542              
543 0 0 0       if($self->show_processid && $cmdpid){
544 0           $self->command_log->$level("PID: $cmdpid\t$message");
545             }
546             else{
547 0           $self->command_log->$level($message);
548             }
549             }
550              
551             sub log_main_messages{
552 0     0 0   my($self, $level, $message) = @_;
553              
554 0 0         return unless $message;
555 0 0         $level = 'debug' unless $level;
556 0           $self->log->$level($message);
557             }
558              
559             __PACKAGE__->meta->make_immutable;
560              
561             1;
562              
563             =head1 AUTHOR
564              
565             Jillian Rowe, C<< <jillian.e.rowe at gmail.com> >>
566              
567             =head1 BUGS
568              
569             Please report any bugs or feature requests to C<bug-runner-init at rt.cpan.org>, or through
570             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HPC-Runner>. I will be notified, and then you'll
571             automatically be notified of progress on your bug as I make changes.
572              
573             =head1 SUPPORT
574              
575             You can find documentation for this module with the perldoc command.
576              
577             perldoc HPC::Runner
578              
579             You can also look for information at:
580              
581             =over 4
582              
583             =item * RT: CPAN's request tracker (report bugs here)
584              
585             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=HPC-Runner>
586              
587             =item * AnnoCPAN: Annotated CPAN documentation
588              
589             L<http://annocpan.org/dist/HPC-Runner>
590              
591             =item * CPAN Ratings
592              
593             L<http://cpanratings.perl.org/d/HPC-Runner>
594              
595             =item * Search CPAN
596              
597             L<http://search.cpan.org/dist/HPC-Runner/>
598              
599             =back
600              
601             =head1 Acknowledgements
602              
603             This module was originally developed at and for Weill Cornell Medical
604             College in Qatar within ITS Advanced Computing Team. With approval from
605             WCMC-Q, this information was generalized and put on github, for which
606             the authors would like to express their gratitude.
607              
608             =head1 LICENSE AND COPYRIGHT
609              
610             Copyright 2014 Weill Cornell Medical College.
611              
612             This program is free software; you can redistribute it and/or modify it
613             under the terms of the the Artistic License (2.0). You may obtain a
614             copy of the full license at:
615              
616             L<http://www.perlfoundation.org/artistic_license_2_0>
617              
618             Any use, modification, and distribution of the Standard or Modified
619             Versions is governed by this Artistic License. By using, modifying or
620             distributing the Package, you accept this license. Do not use, modify,
621             or distribute the Package, if you do not accept this license.
622              
623             If your Modified Version has been derived from a Modified Version made
624             by someone other than you, you are nevertheless required to ensure that
625             your Modified Version complies with the requirements of this license.
626              
627             This license does not grant you the right to use any trademark, service
628             mark, tradename, or logo of the Copyright Holder.
629              
630             This license includes the non-exclusive, worldwide, free-of-charge
631             patent license to make, have made, use, offer to sell, sell, import and
632             otherwise transfer the Package with respect to any patent claims
633             licensable by the Copyright Holder that are necessarily infringed by the
634             Package. If you institute patent litigation (including a cross-claim or
635             counterclaim) against any party alleging that the Package constitutes
636             direct or contributory patent infringement, then this Artistic License
637             to you shall terminate on the date that such litigation is filed.
638              
639             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
640             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
641             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
642             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
643             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
644             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
645             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
646             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
647              
648              
649             =cut
650              
651             #End of Runner::Init