Fix standalone database "restarting" status flickering and add restart tracking
- Fix status flickering: Track databases in active/transient states (restarting, starting, created, paused) not just running - Add isActiveOrTransient() helper to distinguish between active states and terminal states (exited, dead) - Add safeguard: Protect updateNotFoundDatabaseStatus() from marking as exited when containers collection is empty - Add restart_count tracking: New migration adds restart_count, last_restart_at, last_restart_type to all standalone database tables - Update 8 database models with $casts for new restart tracking fields - Update GetContainersStatus to extract RestartCount from Docker and update database models - Reset restart tracking when database exits completely 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
20c6f61858
commit
6d47d24169
13 changed files with 194 additions and 17 deletions
|
|
@ -199,10 +199,33 @@ 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];
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
$database->update(['last_online_at' => now()]);
|
||||
$updateData = ['last_online_at' => now()];
|
||||
|
||||
// Update restart tracking even if status unchanged
|
||||
if ($restartCount > $previousRestartCount) {
|
||||
$updateData['restart_count'] = $restartCount;
|
||||
$updateData['last_restart_at'] = now();
|
||||
$updateData['last_restart_type'] = 'crash';
|
||||
}
|
||||
|
||||
$database->update($updateData);
|
||||
}
|
||||
|
||||
if ($isPublic) {
|
||||
|
|
@ -365,7 +388,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');
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class StandaloneClickhouse extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'clickhouse_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
@ -247,6 +249,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 +267,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}";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class StandaloneDragonfly extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'dragonfly_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class StandaloneKeydb extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'keydb_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class StandaloneMariadb extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'mariadb_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ 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',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class StandaloneMysql extends BaseModel
|
|||
protected $casts = [
|
||||
'mysql_password' => 'encrypted',
|
||||
'mysql_root_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class StandalonePostgresql extends BaseModel
|
|||
protected $casts = [
|
||||
'init_scripts' => 'array',
|
||||
'postgres_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ 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',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -3953,6 +3953,22 @@
|
|||
"logo": "svgs/default.webp",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"soju": {
|
||||
"documentation": "https://soju.im/?utm_source=coolify.io",
|
||||
"slogan": "A user-friendly IRC bouncer with a modern web interface",
|
||||
"compose": "c2VydmljZXM6CiAgc29qdToKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2VtZXJzaW9uL3NvanU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc29qdS1kYjovZGInCiAgICAgIC0gJ3NvanUtdXBsb2FkczovdXBsb2FkcycKICAgICAgLSAnc29qdS1ydW46L3J1bi9zb2p1JwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zb2p1L2NvbmZpZwogICAgICAgIHRhcmdldDogL3NvanUtY29uZmlnCiAgICAgICAgY29udGVudDogImRiIHNxbGl0ZTMgL2RiL21haW4uZGJcbm1lc3NhZ2Utc3RvcmUgZGJcbmZpbGUtdXBsb2FkIGZzIC91cGxvYWRzL1xubGlzdGVuIGlyYytpbnNlY3VyZTovLzAuMC4wLjA6NjY2N1xubGlzdGVuIHdzK2luc2VjdXJlOi8vMC4wLjAuMDo4MFxubGlzdGVuIHVuaXgrYWRtaW46Ly8vcnVuL3NvanUvYWRtaW5cbiIKICAgIG5ldHdvcmtzOgogICAgICBkZWZhdWx0OgogICAgICAgIGFsaWFzZXM6CiAgICAgICAgICAtIGdhbWphLWJhY2tlbmQKICBnYW1qYToKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2VtZXJzaW9uL2dhbWphOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HQU1KQV84MAogICAgZGVwZW5kc19vbjoKICAgICAgLSBzb2p1CnZvbHVtZXM6CiAgc29qdS1kYjogbnVsbAogIHNvanUtdXBsb2FkczogbnVsbAogIHNvanUtcnVuOiBudWxsCg==",
|
||||
"tags": [
|
||||
"irc",
|
||||
"bouncer",
|
||||
"chat",
|
||||
"messaging",
|
||||
"relay"
|
||||
],
|
||||
"category": "communication",
|
||||
"logo": "svgs/soju.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "80"
|
||||
},
|
||||
"soketi": {
|
||||
"documentation": "https://docs.soketi.app?utm_source=coolify.io",
|
||||
"slogan": "Soketi is your simple, fast, and resilient open-source WebSockets server.",
|
||||
|
|
|
|||
|
|
@ -3953,6 +3953,22 @@
|
|||
"logo": "svgs/default.webp",
|
||||
"minversion": "0.0.0"
|
||||
},
|
||||
"soju": {
|
||||
"documentation": "https://soju.im/?utm_source=coolify.io",
|
||||
"slogan": "A user-friendly IRC bouncer with a modern web interface",
|
||||
"compose": "c2VydmljZXM6CiAgc29qdToKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2VtZXJzaW9uL3NvanU6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnc29qdS1kYjovZGInCiAgICAgIC0gJ3NvanUtdXBsb2FkczovdXBsb2FkcycKICAgICAgLSAnc29qdS1ydW46L3J1bi9zb2p1JwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zb2p1L2NvbmZpZwogICAgICAgIHRhcmdldDogL3NvanUtY29uZmlnCiAgICAgICAgY29udGVudDogImRiIHNxbGl0ZTMgL2RiL21haW4uZGJcbm1lc3NhZ2Utc3RvcmUgZGJcbmZpbGUtdXBsb2FkIGZzIC91cGxvYWRzL1xubGlzdGVuIGlyYytpbnNlY3VyZTovLzAuMC4wLjA6NjY2N1xubGlzdGVuIHdzK2luc2VjdXJlOi8vMC4wLjAuMDo4MFxubGlzdGVuIHVuaXgrYWRtaW46Ly8vcnVuL3NvanUvYWRtaW5cbiIKICAgIG5ldHdvcmtzOgogICAgICBkZWZhdWx0OgogICAgICAgIGFsaWFzZXM6CiAgICAgICAgICAtIGdhbWphLWJhY2tlbmQKICBnYW1qYToKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2VtZXJzaW9uL2dhbWphOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HQU1KQV84MAogICAgZGVwZW5kc19vbjoKICAgICAgLSBzb2p1CnZvbHVtZXM6CiAgc29qdS1kYjogbnVsbAogIHNvanUtdXBsb2FkczogbnVsbAogIHNvanUtcnVuOiBudWxsCg==",
|
||||
"tags": [
|
||||
"irc",
|
||||
"bouncer",
|
||||
"chat",
|
||||
"messaging",
|
||||
"relay"
|
||||
],
|
||||
"category": "communication",
|
||||
"logo": "svgs/soju.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "80"
|
||||
},
|
||||
"soketi": {
|
||||
"documentation": "https://docs.soketi.app?utm_source=coolify.io",
|
||||
"slogan": "Soketi is your simple, fast, and resilient open-source WebSockets server.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue