242 lines
7.4 KiB
PHP
242 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/**
|
|
* Stato di avanzamento delle operazioni lunghe di backup, ripristino e
|
|
* azzeramento.
|
|
*
|
|
* Le tre operazioni girano in coda e la pagina le segue interrogando questo
|
|
* stato: `total` e `processed` contano unita' di lavoro davvero completate
|
|
* (file scritti nell'archivio, file estratti, tabelle svuotate), non un
|
|
* temporizzatore. La barra si muove quando qualcosa e' finito sul disco.
|
|
*
|
|
* Stessa impostazione di TerritorioPdfImportState, con cui condivide anche il
|
|
* TTL: lo stato sopravvive un giorno, quindi ricaricare la pagina dopo un
|
|
* ripristino lungo mostra ancora l'esito.
|
|
*/
|
|
class BackupOperationState
|
|
{
|
|
public const TIPO_BACKUP = 'backup';
|
|
public const TIPO_RESTORE = 'restore';
|
|
public const TIPO_RESET = 'reset';
|
|
|
|
/** L'archivio e' pronto ma l'azzeramento aspetta che venga scaricato. */
|
|
public const FASE_ATTESA_DOWNLOAD = 'attesa-download';
|
|
|
|
protected int $ttlSeconds = 86400;
|
|
|
|
/** Coda dei log: solo le ultime righe, lo stato viaggia a ogni poll. */
|
|
private const MAX_LOG = 60;
|
|
|
|
/**
|
|
* Puntatore all'unica operazione ammessa alla volta.
|
|
*
|
|
* Backup, ripristino e azzeramento lavorano tutti sullo stesso database e
|
|
* sullo stesso disco: due in parallelo — due schede aperte, due
|
|
* amministratori — non si limitano a rallentarsi, si sovrascrivono. Un
|
|
* ripristino che riscrive le tabelle mentre un azzeramento le cancella
|
|
* lascia uno stato che nessuno dei due voleva.
|
|
*
|
|
* Il TTL e' la valvola di sicurezza: se il worker muore senza chiudere
|
|
* l'operazione, il blocco si scioglie da solo entro un'ora invece di
|
|
* richiedere un intervento sul server. Prima ancora, corrente() considera
|
|
* libero il posto se l'operazione puntata non esiste piu' o e' terminata.
|
|
*/
|
|
private const CHIAVE_CORRENTE = 'backup-op:corrente';
|
|
private const TTL_CORRENTE = 3600;
|
|
|
|
public function initialize(string $id, string $tipo, string $fase, int $total = 0): array
|
|
{
|
|
$state = [
|
|
'id' => $id,
|
|
'tipo' => $tipo,
|
|
'status' => 'queued',
|
|
'fase' => $fase,
|
|
'total' => $total,
|
|
'processed' => 0,
|
|
'logs' => [],
|
|
'result' => [],
|
|
'error' => null,
|
|
'started_at' => null,
|
|
'finished_at' => null,
|
|
];
|
|
|
|
$this->put($id, $state);
|
|
Cache::put(self::CHIAVE_CORRENTE, $id, self::TTL_CORRENTE);
|
|
|
|
return $state;
|
|
}
|
|
|
|
/**
|
|
* Id dell'operazione in corso, o null se non ce n'e' nessuna.
|
|
*
|
|
* Serve a due cose: rifiutare l'avvio di una seconda operazione, e
|
|
* riagganciare la barra di avanzamento quando la pagina viene ricaricata —
|
|
* senza, un azzeramento fermo in attesa di conferma resterebbe irraggiungibile
|
|
* e nessuno potrebbe piu' confermarlo o annullarlo.
|
|
*/
|
|
public function corrente(): ?string
|
|
{
|
|
$id = Cache::get(self::CHIAVE_CORRENTE);
|
|
|
|
if (! is_string($id) || $id === '') {
|
|
return null;
|
|
}
|
|
|
|
$stato = $this->get($id);
|
|
|
|
// Puntatore orfano (stato scaduto) o operazione gia' chiusa: il posto
|
|
// e' libero, e il puntatore va tolto di mezzo.
|
|
if ($stato === null || in_array($stato['status'] ?? '', ['completed', 'failed'], true)) {
|
|
Cache::forget(self::CHIAVE_CORRENTE);
|
|
|
|
return null;
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
public function get(string $id): ?array
|
|
{
|
|
return Cache::get($this->key($id));
|
|
}
|
|
|
|
public function put(string $id, array $state): void
|
|
{
|
|
Cache::put($this->key($id), $state, $this->ttlSeconds);
|
|
}
|
|
|
|
public function forget(string $id): void
|
|
{
|
|
Cache::forget($this->key($id));
|
|
}
|
|
|
|
public function update(string $id, callable $callback): ?array
|
|
{
|
|
$state = $this->get($id);
|
|
|
|
if (! $state) {
|
|
return null;
|
|
}
|
|
|
|
$state = $callback($state) ?? $state;
|
|
$this->put($id, $state);
|
|
|
|
return $state;
|
|
}
|
|
|
|
public function log(string $id, string $message): void
|
|
{
|
|
$this->update($id, function (array $state) use ($message) {
|
|
$state['logs'][] = '[' . now()->format('H:i:s') . '] ' . $message;
|
|
$state['logs'] = array_slice($state['logs'], -self::MAX_LOG);
|
|
|
|
return $state;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cambia fase e, insieme, il totale di riferimento: ogni fase ha una sua
|
|
* unita' di misura (file da comprimere, file da estrarre, passi di
|
|
* cancellazione) e la barra riparte da zero su quel totale.
|
|
*/
|
|
public function fase(string $id, string $fase, int $total = 0): void
|
|
{
|
|
$this->update($id, function (array $state) use ($fase, $total) {
|
|
$state['fase'] = $fase;
|
|
$state['total'] = $total;
|
|
$state['processed'] = 0;
|
|
|
|
return $state;
|
|
});
|
|
|
|
$this->log($id, $fase . ($total > 0 ? ' (' . $total . ')' : ''));
|
|
}
|
|
|
|
public function avanza(string $id, int $processed, ?int $total = null): void
|
|
{
|
|
$this->update($id, function (array $state) use ($processed, $total) {
|
|
$state['processed'] = $processed;
|
|
if ($total !== null) {
|
|
$state['total'] = $total;
|
|
}
|
|
|
|
return $state;
|
|
});
|
|
}
|
|
|
|
public function markRunning(string $id): void
|
|
{
|
|
$this->update($id, function (array $state) {
|
|
$state['status'] = 'running';
|
|
$state['started_at'] ??= now()->toDateTimeString();
|
|
|
|
return $state;
|
|
});
|
|
}
|
|
|
|
public function markCompleted(string $id, array $result = []): void
|
|
{
|
|
$this->update($id, function (array $state) use ($result) {
|
|
$state['status'] = 'completed';
|
|
$state['processed'] = max($state['processed'], $state['total']);
|
|
$state['result'] = $result + $state['result'];
|
|
$state['finished_at'] = now()->toDateTimeString();
|
|
|
|
return $state;
|
|
});
|
|
|
|
$this->liberaCorrente($id);
|
|
}
|
|
|
|
public function markFailed(string $id, string $error): void
|
|
{
|
|
$this->update($id, function (array $state) use ($error) {
|
|
$state['status'] = 'failed';
|
|
$state['error'] = $error;
|
|
$state['finished_at'] = now()->toDateTimeString();
|
|
|
|
return $state;
|
|
});
|
|
|
|
$this->log($id, 'ERRORE: ' . $error);
|
|
$this->liberaCorrente($id);
|
|
}
|
|
|
|
/**
|
|
* Libera il posto, ma solo se e' ancora questa operazione a occuparlo:
|
|
* senza il confronto, la chiusura di un'operazione vecchia sbloccherebbe
|
|
* quella appena avviata da qualcun altro.
|
|
*/
|
|
private function liberaCorrente(string $id): void
|
|
{
|
|
if (Cache::get(self::CHIAVE_CORRENTE) === $id) {
|
|
Cache::forget(self::CHIAVE_CORRENTE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* L'archivio e' pronto ma non si prosegue: l'azzeramento parte solo quando
|
|
* il download risulta consegnato per intero.
|
|
*/
|
|
public function markAttesaDownload(string $id, array $result): void
|
|
{
|
|
$this->update($id, function (array $state) use ($result) {
|
|
$state['status'] = 'running';
|
|
$state['fase'] = self::FASE_ATTESA_DOWNLOAD;
|
|
$state['processed'] = $state['total'];
|
|
$state['result'] = $result + $state['result'];
|
|
|
|
return $state;
|
|
});
|
|
}
|
|
|
|
protected function key(string $id): string
|
|
{
|
|
return 'backup-op:' . $id;
|
|
}
|
|
}
|