From c56aa14f9ddcae0c90e08823d77bd11e0a20bbb6 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 12:15:26 +0200 Subject: [PATCH] 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 --- app/Provisioning/RunRunner.php | 7 +++++-- .../Steps/Customer/ConfigureDnsAndTls.php | 3 ++- .../Steps/Customer/WaitForGuestAgent.php | 3 ++- tests/Feature/Provisioning/RunRunnerTest.php | 12 ++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/Provisioning/RunRunner.php b/app/Provisioning/RunRunner.php index 1886bad..1adb542 100644 --- a/app/Provisioning/RunRunner.php +++ b/app/Provisioning/RunRunner.php @@ -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); } diff --git a/app/Provisioning/Steps/Customer/ConfigureDnsAndTls.php b/app/Provisioning/Steps/Customer/ConfigureDnsAndTls.php index c29ab02..a63dbf3 100644 --- a/app/Provisioning/Steps/Customer/ConfigureDnsAndTls.php +++ b/app/Provisioning/Steps/Customer/ConfigureDnsAndTls.php @@ -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 diff --git a/app/Provisioning/Steps/Customer/WaitForGuestAgent.php b/app/Provisioning/Steps/Customer/WaitForGuestAgent.php index 0cf1278..c9b01d9 100644 --- a/app/Provisioning/Steps/Customer/WaitForGuestAgent.php +++ b/app/Provisioning/Steps/Customer/WaitForGuestAgent.php @@ -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 diff --git a/tests/Feature/Provisioning/RunRunnerTest.php b/tests/Feature/Provisioning/RunRunnerTest.php index 84c1494..f3efaeb 100644 --- a/tests/Feature/Provisioning/RunRunnerTest.php +++ b/tests/Feature/Provisioning/RunRunnerTest.php @@ -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']);