diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 6ebdf255ae..5994f187de 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -177,6 +177,19 @@ class ApplicationDeploymentQueue extends Model return getJobStatus($this->horizon_job_id); } + /** + * Horizon drops its job record 'trim.pending' minutes after the push, also while the job runs, + * so a missing record counts as running only until the deployment timeout has passed. + */ + public function isHorizonJobActive(): bool + { + return match ($this->getHorizonJobStatus()) { + 'reserved' => true, + 'unknown' => $this->updated_at?->gt(now()->subSeconds($this->server?->settings?->dynamic_timeout ?? 3600)) ?? false, + default => false, + }; + } + public function commitMessage() { if (empty($this->commit_message) || is_null($this->commit_message)) { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2e9fdbb253..81f2dbe726 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2623,8 +2623,7 @@ function isAnyDeploymentInprogress(bool $showAll = false) $deploymentDetails = []; foreach ($runningJobs as $runningJob) { - $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); - if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') { + if ($runningJob->isHorizonJobActive()) { $horizonJobIds[] = $runningJob->horizon_job_id; if ($showAll) { diff --git a/config/constants.php b/config/constants.php index c25c29ca9b..8c71e262dd 100644 --- a/config/constants.php +++ b/config/constants.php @@ -5,7 +5,7 @@ $sshCommandTimeout = (int) env('SSH_COMMAND_TIMEOUT', 3600); return [ 'coolify' => [ - 'version' => env('COOLIFY_VERSION') ?: '4.4.0', + 'version' => env('COOLIFY_VERSION') ?: '4.4.1', 'helper_version' => '1.0.17', 'railpack_version' => '0.23.0', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/config/database.php b/config/database.php index 9238a70551..07039d6d62 100644 --- a/config/database.php +++ b/config/database.php @@ -33,6 +33,8 @@ $pgsql = [ 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', + // Laravel writes dates without an offset, so timestampTz columns need a UTC session. + 'timezone' => 'UTC', 'sslmode' => 'prefer', 'options' => [ (defined('Pdo\Pgsql::ATTR_DISABLE_PREPARES') ? Pgsql::ATTR_DISABLE_PREPARES : PDO::PGSQL_ATTR_DISABLE_PREPARES) => env('DB_DISABLE_PREPARES', false), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 81016eeeb4..bd38eada4e 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "main" + "version": "4.4.1" }, "nightly": { "version": "4.5-rc.1" diff --git a/tests/Feature/DeploymentHorizonJobActiveTest.php b/tests/Feature/DeploymentHorizonJobActiveTest.php new file mode 100644 index 0000000000..07da481f90 --- /dev/null +++ b/tests/Feature/DeploymentHorizonJobActiveTest.php @@ -0,0 +1,59 @@ +create(); + $privateKey = PrivateKey::factory()->create(['team_id' => $team->id]); + $this->server = Server::factory()->create(['team_id' => $team->id, 'private_key_id' => $privateKey->id]); + $this->server->settings->update(['dynamic_timeout' => 3600]); + + $this->deployment = ApplicationDeploymentQueue::create([ + 'application_id' => '1', + 'deployment_uuid' => 'horizon-active-uuid', + 'server_id' => $this->server->id, + 'horizon_job_id' => 'job-1', + ]); +}); + +function fakeHorizonJobStatus(?string $status): void +{ + $jobs = $status === null ? collect() : collect([(object) ['id' => 'job-1', 'status' => $status]]); + + $repository = Mockery::mock(JobRepository::class); + $repository->shouldReceive('getJobs')->andReturn($jobs); + app()->instance(JobRepository::class, $repository); +} + +it('counts a reserved job as active', function () { + fakeHorizonJobStatus('reserved'); + + expect($this->deployment->isHorizonJobActive())->toBeTrue(); +}); + +it('does not count a finished job as active', function (string $status) { + fakeHorizonJobStatus($status); + + expect($this->deployment->isHorizonJobActive())->toBeFalse(); +})->with(['completed', 'failed']); + +it('counts a job without a Horizon record as active within the deployment timeout', function () { + fakeHorizonJobStatus(null); + $this->travel(59)->minutes(); + + expect($this->deployment->isHorizonJobActive())->toBeTrue(); +}); + +it('does not count a job without a Horizon record as active after the deployment timeout', function () { + fakeHorizonJobStatus(null); + $this->travel(61)->minutes(); + + expect($this->deployment->isHorizonJobActive())->toBeFalse(); +}); diff --git a/versions.json b/versions.json index d4a76777b3..bd38eada4e 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.4.0" + "version": "4.4.1" }, "nightly": { "version": "4.5-rc.1"