File Coverage

blib/lib/HPC/Runner.pm
Criterion Covered Total %
statement 39 143 27.2
branch 0 28 0.0
condition 0 3 0.0
subroutine 13 27 48.1
pod n/a
total 52 201 25.8


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