Convert-Pheno
view release on metacpan or search on metacpan
lib/Convert/Pheno/HTTP/Jobs.pm view on Meta::CPAN
if (-d $final) {
$status = $publication->{completed};
_write(path($dir,'status.json'), $status);
my $marker = path($final,'.convert-pheno-owner');
unlink $marker or die "Cannot clear publication marker\n" if -e $marker;
}
if (-d $staging) {
remove_tree("$staging", {error => \my $errors});
die "Cannot clean incomplete export\n" if @$errors;
}
unlink $journal or die "Cannot clear publication record\n";
return $status;
}
sub new {
my ($class, %args) = @_;
die "Job storage and worker executable are required\n" unless $args{root} && $args{worker};
make_path($args{root}, { mode => 0700 });
my $self = bless { %args, root => _resolved_path($args{root}), queue => [], grants => {}, active => {} }, $class;
# Recovery may rewrite run state, so only one supervisor may own this store.
# Keep the handle open for its lifetime; do not unlink the lock file.
open my $lock, '>>', path($self->{root}, '.supervisor.lock') or die "Cannot lock job storage\n";
flock($lock, LOCK_EX | LOCK_NB) or die "This job storage is already in use by another Convert-Pheno service\n";
$self->{storage_lock} = $lock;
# Desktop supplies its platform-detected CPU limit. Standalone services
# default to one worker unless the operator explicitly configures a limit.
$self->{max_allowed_jobs} = _validate_job_limit($ENV{CONVERT_PHENO_JOB_LIMIT} // 1);
my $settings_file = path($self->{root}, 'scheduler-settings.json');
my $settings = -f $settings_file ? _read($settings_file) : {maxConcurrentJobs => 1};
$self->{max_concurrent_jobs} = _validate_job_limit($settings->{maxConcurrentJobs});
$self->{max_concurrent_jobs} = $self->{max_allowed_jobs}
if $self->{max_concurrent_jobs} > $self->{max_allowed_jobs};
my %worker_locks;
# Preflight all runs before changing any state. A surviving child still owns
# its run even if its former supervisor's store lock has been released.
for my $dir (path($self->{root})->children) {
next unless $dir->basename =~ /\A[a-f0-9]{40}\z/ && -f "$dir/status.json";
$worker_locks{"$dir"} = _worker_lock($dir);
}
for my $dir (path($self->{root})->children) {
next unless -f "$dir/status.json";
my $status = eval { _read("$dir/status.json") } or next;
$status = _recover_publication($dir, $status);
if ($status->{status} =~ /\A(?:queued|running|cancelling)\z/) {
$status->{status} = 'interrupted';
$status->{message} = 'The service stopped before this run completed';
$status->{finished} = time();
_write("$dir/status.json", $status);
remove_tree("$dir/staging");
}
unlink "$dir/request.json";
}
return $self;
}
sub settings {
my ($self) = @_;
return {maxConcurrentJobs => $self->{max_concurrent_jobs}, maxAllowedConcurrentJobs => $self->{max_allowed_jobs}};
}
sub update_settings {
my ($self, $settings) = @_;
die "The job service has stopped\n" if $self->{stopped};
die "Provide only maxConcurrentJobs\n" unless ref($settings) eq 'HASH'
&& keys(%$settings) == 1 && exists $settings->{maxConcurrentJobs};
my $limit = _validate_job_limit($settings->{maxConcurrentJobs}, $self->{max_allowed_jobs});
_write(path($self->{root}, 'scheduler-settings.json'), {maxConcurrentJobs => $limit});
$self->{max_concurrent_jobs} = $limit;
$self->_next;
return $self->settings;
}
sub register_file {
my ($self, $file) = @_;
die "Select an existing file or directory\n" if !defined($file) || ref($file) || -l $file;
my $resolved = _resolved_path($file);
die "Selected location is unavailable\n" unless $resolved && (-f $resolved || -d $resolved);
my $id = _id();
$self->{grants}{$id} = $resolved;
return { id => $id, filename => path($resolved)->basename,
directory => -d $resolved ? JSON::XS::true : JSON::XS::false,
bytes => -f $resolved ? -s $resolved : 0 };
}
sub resolve_grant {
my ($self, $id) = @_;
die "Unknown or expired file selection; select the file again\n"
unless defined($id) && !ref($id) && exists $self->{grants}{$id};
my $file = $self->{grants}{$id};
die "Selected location changed or is no longer available\n"
unless !-l $file && -e $file && _same_path(abs_path($file), $file);
return $file;
}
sub submit {
my ($self, $request) = @_;
die "The job service has stopped\n" if $self->{stopped};
die "Request must be an object\n" unless ref($request) eq 'HASH';
my %allowed = map { $_ => 1 } qw(conversion input output options destination);
die "Unknown job field\n" if grep { !$allowed{$_} } keys %$request;
die "The conversion queue is full\n" if @{$self->{queue}} >= 16;
my $conversion = $request->{conversion};
my ($route) = grep { defined($conversion) && !ref($conversion) && $_->{id} eq $conversion } @{catalog()->{data}};
die "Unknown conversion\n" unless $route;
die "$route->{unavailableReason}\n" unless $route->{available};
my $input = $request->{input};
die "Provide JSON data or selected input files\n" unless ref($input) eq 'HASH'
&& keys(%$input) == 1 && (exists $input->{data} || ref($input->{files}) eq 'HASH');
my %files;
for my $role (keys %{$input->{files} || {}}) {
die "Selected files must be an array\n" unless ref($input->{files}{$role}) eq 'ARRAY';
$files{$role} = [map {
my $file = $self->resolve_grant($_);
+{path => $file, filename => path($file)->basename};
} @{$input->{files}{$role}}];
}
my $destination = exists $request->{destination} ? $self->resolve_grant($request->{destination}) : undef;
die "Output destination must be a directory\n" if defined($destination) && !-d $destination;
my $id = _id();
my $dir = path($self->{root}, $id);
$dir->mkpath({mode => 0700});
( run in 0.541 second using v1.01-cache-2.11-cpan-145b269f2f3 )