Files
coolify/app/Support/DatabaseImport/DatabaseImportCleanup.php
T
Andras BacsaiandClaude Opus 5.5 8246492bb9 fix(databases): stop and clean up interrupted or stale imports
- Imports store their cleanup data (names and paths, no credentials)
  on the activity. When Coolify fails an import after a restart or as
  stale, it queues a stop of the restore inside the database container
  (only processes of that operation and their children, bottom-up so
  the database server is not affected) and the normal cleanup.
- The cleanup runs once per import, and a stopped CoolifyTask does
  not run again when the queue retries it.
- The stop message stays on the activity: RunRemoteProcess saves the
  process id at start from the stored properties and keeps a stop
  status at the end, and CoolifyTask::failed() keeps the message.
- Regenerate the OpenAPI spec (409 for database start, restart, and
  import, plus earlier API changes that were missing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:18:17 +02:00

124 lines
3.8 KiB
PHP

<?php
namespace App\Support\DatabaseImport;
use App\Events\DatabaseImportFinished;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\Activitylog\Models\Activity;
use Throwable;
/**
* Cleanup of a database import: helper containers and temporary files on the server
* and in the database container.
*
* The normal path sends the cleanup data through the encrypted CoolifyTask payload.
* The import also stores the same data on its activity, so Coolify can stop and clean up
* an import that it marks as failed after a restart or because it is stale.
* The data has only ids, container names and paths. It must never have credentials.
*/
class DatabaseImportCleanup
{
public const PROPERTY = 'cleanup';
/**
* Set on the activity when Coolify stopped the import. A retried CoolifyTask does not run it again.
*/
public const STOP_REQUESTED_PROPERTY = 'stop_requested_at';
public const CLAIM_TTL_SECONDS = 7 * 24 * 3600;
/**
* The only keys that are stored on the activity and sent to the cleanup listener.
*/
private const KEYS = [
'container',
'containerTmpPath',
'scriptPath',
'serverId',
'operationUuid',
'containerName',
'serverTmpPath',
'credentialTmpPath',
];
/**
* @param array<string, mixed> $data
* @return array<string, int|string>
*/
public static function storable(array $data): array
{
return array_filter(
Arr::only($data, self::KEYS),
fn ($value): bool => is_string($value) || is_int($value),
);
}
/**
* The cleanup data stored on an import activity, or null if the activity has no usable data.
*
* @return array<string, int|string>|null
*/
public static function stored(Activity $activity): ?array
{
$data = data_get($activity, 'properties.'.self::PROPERTY);
if (! is_array($data)) {
return null;
}
$data = self::storable($data);
if (! is_int($data['serverId'] ?? null) || ! Str::isUuid($data['operationUuid'] ?? null)) {
return null;
}
return $data;
}
public static function stopRequested(Activity $activity): bool
{
return filled(data_get($activity, 'properties.'.self::STOP_REQUESTED_PROPERTY));
}
/**
* Queue the stop of the remote restore and the cleanup. It does not run SSH commands
* inline, so it is safe during boot. Errors are logged and do not stop the caller.
*
* @param array<string, int|string> $data
*/
public static function queueStop(array $data): void
{
try {
event(new DatabaseImportFinished([...$data, 'stopRestore' => true]));
} catch (Throwable $exception) {
Log::warning('Could not queue the stop of an interrupted database import', [
'operationUuid' => $data['operationUuid'] ?? null,
'serverId' => $data['serverId'] ?? null,
'error' => $exception->getMessage(),
]);
}
}
public static function claimKey(string $operationUuid): string
{
return "database-import-cleanup:{$operationUuid}";
}
/**
* Only the first caller for an operation gets true, so the cleanup runs at most once per import.
*/
public static function claim(string $operationUuid): bool
{
return Cache::add(self::claimKey($operationUuid), true, self::CLAIM_TTL_SECONDS);
}
/**
* Release the claim after a failed attempt, so a retry of the cleanup can run.
*/
public static function release(string $operationUuid): void
{
Cache::forget(self::claimKey($operationUuid));
}
}