setInterval(300); $this->setTimeSensitivity(IJob::TIME_SENSITIVE); } private const MAX_DAILY_ATTEMPTS = 3; protected function run($argument): void { $budget = PHP_SAPI === 'cli' ? 240.0 : 20.0; $deadline = microtime(true) + $budget; $this->startScheduled(); foreach ($this->runs->findRunning(null, BackupService::KIND_FULL) as $run) { while (microtime(true) < $deadline && $run->getStatus() === 'running') { $before = $run->getUpdatedAt(); $run = $this->backups->step($run, min($deadline, microtime(true) + $budget)); if ($run->getUpdatedAt() === $before && $run->getStatus() === 'running') { break; // another process holds this run's lease: leave it to that process } } } $this->failStaleRestores(); $this->weeklyPrune(); foreach ($this->targets->list() as $t) { $this->alerts->checkStale($t); } } /** A restore run that has not moved for 6 hours is considered dead (it would block cleanup forever). */ private function failStaleRestores(): void { foreach ($this->runs->findRunning(null, 'restore') as $run) { if (time() - $run->getUpdatedAt() > 6 * 3600) { $run->setStatus(\OCA\NgBackup\Db\Run::FAILED); $run->setError('No progress for 6 hours; marked as failed'); $run->setFinishedAt(time()); $this->runs->update($run); $this->alerts->runFailed($run, '#' . $run->getTargetId()); } } } /** * Once a week, apply the retention policy to non-append-only locations: from Sunday on, at the * first moment no backup is running, so a long Sunday backup only delays it instead of skipping * the week. */ private function weeklyPrune(): void { $lastWeek = $this->appConfig->getValueString(Application::APP_ID, 'last_prune_week', ''); $due = date('N') === '7' ? date('o-W') : date('o-W', (int)strtotime('last sunday')); if (strcmp($lastWeek, $due) >= 0 || $this->runs->findRunning(null, BackupService::KIND_FULL) !== []) { return; } foreach ($this->targets->list() as $t) { if ($t->getAppendOnly()) { continue; } try { $this->prune->apply($t, false, true); } catch (\Throwable $e) { $this->logger->warning('NG Backup: cleanup of ' . $t->getName() . ' failed: ' . $e->getMessage()); } } $this->appConfig->setValueString(Application::APP_ID, 'last_prune_week', $due); } /** Daily schedule: appconfig ng_backup/schedule_time "HH:MM" (server time); empty = no schedule. */ private function startScheduled(): void { $at = $this->appConfig->getValueString(Application::APP_ID, 'schedule_time', ''); if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $at)) { return; } $todayAt = (int)strtotime(date('Y-m-d') . ' ' . $at); if (time() < $todayAt) { return; } foreach ($this->targets->list() as $t) { $last = $this->runs->lastFinished($t->getId(), BackupService::KIND_FULL); if ($this->runs->findRunning($t->getId(), BackupService::KIND_FULL) !== [] || ($last !== null && $last->getStartedAt() >= $todayAt)) { continue; } // After a failure: retry at most MAX_DAILY_ATTEMPTS times a day, an hour apart, instead // of starting (and notifying about) a new full run every five minutes all day. $today = $this->runs->startedSince($t->getId(), BackupService::KIND_FULL, $todayAt); if (count($today) >= self::MAX_DAILY_ATTEMPTS || ($today !== [] && time() - (int)$today[0]->getFinishedAt() < 3600)) { continue; } try { $this->backups->start($t, 'scheduled'); } catch (\Throwable $e) { $this->logger->warning('NG Backup: scheduled backup to ' . $t->getName() . ' not started: ' . $e->getMessage()); } } } }