mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
fix(deployments): expire missing Horizon jobs after deployment timeout
Keep reserved jobs active and bound missing Horizon records by the server's deployment timeout. Add regression coverage for active and finished jobs. Use UTC PostgreSQL sessions and bump the release version to 4.4.1.
This commit is contained in:
1 parent
41ed5b7a69
commit
be72005980
7 files changed
+78
-5
No files matched your search
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "main"
|
||||
"version": "4.4.1"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.5-rc.1"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$team = Team::factory()->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();
|
||||
});
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.4.0"
|
||||
"version": "4.4.1"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.5-rc.1"
|
||||
|
||||
Reference in new issue
Block a user