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   12929 use Data::Dumper;
  1         6080  
  1         44  
5 1     1   443 use IPC::Open3;
  1         2727  
  1         39  
6 1     1   422 use IO::Select;
  1         1096  
  1         34  
7 1     1   5 use Symbol;
  1         1  
  1         40  
8 1     1   700 use Log::Log4perl qw(:easy);
  1         34490  
  1         5  
9 1     1   1274 use DateTime;
  1         71121  
  1         33  
10 1     1   489 use DateTime::Format::Duration;
  1         3884  
  1         40  
11 1     1   5 use Cwd;
  1         1  
  1         46  
12 1     1   4 use File::Path qw(make_path);
  1         1  
  1         48  
13 1     1   12 use File::Spec;
  1         1  
  1         13  
14              
15 1     1   500 use Moose;
  1         303103  
  1         7  
16 1     1   4693 use namespace::autoclean;
  1         1  
  1         8  
17              
18 1     1   61 use Moose::Util::TypeConstraints;
  1         1  
  1         9  
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.47';
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 'metastr' => (
183             is => 'rw',
184             isa => 'Str',
185             default => "",
186             documentation => q{Meta str passed from HPC::Runner::Scheduler},
187             required => 0,
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 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 1   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 1   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 1   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 1   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 1   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 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 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 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 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 0   my($self, $level, $message) = @_;
588              
589 0 0         return unless $message;
590 0 0         $level = 'debug' unless $level;
591 0           $self->log->$level($message);
592             }
593              
594             __PACKAGE__->meta->make_immutable;
595              
596             1;
597              
598             =head1 AUTHOR
599              
600             Jillian Rowe, C<< <jillian.e.rowe at gmail.com> >>
601              
602             =head1 BUGS
603              
604             Please report any bugs or feature requests to C<bug-runner-init at rt.cpan.org>, or through
605             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HPC-Runner>. I will be notified, and then you'll
606             automatically be notified of progress on your bug as I make changes.
607              
608             =head1 SUPPORT
609              
610             You can find documentation for this module with the perldoc command.
611              
612             perldoc HPC::Runner
613              
614             You can also look for information at:
615              
616             =over 4
617              
618             =item * RT: CPAN's request tracker (report bugs here)
619              
620             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=HPC-Runner>
621              
622             =item * AnnoCPAN: Annotated CPAN documentation
623              
624             L<http://annocpan.org/dist/HPC-Runner>
625              
626             =item * CPAN Ratings
627              
628             L<http://cpanratings.perl.org/d/HPC-Runner>
629              
630             =item * Search CPAN
631              
632             L<http://search.cpan.org/dist/HPC-Runner/>
633              
634             =back
635              
636             =head1 Acknowledgements
637              
638             This module was originally developed at and for Weill Cornell Medical
639             College in Qatar within ITS Advanced Computing Team. With approval from
640             WCMC-Q, this information was generalized and put on github, for which
641             the authors would like to express their gratitude.
642              
643             =head1 LICENSE AND COPYRIGHT
644              
645             Copyright 2014 Weill Cornell Medical College.
646              
647             This program is free software; you can redistribute it and/or modify it
648             under the terms of the the Artistic License (2.0). You may obtain a
649             copy of the full license at:
650              
651             L<http://www.perlfoundation.org/artistic_license_2_0>
652              
653             Any use, modification, and distribution of the Standard or Modified
654             Versions is governed by this Artistic License. By using, modifying or
655             distributing the Package, you accept this license. Do not use, modify,
656             or distribute the Package, if you do not accept this license.
657              
658             If your Modified Version has been derived from a Modified Version made
659             by someone other than you, you are nevertheless required to ensure that
660             your Modified Version complies with the requirements of this license.
661              
662             This license does not grant you the right to use any trademark, service
663             mark, tradename, or logo of the Copyright Holder.
664              
665             This license includes the non-exclusive, worldwide, free-of-charge
666             patent license to make, have made, use, offer to sell, sell, import and
667             otherwise transfer the Package with respect to any patent claims
668             licensable by the Copyright Holder that are necessarily infringed by the
669             Package. If you institute patent litigation (including a cross-claim or
670             counterclaim) against any party alleging that the Package constitutes
671             direct or contributory patent infringement, then this Artistic License
672             to you shall terminate on the date that such litigation is filed.
673              
674             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
675             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
676             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
677             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
678             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
679             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
680             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
681             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
682              
683              
684             =cut
685              
686             #End of Runner::Init