feat: configurable stop grace period for applications
Adds stop_grace_period to application settings (seconds, 1-3600, default 30). Used in place of the hardcoded docker stop -t 30 in the four places that stop application containers: rolling update shutdown, manual stop, stop on another server, and preview deployment stop. Non-positive values fall back to the default via ($val > 0) ? $val : default, with tests covering 0 and -10 so the cast does not blow up if a bad value ever lands in the db. Picks up Jack Coy's work from #7125 which went dormant. His commits are squashed here with credit below. Co-authored-by: Jack Coy <jackman3000@gmail.com>
This commit is contained in:
parent
37518813a6
commit
60d8aba323
10 changed files with 152 additions and 7 deletions
|
|
@ -36,10 +36,13 @@ public function handle(Application $application, bool $previewDeployments = fals
|
|||
: getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
|
||||
$containersToStop = $containers->pluck('Names')->toArray();
|
||||
$timeout = ($application->settings->stop_grace_period > 0)
|
||||
? $application->settings->stop_grace_period
|
||||
: DEFAULT_STOP_GRACE_PERIOD_SECONDS;
|
||||
|
||||
foreach ($containersToStop as $containerName) {
|
||||
instant_remote_process(command: [
|
||||
"docker stop -t 30 $containerName",
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,17 @@ public function handle(Application $application, Server $server)
|
|||
}
|
||||
try {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
$timeout = ($application->settings->stop_grace_period > 0)
|
||||
? $application->settings->stop_grace_period
|
||||
: DEFAULT_STOP_GRACE_PERIOD_SECONDS;
|
||||
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
[
|
||||
"docker stop -t 30 $containerName",
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
],
|
||||
$server
|
||||
|
|
|
|||
|
|
@ -3310,14 +3310,21 @@ private function build_image()
|
|||
private function graceful_shutdown_container(string $containerName, bool $skipRemove = false)
|
||||
{
|
||||
try {
|
||||
$timeout = isDev() ? 1 : 30;
|
||||
if (isDev()) {
|
||||
$timeout = 1;
|
||||
} else {
|
||||
$timeout = ($this->application->settings->stop_grace_period > 0)
|
||||
? $this->application->settings->stop_grace_period
|
||||
: DEFAULT_STOP_GRACE_PERIOD_SECONDS;
|
||||
}
|
||||
|
||||
if ($skipRemove) {
|
||||
$this->execute_remote_command(
|
||||
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ class Advanced extends Component
|
|||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gpuOptions = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $stopGracePeriod = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isBuildServerEnabled = false;
|
||||
|
||||
|
|
@ -145,6 +148,10 @@ public function syncData(bool $toModel = false)
|
|||
$this->injectBuildArgsToDockerfile = $this->application->settings->inject_build_args_to_dockerfile ?? true;
|
||||
$this->includeSourceCommitInBuild = $this->application->settings->include_source_commit_in_build ?? false;
|
||||
}
|
||||
|
||||
// Load stop_grace_period separately since it has its own save handler
|
||||
// Convert null to empty string to prevent dirty detection issues
|
||||
$this->stopGracePeriod = $this->application->settings->stop_grace_period ?? '';
|
||||
}
|
||||
|
||||
private function resetDefaultLabels()
|
||||
|
|
@ -252,6 +259,34 @@ public function saveCustomName()
|
|||
}
|
||||
}
|
||||
|
||||
public function saveStopGracePeriod()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
// Convert empty string to null, otherwise cast to integer
|
||||
$value = ($this->stopGracePeriod === '' || $this->stopGracePeriod === null)
|
||||
? null
|
||||
: (int) $this->stopGracePeriod;
|
||||
|
||||
// Validate the integer value
|
||||
if ($value !== null && ($value < 1 || $value > 3600)) {
|
||||
$this->dispatch('error', 'Stop grace period must be between 1 and 3600 seconds.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to model
|
||||
$this->application->settings->stop_grace_period = $value;
|
||||
$this->application->settings->save();
|
||||
|
||||
// User feedback
|
||||
$this->dispatch('success', 'Stop grace period updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.advanced');
|
||||
|
|
|
|||
|
|
@ -338,10 +338,13 @@ public function addDockerImagePreview()
|
|||
private function stopContainers(array $containers, $server)
|
||||
{
|
||||
$containersToStop = collect($containers)->pluck('Names')->toArray();
|
||||
$timeout = ($this->application->settings->stop_grace_period > 0)
|
||||
? $this->application->settings->stop_grace_period
|
||||
: DEFAULT_STOP_GRACE_PERIOD_SECONDS;
|
||||
|
||||
foreach ($containersToStop as $containerName) {
|
||||
instant_remote_process(command: [
|
||||
"docker stop -t 30 $containerName",
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ApplicationSetting extends Model
|
|||
'is_git_lfs_enabled' => 'boolean',
|
||||
'is_git_shallow_clone_enabled' => 'boolean',
|
||||
'docker_images_to_keep' => 'integer',
|
||||
'stop_grace_period' => 'integer',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
'@yearly' => '0 0 1 1 *',
|
||||
];
|
||||
const RESTART_MODE = 'unless-stopped';
|
||||
const DEFAULT_STOP_GRACE_PERIOD_SECONDS = 30;
|
||||
|
||||
const DATABASE_DOCKER_IMAGES = [
|
||||
'bitnami/mariadb',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->integer('stop_grace_period')
|
||||
->nullable()
|
||||
->after('use_build_secrets')
|
||||
->comment('Seconds to wait for graceful shutdown before forcing container stop (1-3600). Null uses default of 30 seconds.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('stop_grace_period');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -86,7 +86,21 @@
|
|||
helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled
|
||||
instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" />
|
||||
@endif
|
||||
|
||||
<h3 class="pt-4">Operations</h3>
|
||||
<form class="flex items-end gap-2" wire:submit.prevent='saveStopGracePeriod'>
|
||||
<x-forms.input
|
||||
type="number"
|
||||
id="stopGracePeriod"
|
||||
label="Stop Grace Period (seconds)"
|
||||
placeholder="{{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }}"
|
||||
helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: 1-3600 seconds (1 hour)."
|
||||
min="1"
|
||||
max="3600"
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
/>
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
<h3 class="pt-4">Logs</h3>
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave id="isLogDrainEnabled" label="Drain Logs" canGate="update" :canResource="$application" />
|
||||
|
|
|
|||
|
|
@ -103,3 +103,49 @@
|
|||
->and($casts[$field])->toBe('boolean');
|
||||
}
|
||||
});
|
||||
|
||||
it('casts stop_grace_period to integer', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$casts = $setting->getCasts();
|
||||
|
||||
expect($casts)->toHaveKey('stop_grace_period')
|
||||
->and($casts['stop_grace_period'])->toBe('integer');
|
||||
});
|
||||
|
||||
it('handles null stop_grace_period for default behavior', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = null;
|
||||
|
||||
expect($setting->stop_grace_period)->toBeNull();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period from string to integer', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = '60';
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(60)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period zero to integer (documents fallback trigger)', function () {
|
||||
// Value of 0 is not a valid grace period — consumers guard with
|
||||
// `($value > 0) ? $value : DEFAULT_STOP_GRACE_PERIOD_SECONDS`, so
|
||||
// the cast itself must still round-trip cleanly without throwing.
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = 0;
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(0)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period negative value to integer (documents fallback trigger)', function () {
|
||||
// Negative values should never be persisted (UI validates `min=1`),
|
||||
// but if one slips through (direct DB write, older data), the cast
|
||||
// must not throw and consumers will treat it as the fallback via the
|
||||
// `> 0` guard.
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = -10;
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(-10)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue