fix: Add error ignore functionality to cronjob backups (#9396)

This commit is contained in:
ssongliu 2025-07-03 16:31:25 +08:00 committed by GitHub
parent fc81801c4c
commit 3fade48f8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 102 additions and 8 deletions

View file

@ -45,6 +45,7 @@ type CronjobOperate struct {
RetainCopies int `json:"retainCopies" validate:"number,min=1"`
RetryTimes int `json:"retryTimes" validate:"number,min=0"`
Timeout uint `json:"timeout" validate:"number,min=1"`
IgnoreErr bool `json:"ignoreErr"`
Secret string `json:"secret"`
AlertCount uint `json:"alertCount"`
@ -105,6 +106,7 @@ type CronjobInfo struct {
RetainCopies int `json:"retainCopies"`
RetryTimes int `json:"retryTimes"`
Timeout uint `json:"timeout"`
IgnoreErr bool `json:"ignoreErr"`
SnapshotRule SnapshotRule `json:"snapshotRule"`
SourceAccounts []string `json:"sourceAccounts"`

View file

@ -36,6 +36,7 @@ type Cronjob struct {
DownloadAccountID uint `json:"downloadAccountID"`
RetryTimes uint `json:"retryTimes"`
Timeout uint `json:"timeout"`
IgnoreErr bool `json:"ignoreErr"`
RetainCopies uint64 `json:"retainCopies"`
Status string `json:"status"`

View file

@ -439,6 +439,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
upMap["retain_copies"] = req.RetainCopies
upMap["retry_times"] = req.RetryTimes
upMap["timeout"] = req.Timeout
upMap["ignore_err"] = req.IgnoreErr
upMap["secret"] = req.Secret
err = cronjobRepo.Update(id, upMap)
if err != nil {

View file

@ -48,6 +48,7 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
return err
}
for _, app := range apps {
retry := 0
taskItem.AddSubTaskWithOps(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
var record model.BackupRecord
record.From = "cronjob"
@ -59,11 +60,22 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
backupDir := path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s/%s", app.App.Key, app.Name))
record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
if err := doAppBackup(&app, task, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
} else {
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
return nil
}
}
downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
if err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
}
task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
return nil
}
record.FileDir = path.Dir(downloadPath)
if err := backupRepo.CreateRecord(&record); err != nil {
@ -87,6 +99,7 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
return err
}
for _, web := range webs {
retry := 0
taskItem.AddSubTaskWithOps(task.GetTaskName(web.Alias, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
var record model.BackupRecord
record.From = "cronjob"
@ -99,12 +112,23 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.Alias, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
if err := doWebsiteBackup(&web, taskItem, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
} else {
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
return nil
}
}
downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
if err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
}
task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
return nil
}
record.FileDir = path.Dir(downloadPath)
if err := backupRepo.CreateRecord(&record); err != nil {
@ -128,6 +152,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
return err
}
for _, dbInfo := range dbs {
retry := 0
itemName := fmt.Sprintf("%s[%s] - %s", dbInfo.Database, dbInfo.DBType, dbInfo.Name)
taskItem.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
var record model.BackupRecord
@ -142,17 +167,34 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
if err := doMysqlBackup(dbInfo, backupDir, record.FileName); err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
} else {
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
return nil
}
}
} else {
if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName); err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
} else {
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
return nil
}
}
}
downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
if err != nil {
return err
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
}
task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
return nil
}
record.FileDir = path.Dir(downloadPath)
if err := backupRepo.CreateRecord(&record); err != nil {
@ -280,6 +322,7 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J
AppData: itemData.AppData,
PanelData: itemData.PanelData,
BackupData: itemData.BackupData,
WithDockerConf: true,
WithMonitorData: true,
WithLoginLog: true,
WithOperationLog: true,

View file

@ -205,6 +205,8 @@ SystemLog: 'System Log'
CutWebsiteLog: 'Rotate Website Log'
FileOrDir: 'Directory / File'
UploadFile: 'Uploading backup file {{ .file }} to {{ .backup }}'
IgnoreBackupErr: 'Backup failed, error: {{ .detail }}, ignoring this error...'
IgnoreUploadErr: 'Upload failed, error: {{ .detail }}, ignoring this error...'
#toolbox
ErrNotExistUser: 'The current user does not exist, please modify and try again!'

View file

@ -205,6 +205,8 @@ SystemLog: 'システムログ'
CutWebsiteLog: 'ウェブサイトログのローテーション'
FileOrDir: 'ディレクトリ / ファイル'
UploadFile: 'バックアップファイル {{ .file }} を {{ .backup }} にアップロード中'
IgnoreBackupErr: 'バックアップ失敗、エラー:{{ .detail }}、このエラーを無視します...'
IgnoreUploadErr: 'アップロード失敗、エラー:{{ .detail }}、このエラーを無視します...'
#toolbox
ErrNotExistUser: '現在のユーザーは存在しません。変更してもう一度お試しください。'

View file

@ -205,6 +205,8 @@ SystemLog: '시스템 로그'
CutWebsiteLog: '웹사이트 로그 회전'
FileOrDir: '디렉터리 / 파일'
UploadFile: '백업 파일 {{ .file }} 을(를) {{ .backup }}(으)로 업로드 중'
IgnoreBackupErr: 'Sandaran gagal, ralat: {{ .detail }}, abaikan ralat ini...'
IgnoreUploadErr: 'Muat naik gagal, ralat: {{ .detail }}, abaikan ralat ini...'
#도구상자
ErrNotExistUser: '현재 사용자가 존재하지 않습니다. 수정한 후 다시 시도하세요!'

View file

@ -204,6 +204,8 @@ SystemLog: 'Log Sistem'
CutWebsiteLog: 'Putar Log Laman Web'
FileOrDir: 'Direktori / Fail'
UploadFile: 'Muat naik fail sandaran {{ .file }} ke {{ .backup }}'
IgnoreBackupErr: 'Sandaran gagal, ralat: {{ .detail }}, abaikan ralat ini...'
IgnoreUploadErr: 'Muat naik gagal, ralat: {{ .detail }}, abaikan ralat ini...'
#kotak alat
ErrNotExistUser: 'Pengguna semasa tidak wujud, sila ubah suai dan cuba lagi!'

View file

@ -205,6 +205,8 @@ SystemLog: 'Log do Sistema'
CutWebsiteLog: 'Rotacionar Log do Website'
FileOrDir: 'Diretório / Arquivo'
UploadFile: 'Enviando arquivo de backup {{ .file }} para {{ .backup }}'
IgnoreBackupErr: 'Backup falhou, erro: {{ .detail }}, ignorando este erro...'
IgnoreUploadErr: 'Upload falhou, erro: {{ .detail }}, ignorando este erro...'
#caixa de ferramentas
ErrNotExistUser: 'O usuário atual não existe, modifique e tente novamente!'

View file

@ -205,6 +205,8 @@ SystemLog: 'Системный лог'
CutWebsiteLog: 'Ротация логов сайта'
FileOrDir: 'Каталог / Файл'
UploadFile: 'Загрузка файла резервной копии {{ .file }} в {{ .backup }}'
IgnoreBackupErr: 'Ошибка резервного копирования: {{ .detail }}, игнорируем эту ошибку...'
IgnoreUploadErr: 'Ошибка загрузки: {{ .detail }}, игнорируем эту ошибку...'
#ящик для инструментов
ErrNotExistUser: 'Текущий пользователь не существует, измените его и повторите попытку!'

View file

@ -204,6 +204,8 @@ SystemLog: '系統日誌'
CutWebsiteLog: '切割網站日誌'
FileOrDir: '目錄 / 檔案'
UploadFile: '上傳備份文件 {{ .file }} 到 {{ .backup }}'
IgnoreBackupErr: '備份失敗,錯誤:{{ .detail }},忽略本次錯誤...'
IgnoreUploadErr: '上傳失敗,錯誤:{{ .detail }},忽略本次錯誤...'
#toolbox
ErrNotExistUser: '目前使用者不存在,請修改後重試!'

View file

@ -204,6 +204,8 @@ SystemLog: "系统日志"
CutWebsiteLog: "切割网站日志"
FileOrDir: "目录 / 文件"
UploadFile: "上传备份文件 {{ .file }} 到 {{ .backup }}"
IgnoreBackupErr: "备份失败,错误:{{ .detail }},忽略本次错误..."
IgnoreUploadErr: "上传失败,错误:{{ .detail }},忽略本次错误..."
#toolbox
ErrNotExistUser: "当前用户不存在,请修改后重试!"

View file

@ -322,7 +322,7 @@ var UpdateRuntime = &gormigrate.Migration{
}
var AddSnapshotRule = &gormigrate.Migration{
ID: "20250627-add-snapshot-rule",
ID: "20250703-add-snapshot-rule",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.Cronjob{},

View file

@ -45,6 +45,7 @@ export namespace Cronjob {
sourceAccountItems: Array<number>;
retainCopies: number;
ignoreErr: boolean;
retryTimes: number;
timeout: number;
timeoutItem: number;
@ -91,6 +92,7 @@ export namespace Cronjob {
retainCopies: number;
retryTimes: number;
timeout: number;
ignoreErr: boolean;
secret: string;
alertCount: number;

View file

@ -1035,6 +1035,8 @@ const message = {
retainCopies: 'Retain records',
retryTimes: 'Retry Attempts',
timeout: 'Timeout',
ignoreErr: 'Ignore errors',
ignoreErrHelper: 'Ignore errors during backup to ensure all backup tasks complete',
retryTimesHelper: '0 means no retry after failure',
retainCopiesHelper: 'Number of copies to retain for execution records and logs',
retainCopiesHelper1: 'Number of copies to retain for backup files',

View file

@ -1004,6 +1004,8 @@ const message = {
retainCopies: '記録を保持します',
retryTimes: 'リトライ回数',
timeout: 'タイムアウト',
ignoreErr: 'エラーを無視',
ignoreErrHelper: 'バックアップ中のエラーを無視し全てのバックアップタスクを確実に実行します',
retryTimesHelper: '0は失敗後リトライしないことを意味します',
retainCopiesHelper: '実行記録とログのために保持するコピーの数',
retainCopiesHelper1: 'バックアップファイル用に保持するコピーの数',

View file

@ -991,6 +991,8 @@ const message = {
retainCopies: '기록 보관',
retryTimes: '재시도 횟수',
timeout: '타임아웃',
ignoreErr: '오류 무시',
ignoreErrHelper: '백업 과정에서 발생하는 오류를 무시하여 모든 백업 작업이 실행되도록 합니다',
retryTimesHelper: '0 실패 재시도 함을 의미합니다',
retainCopiesHelper: '실행 기록과 로그에 대해 보관할 복사본 ',
retainCopiesHelper1: '백업 파일에 대해 보관할 복사본 ',

View file

@ -1025,6 +1025,8 @@ const message = {
retainCopies: 'Simpan salinan',
retryTimes: 'Bilangan Cubaan Semula',
timeout: 'Masa Tamat',
ignoreErr: 'Abaikan ralat',
ignoreErrHelper: 'Abaikan ralat semasa sandaran untuk memastikan semua tugas sandaran dilaksanakan',
retryTimesHelper: '0 bermaksud tiada cubaan semula selepas gagal',
retainCopiesHelper: 'Bilangan salinan untuk menyimpan rekod pelaksanaan dan log',
retainCopiesHelper1: 'Bilangan salinan untuk menyimpan fail sandaran',

View file

@ -1021,6 +1021,8 @@ const message = {
retainCopies: 'Manter cópias',
retryTimes: 'Tentativas de Repetição',
timeout: 'Tempo Limite',
ignoreErr: 'Ignorar erros',
ignoreErrHelper: 'Ignorar erros durante o backup para garantir a execução de todas as tarefas de backup',
retryTimesHelper: '0 significa não repetir após falha',
retainCopiesHelper: 'Número de cópias a serem mantidas para registros de execução e logs',
retainCopiesHelper1: 'Número de cópias a serem mantidas para arquivos de backup',

View file

@ -1017,6 +1017,9 @@ const message = {
retainCopies: 'Сохранять записи',
retryTimes: 'Количество повторов',
timeout: 'Таймаут',
ignoreErr: 'Игнорировать ошибки',
ignoreErrHelper:
'Игнорировать ошибки во время резервного копирования для выполнения всех задач резервного копирования',
retryTimesHelper: '0 означает отсутствие повторов после сбоя',
retainCopiesHelper: 'Количество копий для сохранения записей выполнения и логов',
retainCopiesHelper1: 'Количество копий для сохранения файлов резервных копий',

View file

@ -983,6 +983,8 @@ const message = {
retainCopies: '保留份數',
retryTimes: '失敗重試次數',
timeout: '逾時時間',
ignoreErr: '忽略錯誤',
ignoreErrHelper: '忽略備份過程中出現的錯誤保證所有備份任務執行',
retryTimesHelper: '為0表示失敗後不重試',
retainCopiesHelper: '執行記錄及日誌保留份数',
retainCopiesHelper1: '備份文件保留份数',

View file

@ -982,6 +982,8 @@ const message = {
retainCopies: '保留份数',
retryTimes: '失败重试次数',
timeout: '超时时间',
ignoreErr: '忽略错误',
ignoreErrHelper: '忽略备份过程中出现的错误保证所有备份任务执行',
retryTimesHelper: ' 0 表示失败后不重试',
retainCopiesHelper: '执行记录及日志保留份数',
retainCopiesHelper1: '备份文件保留份数',

View file

@ -644,6 +644,12 @@
</div>
<el-row :gutter="20">
<LayoutCol :span="20" v-if="hasIgnore()">
<el-form-item>
<el-checkbox v-model="form.ignoreErr" :label="$t('cronjob.ignoreErr')" />
<span class="input-help">{{ $t('cronjob.ignoreErrHelper') }}</span>
</el-form-item>
</LayoutCol>
<LayoutCol>
<el-form-item :label="$t('cronjob.timeout')" prop="timeoutItem">
<el-input type="number" class="selectClass" v-model.number="form.timeoutItem">
@ -776,6 +782,7 @@ const form = reactive<Cronjob.CronjobInfo>({
dbNameList: [],
retainCopies: 7,
ignoreErr: false,
retryTimes: 3,
timeout: 3600,
timeoutItem: 3600,
@ -863,6 +870,7 @@ const search = async () => {
}
}
form.ignoreErr = res.data.ignoreErr;
form.retainCopies = res.data.retainCopies;
form.retryTimes = res.data.retryTimes;
form.timeout = res.data.timeout;
@ -1287,6 +1295,10 @@ function hasExclusionRules() {
);
}
function hasIgnore() {
return form.type === 'app' || form.type === 'website' || form.type === 'database';
}
function hasScript() {
return form.type === 'shell';
}