App-karr
view release on metacpan or search on metacpan
lib/App/karr/Foundation/ChainStore.pm view on Meta::CPAN
sub _new_chain_id {
return strftime( '%Y%m%dT%H%M%SZ', gmtime() ) . '-'
. substr( md5_hex( $$, time, rand ), 0, 6 );
}
sub header {
my ( $self ) = @_;
return $self->_read_yaml( CHAIN_META, 'chain header' ) // {};
}
sub steps {
my ( $self ) = @_;
my @steps;
for my $ref ( $self->git->list_refs(STEP_ROOT) ) {
my $id = substr $ref, length STEP_ROOT;
next unless _valid_id($id);
my $step = $self->_read_yaml( $ref, "chain step '$id'" ) or next;
push @steps, _as_step( $step, $id );
}
# Assigned before it is returned: sort() in scalar context is undefined
# behaviour, and a caller asking how many steps there are is ordinary.
my @sorted = sort { _id_sort_key( $a->{id} ) cmp _id_sort_key( $b->{id} ) } @steps;
return @sorted;
}
# What a step ref holds, as the rest of this class expects to see it: the id
# filled in from the ref name it was found under, and needs always a list. A
# ref written by hand may spell a one-element needs as a scalar, and every
# reader here dereferences it -- normalising once on the way in is cheaper than
# guarding at each of them.
sub _as_step {
my ( $step, $id ) = @_;
$step->{id} //= "$id";
my $needs = $step->{needs};
$step->{needs} = ref $needs eq 'ARRAY' ? $needs
: ( defined $needs && length "$needs" ? [ $needs ] : [] );
return $step;
}
# A total order over ids that are usually numbers and occasionally not: pad
# the numbers so they sort among themselves numerically, and leave names to
# sort after them (digits sort before letters).
sub _id_sort_key {
my ( $id ) = @_;
$id = defined $id ? "$id" : '';
return $id =~ /\A[0-9]+\z/ ? sprintf( '%020d', $id ) : "~$id";
}
sub step {
my ( $self, $id ) = @_;
return undef unless _valid_id($id);
my $step = $self->_read_yaml( $self->_step_ref($id), "chain step '$id'" )
or return undef;
return _as_step( $step, $id );
}
sub update_step {
my ( $self, $id, $code ) = @_;
return undef unless _valid_id($id);
my $ref = $self->_step_ref($id);
# retry_contended reads an empty return as "lost the race, read again"; a
# one-element (undef) return is a committed answer and comes back as undef.
return scalar $self->git->retry_contended( "chain step $id", sub {
my ( $oid, $content ) = $self->git->read_ref_with_oid($ref);
return undef unless defined $oid;
my $step = $self->_decode_yaml( $content, "chain step '$id'" ) or return undef;
$step = _as_step( $step, $id );
my $updated = $code->($step);
return undef unless ref $updated eq 'HASH';
my $valid = $self->_validate_step($updated);
user_error("Chain step '$id' cannot be renamed to '$valid->{id}'")
unless $valid->{id} eq "$id";
return $self->git->write_ref_cas( $ref, yaml_dump($valid), $oid )
? $valid : ();
} );
}
sub mark_stale {
my ( $self, $id, $reason ) = @_;
return $self->update_step( $id, sub {
my ( $step ) = @_;
return undef if ( $step->{state} // 'pending' ) eq 'stale';
$step->{state} = 'stale';
$step->{stale_at} = _now();
if ( defined $reason && length $reason ) { $step->{stale_reason} = $reason }
else { delete $step->{stale_reason} }
return $step;
} );
}
sub ready_steps {
my ( $self ) = @_;
my $chain = $self->header->{id};
return () unless defined $chain;
my @steps = grep { ( $_->{chain} // '' ) eq $chain } $self->steps;
my %state = map { $_->{id} => ( $_->{state} // 'pending' ) } @steps;
return grep {
my $step = $_;
( $state{ $step->{id} } eq 'pending' )
&& !grep { ( $state{$_} // '' ) ne 'done' } @{ $step->{needs} || [] };
} @steps;
}
sub clear_chain {
my ( $self ) = @_;
my $removed = $self->git->delete_ref(CHAIN_META) ? 1 : 0;
return $removed + $self->_clear_steps;
}
sub _clear_steps {
( run in 0.904 second using v1.01-cache-2.11-cpan-145b269f2f3 )