Fix database status flickering and add restart tracking (#7665)

This commit is contained in:
Andras Bacsai 2025-12-17 17:48:15 +01:00 committed by GitHub
commit 1b3be5bef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 161 additions and 17 deletions

View file

@ -199,12 +199,26 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
// Track restart count for databases (single-container)
$restartCount = data_get($container, 'RestartCount', 0);
$previousRestartCount = $database->restart_count ?? 0;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
$updateData = ['status' => $containerStatus];
} else {
$database->update(['last_online_at' => now()]);
$updateData = ['last_online_at' => now()];
}
// Update restart tracking if restart count increased
if ($restartCount > $previousRestartCount) {
$updateData['restart_count'] = $restartCount;
$updateData['last_restart_at'] = now();
$updateData['last_restart_type'] = 'crash';
}
$database->update($updateData);
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
@ -365,7 +379,13 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
if (str($database->status)->startsWith('exited')) {
continue;
}
$database->update(['status' => 'exited']);
// Reset restart tracking when database exits completely
$database->update([
'status' => 'exited',
'restart_count' => 0,
'last_restart_at' => null,
'last_restart_type' => null,
]);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');

View file

@ -237,8 +237,9 @@ public function handle()
$this->foundProxy = true;
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
} else {
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
if ($this->allDatabaseUuids->contains($uuid) && $this->isActiveOrTransient($containerStatus)) {
$this->foundDatabaseUuids->push($uuid);
// TCP proxy should only be started/managed when database is actually running
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
} else {
@ -503,20 +504,28 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
private function updateNotFoundDatabaseStatus()
{
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
if ($notFoundDatabaseUuids->isNotEmpty()) {
$notFoundDatabaseUuids->each(function ($databaseUuid) {
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
}
});
if ($notFoundDatabaseUuids->isEmpty()) {
return;
}
// Only protection: Verify we received any container data at all
// If containers collection is completely empty, Sentinel might have failed
if ($this->containers->isEmpty()) {
return;
}
$notFoundDatabaseUuids->each(function ($databaseUuid) {
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
if (! str($database->status)->startsWith('exited')) {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
}
});
}
private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
@ -576,6 +585,23 @@ private function isRunning(string $containerStatus)
return str($containerStatus)->contains('running');
}
/**
* Check if container is in an active or transient state.
* Active states: running
* Transient states: restarting, starting, created, paused
*
* These states indicate the container exists and should be tracked.
* Terminal states (exited, dead, removing) should NOT be tracked.
*/
private function isActiveOrTransient(string $containerStatus): bool
{
return str($containerStatus)->contains('running') ||
str($containerStatus)->contains('restarting') ||
str($containerStatus)->contains('starting') ||
str($containerStatus)->contains('created') ||
str($containerStatus)->contains('paused');
}
private function checkLogDrainContainer()
{
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {

View file

@ -18,6 +18,9 @@ class StandaloneClickhouse extends BaseModel
protected $casts = [
'clickhouse_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()
@ -247,6 +250,7 @@ protected function internalDbUrl(): Attribute
$encodedUser = rawurlencode($this->clickhouse_admin_user);
$encodedPass = rawurlencode($this->clickhouse_admin_password);
$database = $this->clickhouse_db ?? 'default';
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$database}";
},
);
@ -264,6 +268,7 @@ protected function externalDbUrl(): Attribute
$encodedUser = rawurlencode($this->clickhouse_admin_user);
$encodedPass = rawurlencode($this->clickhouse_admin_password);
$database = $this->clickhouse_db ?? 'default';
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$database}";
}

View file

@ -18,6 +18,9 @@ class StandaloneDragonfly extends BaseModel
protected $casts = [
'dragonfly_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()

View file

@ -18,6 +18,9 @@ class StandaloneKeydb extends BaseModel
protected $casts = [
'keydb_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()

View file

@ -19,6 +19,9 @@ class StandaloneMariadb extends BaseModel
protected $casts = [
'mariadb_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()

View file

@ -16,6 +16,12 @@ class StandaloneMongodb extends BaseModel
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()
{
static::created(function ($database) {

View file

@ -19,6 +19,9 @@ class StandaloneMysql extends BaseModel
protected $casts = [
'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()

View file

@ -19,6 +19,9 @@ class StandalonePostgresql extends BaseModel
protected $casts = [
'init_scripts' => 'array',
'postgres_password' => 'encrypted',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()

View file

@ -16,6 +16,12 @@ class StandaloneRedis extends BaseModel
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
'last_restart_type' => 'string',
];
protected static function booted()
{
static::created(function ($database) {

View file

@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* The standalone database tables to add restart tracking columns to.
*/
private array $tables = [
'standalone_postgresqls',
'standalone_mysqls',
'standalone_mariadbs',
'standalone_redis',
'standalone_mongodbs',
'standalone_keydbs',
'standalone_dragonflies',
'standalone_clickhouses',
];
/**
* Run the migrations.
*/
public function up(): void
{
foreach ($this->tables as $table) {
if (! Schema::hasColumn($table, 'restart_count')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->integer('restart_count')->default(0)->after('status');
});
}
if (! Schema::hasColumn($table, 'last_restart_at')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->timestamp('last_restart_at')->nullable()->after('restart_count');
});
}
if (! Schema::hasColumn($table, 'last_restart_type')) {
Schema::table($table, function (Blueprint $blueprint) {
$blueprint->string('last_restart_type', 10)->nullable()->after('last_restart_at');
});
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
foreach ($this->tables as $table) {
foreach ($columns as $column) {
if (Schema::hasColumn($table, $column)) {
Schema::table($table, function (Blueprint $blueprint) use ($column) {
$blueprint->dropColumn($column);
});
}
}
}
}
};