fix(engine): don't reset started_at on poll so step deadlines accumulate

RunRunner::onPoll no longer resets started_at, so a poll step's own deadline
(WaitForGuestAgent 270s, ConfigureDnsAndTls cert 840s) actually fires instead
of resetting every poll. maxDuration raised above each own-deadline so the
step's fail wins over the generic timeout. Shared core fix (A + B).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 12:15:26 +02:00
parent fffcecb152
commit c56aa14f9d
4 changed files with 21 additions and 4 deletions

View File

@ -147,12 +147,15 @@ class RunRunner
$this->record($run, $step->key(), 'retry', $result->reason);
}
/** Poll: wait and re-run without consuming the retry budget (step owns its deadline). */
/**
* Poll: wait and re-run without consuming the retry budget. started_at is NOT
* reset here, so a step's own deadline (measured from started_at) actually
* accumulates across polls instead of resetting every time.
*/
private function onPoll(ProvisioningRun $run, ProvisioningStep $step, StepResult $result): void
{
$run->status = ProvisioningRun::STATUS_WAITING;
$run->next_attempt_at = now()->addSeconds($result->afterSeconds);
$run->started_at = now();
$run->save();
$this->record($run, $step->key(), 'info', $result->reason);
}

View File

@ -21,7 +21,8 @@ class ConfigureDnsAndTls extends CustomerStep
public function maxDuration(): int
{
return 900;
// Above the 840s cert own-deadline below, so the step's own fail fires first.
return 960;
}
public function execute(ProvisioningRun $run): StepResult

View File

@ -17,7 +17,8 @@ class WaitForGuestAgent extends CustomerStep
public function maxDuration(): int
{
return 300;
// Above the 270s own-deadline below, so the step's own fail fires first.
return 360;
}
public function execute(ProvisioningRun $run): StepResult

View File

@ -93,6 +93,18 @@ it('polls without consuming the retry budget', function () {
expect($run->fresh()->status)->toBe('waiting')->and($run->fresh()->attempt)->toBe(0);
});
it('keeps started_at stable across polls so step deadlines can accumulate', function () {
bindPipeline(['test' => [FakePollStep::class]]);
$run = ProvisioningRun::factory()->create([
'pipeline' => 'test', 'status' => 'running', 'started_at' => now()->subMinutes(5),
]);
$original = $run->started_at->timestamp;
app(RunRunner::class)->advance($run);
expect($run->fresh()->started_at->timestamp)->toBe($original);
});
it('marks a Host subject as error when the run fails', function () {
bindPipeline(['test' => [FakeFailStep::class]]);
$host = Host::factory()->create(['status' => 'onboarding']);