App-karr
view release on metacpan or search on metacpan
lib/App/karr/Foundation/Executor.pm view on Meta::CPAN
};
}
# escalate_to_ai is recorded, not resolved. The coordination agent it names is
# the judgement layer, and it is called once at the end of the tick (#210) --
# never from in here, where it would run inside the resolution of one
# question and be called again for the next one. So this policy does what a
# `kind: plan` step does: planner wanted, step untouched, a line that says
# so. Whatever the agent then decides arrives as an answer in the mailbox,
# through the same door a person uses.
return {
wait => "question #$id is past its deadline and its escalate_to_ai "
. 'policy wants the coordination agent',
planner => "escalate_to_ai on question #$id",
policy => 'escalate_to_ai',
} if $policy eq 'escalate_to_ai';
# block, the default and the only policy that never invents an answer:
# waiting is not a failure of the plan, it IS the plan.
return { wait => "question #$id is past its deadline and its block policy "
. 'keeps waiting for a person' };
}
# ---------------------------------------------------------------------------
# Writing a step back
# ---------------------------------------------------------------------------
sub _finish {
my ( $self, $run, $step, $verdict ) = @_;
my $store = $self->store;
my $id = $step->{id};
my $state = $verdict->{state};
$store->update_step( $id, sub {
my ( $current ) = @_;
return undef unless ( $current->{state} // '' ) eq 'running';
$current->{state} = $state;
$current->{finished} = _now();
$current->{result} = {
at => _now(),
run => $run,
( defined $verdict->{outcome} ? ( outcome => $verdict->{outcome} ) : () ),
( defined $verdict->{exit} ? ( exit => $verdict->{exit} ) : () ),
( defined $verdict->{detail} ? ( detail => $verdict->{detail} ) : () ),
};
return $current;
} );
$store->log_run( $run, event => 'step', step => "$id", state => $state,
( defined $verdict->{detail} ? ( detail => $verdict->{detail} ) : () ) );
$self->_push;
$self->_say( $self->_describe($step) . ": $state -- "
. ( $verdict->{detail} // '' ) );
return { state => $state } unless $state eq 'failed';
# on_stall, at the seam. The policy the spec writes is `plan`, and this
# executor cannot plan -- so it records that the planner is wanted instead
# of pretending, and the dependents of this step simply never become ready
# (ready_steps releases a step only when every step it needs is done), which
# is the branch pruning itself without anybody computing a cascade.
my $policy = $step->{on_stall} // 'plan';
$store->log_run( $run, event => 'planner', step => "$id",
policy => "$policy", reason => ( $verdict->{detail} // 'the step failed' ) );
return { state => $state, planner => "on_stall: $policy" };
}
# Back to pending: nothing about the plan was learned, so nothing about the
# plan is written down. The attempt counter the claim bumped stays as it is --
# it is the record of how often this step has been tried, which is what makes
# a step that can never run visible to whoever reads the chain.
sub _requeue {
my ( $self, $run, $step, $detail ) = @_;
my $store = $self->store;
my $id = $step->{id};
$store->update_step( $id, sub {
my ( $current ) = @_;
return undef if ( $current->{state} // 'pending' ) eq 'pending';
$current->{state} = 'pending';
delete $current->{started};
$current->{result} = { at => _now(), run => $run, detail => $detail };
return $current;
} );
$store->log_run( $run, event => 'step', step => "$id", state => 'pending',
detail => $detail );
$self->_push;
$self->_say( $self->_describe($step) . ": requeued -- $detail" );
return { state => 'pending' };
}
# $summary is the half-line the tick's closing report puts in brackets after
# the step id; the reason itself, which is a sentence, goes on the step and into
# the run log. Defaulted rather than derived because the caller is the only one
# that knows which of the two stale cases this is.
sub _stale {
my ( $self, $run, $step, $reason, $summary ) = @_;
my $store = $self->store;
$store->mark_stale( $step->{id}, $reason );
$store->log_run( $run, event => 'step', step => "$step->{id}",
state => 'stale', detail => $reason );
$store->log_run( $run, event => 'planner', step => "$step->{id}",
policy => 'plan', reason => $reason );
$self->_push;
$self->_say( $self->_describe($step) . ": stale -- $reason" );
return { state => 'stale',
planner => ( $summary // 'the precheck no longer holds' ) };
}
sub _stale_reason {
my ( $self, $step, $facts ) = @_;
my $p = $self->store->parse_precheck( $step->{precheck} )
or return 'the precheck no longer holds';
my $have = exists $facts->{ $p->{fact} }
? "'" . $facts->{ $p->{fact} } . "'"
: 'not measurable here';
return "precheck '$step->{precheck}' no longer holds ($p->{fact} is $have)";
}
# ---------------------------------------------------------------------------
# Facts
lib/App/karr/Foundation/Executor.pm view on Meta::CPAN
A card can wait on a card in a B<different repository>: C<< needs:BOARD#ID >>,
the link L<App::karr::CrossBoard> puts on it (#192). C<ticket_links> is what
the chain can ask about that, in one word for the whole card:
=over 4
=item * C<settled> -- every link the card carries is in one of the B<far>
board's own terminal statuses, never a hardcoded C<done> (#67). A card carrying
B<no> link is settled too: nothing elsewhere is holding it, exactly as
C<ticket_blocked> says C<no> for a card nobody blocked. That is also what keeps
a precheck working after C<< karr needs --resolve >> has done its job and
dropped the tag -- the reading where an empty card had no answer would make the
successful resolution of the link the thing that stops the step for ever.
=item * C<open> -- a far card exists and is not finished.
=item * C<missing> -- a link names a card the far board does not have. Not
C<settled>: unblocking on the strength of a ticket nobody can find is the
silent wrong answer, which karr already declines for C<depends_on> (#123) and
declines here too (#192, decision 5). Where the card carries several links the
value is the first unsettled one in tag order, and C<settled> only when every
one of them is -- the same order in which they release the card, and the same
rule C<question_state> follows.
=back
The fact is B<absent> as soon as one link names a board this machine cannot
place: an unknown name, or a directory holding no board. #192 treats that as an
answer rather than an error and so does this -- but the answer is "not
measurable here", so the precheck does not hold, the step goes stale and the
planner hears about it. A machine holding four repositories of a six-repository
fleet must not run a step on the strength of a board it never read, and the
other machine, the one that does have it, measures the fact and runs the step.
Where the directories come from is B<not> a decision this class takes twice:
the fleet config this run already read (C<dirs:> outright, C<scan:> as
children, matched on the directory basename) is handed to
L<App::karr::CrossBoard/config_data> as it stands, so C<--config> relocates it
here as well and a second local file describing the same fleet never appears --
the argument #189 used for resolving the hub exactly once.
What this does B<not> do is resolve anything. The far board is read exactly as
it stands in that working copy -- nothing is fetched, because pulling somebody
else's repository from inside a tick is transport nobody asked for -- and the
C<blocked> flag on the near card is left alone even when every link has
settled. B<The link is the fact, C<blocked> is the decision> (#192, decision
4): somebody set it on purpose, C<< karr needs --resolve >> is what lifts it,
and an executor that lifted it unasked would be stricter than the board it
coordinates -- the line C<Picker> holds by not filtering (#185) and C<pick> by
handing the card over with a warning (#123). C<verified>, the back-reference
half of the link, is not part of the fact either: C<< --resolve >> settles on
C<settled> alone, and a second opinion here would mean a far card whose author
forgot the C<< escalated-from: >> tag could never settle anything.
=head2 What a failure does to the DAG
Nothing, and that is the design rather than an omission.
L<App::karr::Foundation::ChainStore/ready_steps> releases a step only when every
step it C<needs> is C<done>, so a step that ends C<failed> or C<stale> stops its
own branch B<by construction>:
its dependents never become ready, no cascade has to be computed, and every
branch that does not run through it carries on. The chain then cannot finish,
and that unreachability is exactly the signal C<on_stall: plan> names.
The planner itself does not run in here. Where the spec says "call the planner"
this executor B<records that the planner is wanted> -- a C<planner> entry in the
run log naming the step and why, and a line of output at the end of the tick --
and writes nothing about the plan that a planner would have to undo. What has
changed with #210 is only who hears it: the recorded entries are handed to
L<App::karr::Foundation::Coordinator> when the tick is over, and where the fleet
marks an agent C<< role: coordinator >> F<karr-foundation> calls it B<once> for
all of them. Where it marks none, this is exactly what it was -- a line of
output for the operator, who is then the planner.
Three outcomes are deliberately B<not> failures, because none of them is a
statement about the plan:
=over 4
=item * A B<common error> (a rate-limited or broken agent command) requeues the
step to C<pending>. The board's own cooldown and the agent's availability record
have already been written by the drain; the step is simply not this machine's
to run right now.
=item * A B<skipped board> -- disabled, locked by another tick, in cooldown, or
on an agent that is currently failing -- requeues the step the same way and says
which of those it was.
=item * A step naming a B<repository this machine does not have> is left
untouched and unclaimed. The chain is shared and the machines are not, so this
is the ordinary case in a fleet, not a broken plan.
=back
=head2 A question step resolves a question, it does not ask one
A C<kind: question> step waits on the mailbox
(L<App::karr::Foundation::Questions>): the planner asks the question first, with
C<karr-foundation ask --step ID>, and the step does nothing but resolve it. A
step does B<not> ask its own question, and that is a decision about schemas
rather than about convenience -- a self-asking step would have to carry the
question text, its C<options>, its C<policy>, its C<default> and its C<deadline>
in the step itself, which is the mailbox's schema written out a second time and
kept in step with the first one by hand.
The consequence is that a B<ready question step nothing in the mailbox names is
a planning error>, and it is reported as one: C<stale>, with the reason in the
run log and on the tick's output, rather than left waiting quietly for a
question that is never going to arrive. Same answer, same cost and same reason
as a C<kind: ticket> step whose card is not on the board.
What a step that does have its question then does is
L<App::karr::Foundation::Questions/resolve> plus the policy the asker wrote
down for the case where nobody answers:
answered done, and the answer is in the run log
open pending and unclaimed; its dependents wait
overdue + block pending: waiting IS what block means
overdue + use_default done, with the default as the answer
overdue + escalate_to_ai pending, and the planner recorded as wanted
( run in 0.583 second using v1.01-cache-2.11-cpan-e7c6538aa59 )