!96 删除数据库备份和恢复历史

* feat: 删除数据库备份历史
* refactor dbScheduler
* feat: 从数据库备份历史中恢复数据库
* feat: 删除数据库恢复历史记录
* refactor dbScheuler
This commit is contained in:
kanzihuang
2024-01-30 13:12:43 +00:00
committed by Coder慌
parent fc1b9ef35d
commit 3f828cc5b0
33 changed files with 1101 additions and 378 deletions

View File

@@ -0,0 +1,155 @@
<template>
<div class="db-backup-history">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbBackupHistories"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
</template>
<template #action="{ data }">
<div>
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('name', '备份名称'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('lastResult', '恢复结果'),
TableColumn.new('lastTime', '恢复时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
];
const emptyQuery = {
dbId: 0,
dbName: '',
pageNum: 1,
pageSize: 10,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
/**
* 选中的数据
*/
selectedData: [],
});
const { query } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const deleteDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
await search();
ElMessage.success('删除成功');
};
const restoreDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
const pluralDbNames: string[] = [];
const dbNames: Map<string, boolean> = new Map();
state.selectedData.forEach((item: any) => {
if (!dbNames.has(item.dbName)) {
dbNames.set(item.dbName, false);
return;
}
if (!dbNames.get(item.dbName)) {
dbNames.set(item.dbName, true);
pluralDbNames.push(item.dbName);
}
});
if (pluralDbNames.length > 0) {
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
return;
}
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要恢复的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.restoreDbBackupHistory.request({
dbId: props.dbId,
backupHistoryId: backupHistoryId,
});
await search();
ElMessage.success('成功创建数据库恢复任务');
};
</script>
<style lang="scss"></style>

View File

@@ -21,6 +21,7 @@
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
</template>
<template #action="{ data }">
@@ -29,6 +30,7 @@
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
@@ -49,7 +51,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
@@ -72,10 +74,10 @@ const columns = [
TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabled', '是否启用'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
];
const emptyQuery = {
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
await search();
ElMessage.success('备份任务启动成功');
};
const deleteDbBackup = async (data: any) => {
let backupId: string;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('删除成功');
};
</script>
<style lang="scss"></style>

View File

@@ -63,13 +63,19 @@
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
备份
备份任务
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -138,6 +144,16 @@
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
</el-dialog>
<el-dialog
width="80%"
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
:close-on-click-modal="false"
:destroy-on-close="true"
v-model="dbBackupHistoryDialog.visible"
>
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
</el-dialog>
<el-dialog
width="80%"
:title="`${dbRestoreDialog.title} - 数据库恢复`"
@@ -192,6 +208,7 @@ import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -263,6 +280,13 @@ const state = reactive({
dbs: [],
dbId: 0,
},
// 数据库备份历史弹框
dbBackupHistoryDialog: {
title: '',
visible: false,
dbs: [],
dbId: 0,
},
// 数据库恢复弹框
dbRestoreDialog: {
title: '',
@@ -295,7 +319,8 @@ const state = reactive({
},
});
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
@@ -359,6 +384,10 @@ const handleMoreActionCommand = (commond: any) => {
onShowDbBackupDialog(data);
return;
}
case 'backupHistory': {
onShowDbBackupHistoryDialog(data);
return;
}
case 'restoreDb': {
onShowDbRestoreDialog(data);
return;
@@ -412,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.visible = true;
};
const onShowDbBackupHistoryDialog = async (row: any) => {
state.dbBackupHistoryDialog.title = `${row.name}`;
state.dbBackupHistoryDialog.dbId = row.id;
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
state.dbBackupHistoryDialog.visible = true;
};
const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id;

View File

@@ -62,7 +62,7 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
data: {
@@ -161,7 +161,7 @@ const state = reactive({
id: 0,
dbId: 0,
dbName: null as any,
intervalDay: 1,
intervalDay: 0,
startTime: null as any,
repeated: null as any,
dbBackupId: null as any,
@@ -233,7 +233,8 @@ const init = async (data: any) => {
} else {
state.form.dbName = '';
state.editOrCreate = false;
state.form.intervalDay = 1;
state.form.intervalDay = 0;
state.form.repeated = false;
state.form.pointInTime = new Date();
state.form.startTime = new Date();
state.histories = [];
@@ -252,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
const btnOk = async () => {
restoreForm.value.validate(async (valid: any) => {
if (valid) {
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
if (state.restoreMode == 'point-in-time') {
state.form.dbBackupId = 0;
state.form.dbBackupHistoryId = 0;
@@ -260,13 +267,14 @@ const btnOk = async () => {
state.form.pointInTime = null;
}
state.form.repeated = false;
state.form.intervalDay = 0;
const reqForm = { ...state.form };
let api = dbApi.createDbRestore;
if (props.data) {
api = dbApi.saveDbRestore;
}
api.request(reqForm).then(() => {
ElMessage.success('保存成功');
ElMessage.success('成功创建数据库恢复任务');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {

View File

@@ -21,12 +21,14 @@
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
</template>
<template #action="{ data }">
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
</template>
</page-table>
@@ -49,7 +51,7 @@
infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions>
@@ -63,7 +65,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('enabled', '是否启用'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
state.dbRestoreEditDialog.visible = true;
};
const deleteDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库恢复任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('删除成功');
};
const showDbRestore = async (data: any) => {
state.infoDialog.data = data;
state.infoDialog.visible = true;
};
const enableDbRestore = async (data: any) => {
let restoreId: String;
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的恢复任务');
ElMessage.error('请选择需要启用的数据库恢复任务');
return;
}
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
};
const disableDbRestore = async (data: any) => {
let restoreId: String;
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的恢复任务');
ElMessage.error('请选择需要禁用的数据库恢复任务');
return;
}
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });

View File

@@ -49,16 +49,20 @@ export const dbApi = {
// 获取数据库备份列表
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
// 获取数据库备份列表
// 获取数据库恢复列表
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),

View File

@@ -9,13 +9,16 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/timex"
"strconv"
"strings"
"time"
)
type DbBackup struct {
dbBackupApp *application.DbBackupApp `inject:"DbBackupApp"`
dbApp application.Db `inject:"DbApp"`
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
dbApp application.Db `inject:"DbApp"`
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
}
// todo: 鉴权,避免未经授权进行数据库备份和恢复
@@ -28,10 +31,10 @@ func (d *DbBackup) GetPageList(rc *req.Ctx) {
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.dbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
rc.ResData = res
}
@@ -50,21 +53,20 @@ func (d *DbBackup) Create(rc *req.Ctx) {
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
jobs := make([]*entity.DbBackup, 0, len(dbNames))
for _, dbName := range dbNames {
job := &entity.DbBackup{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
DbName: dbName,
Enabled: true,
Repeated: backupForm.Repeated,
StartTime: backupForm.StartTime,
Interval: backupForm.Interval,
Name: backupForm.Name,
DbInstanceId: db.InstanceId,
DbName: dbName,
Enabled: true,
Repeated: backupForm.Repeated,
StartTime: backupForm.StartTime,
Interval: backupForm.Interval,
Name: backupForm.Name,
}
jobs = append(jobs, job)
}
biz.ErrIsNilAppendErr(d.dbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
}
// Update 保存数据库备份任务
@@ -74,17 +76,17 @@ func (d *DbBackup) Update(rc *req.Ctx) {
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
rc.ReqParam = backupForm
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
job := &entity.DbBackup{}
job.Id = backupForm.Id
job.Name = backupForm.Name
job.StartTime = backupForm.StartTime
job.Interval = backupForm.Interval
biz.ErrIsNilAppendErr(d.dbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
}
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
biz.NotEmpty(idsStr, "backupId 为空")
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, paramName)
biz.NotEmpty(idsStr, paramName+" 为空")
rc.ReqParam = idsStr
ids := strings.Fields(idsStr)
for _, v := range ids {
@@ -104,28 +106,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
// Delete 删除数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
func (d *DbBackup) Delete(rc *req.Ctx) {
err := d.walk(rc, d.dbBackupApp.Delete)
err := d.walk(rc, "backupId", d.backupApp.Delete)
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
}
// Enable 启用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
func (d *DbBackup) Enable(rc *req.Ctx) {
err := d.walk(rc, d.dbBackupApp.Enable)
err := d.walk(rc, "backupId", d.backupApp.Enable)
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
}
// Disable 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
func (d *DbBackup) Disable(rc *req.Ctx) {
err := d.walk(rc, d.dbBackupApp.Disable)
err := d.walk(rc, "backupId", d.backupApp.Disable)
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
}
// Start 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
func (d *DbBackup) Start(rc *req.Ctx) {
err := d.walk(rc, d.dbBackupApp.Start)
err := d.walk(rc, "backupId", d.backupApp.StartNow)
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
}
@@ -136,7 +138,7 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database)
dbNamesWithoutBackup, err := d.dbBackupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutBackup
}
@@ -149,10 +151,71 @@ func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.dbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
backupHistoryCond.DbInstanceId = db.InstanceId
backupHistoryCond.InDbNames = strings.Fields(db.Database)
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
historyIds := make([]uint64, 0, len(backupHistories))
for _, history := range backupHistories {
historyIds = append(historyIds, history.Id)
}
restores := make([]*entity.DbRestore, 0, page.PageSize)
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
}
for _, history := range backupHistories {
for _, restore := range restores {
if restore.DbBackupHistoryId == history.Id {
history.LastStatus = restore.LastStatus
history.LastResult = restore.LastResult
history.LastTime = restore.LastTime
break
}
}
}
rc.ResData = res
}
// RestoreHistories 删除数据库备份历史
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
biz.NotEmpty(pm, "backupHistoryId 为空")
idsStr := strings.Fields(pm)
ids := make([]uint64, 0, len(idsStr))
for _, s := range idsStr {
id, err := strconv.ParseUint(s, 10, 64)
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
ids = append(ids, id)
}
histories := make([]*entity.DbBackupHistory, 0, len(ids))
err := d.backupApp.GetHistories(ids, &histories)
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
restores := make([]*entity.DbRestore, 0, len(histories))
now := time.Now()
for _, history := range histories {
job := &entity.DbRestore{
DbInstanceId: history.DbInstanceId,
DbName: history.DbName,
Enabled: true,
Repeated: false,
StartTime: now,
Interval: 0,
PointInTime: timex.NewNullTime(time.Time{}),
DbBackupId: history.DbBackupId,
DbBackupHistoryId: history.Id,
DbBackupHistoryName: history.Name,
}
restores = append(restores, job)
}
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
}
// DeleteHistories 删除数据库备份历史
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
}

View File

@@ -27,7 +27,7 @@ func (d *DbRestore) GetPageList(rc *req.Ctx) {
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
var restores []vo.DbRestore
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
@@ -48,7 +48,8 @@ func (d *DbRestore) Create(rc *req.Ctx) {
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
job := &entity.DbRestore{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
DbInstanceId: db.InstanceId,
DbName: restoreForm.DbName,
Enabled: true,
Repeated: restoreForm.Repeated,
StartTime: restoreForm.StartTime,
@@ -58,10 +59,13 @@ func (d *DbRestore) Create(rc *req.Ctx) {
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
}
job.DbName = restoreForm.DbName
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
}
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
}
// Update 保存数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
func (d *DbRestore) Update(rc *req.Ctx) {

View File

@@ -2,38 +2,50 @@ package vo
import (
"encoding/json"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/utils/timex"
"time"
)
// DbBackup 数据库备份任务
type DbBackup struct {
Id uint64 `json:"id"`
DbName string `json:"dbName"` // 数据库名
CreateTime time.Time `json:"createTime"` // 创建时间
StartTime time.Time `json:"startTime"` // 开始时间
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称
Id uint64 `json:"id"`
DbName string `json:"dbName"` // 数据库名
CreateTime time.Time `json:"createTime"` // 创建时间
StartTime time.Time `json:"startTime"` // 开始时间
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称
}
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
type dbBackup DbBackup
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
if len(backup.EnabledDesc) == 0 {
if backup.Enabled {
backup.EnabledDesc = "任务已启用"
} else {
backup.EnabledDesc = "任务已禁用"
}
}
return json.Marshal((*dbBackup)(backup))
}
// DbBackupHistory 数据库备份历史
type DbBackupHistory struct {
Id uint64 `json:"id"`
DbBackupId uint64 `json:"dbBackupId"`
CreateTime time.Time `json:"createTime"`
DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称
BinlogFileName string `json:"binlogFileName"`
Id uint64 `json:"id"`
DbBackupId uint64 `json:"dbBackupId"`
CreateTime time.Time `json:"createTime"`
DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称
BinlogFileName string `json:"binlogFileName"`
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
}

View File

@@ -14,6 +14,7 @@ type DbRestore struct {
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
@@ -27,6 +28,13 @@ type DbRestore struct {
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
type dbBackup DbRestore
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
if len(restore.EnabledDesc) == 0 {
if restore.Enabled {
restore.EnabledDesc = "任务已启用"
} else {
restore.EnabledDesc = "任务已禁用"
}
}
return json.Marshal((*dbBackup)(restore))
}

View File

@@ -3,9 +3,14 @@ package application
import (
"context"
"encoding/binary"
"errors"
"fmt"
"gorm.io/gorm"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"sync"
"github.com/google/uuid"
)
@@ -14,6 +19,9 @@ type DbBackupApp struct {
scheduler *dbScheduler `inject:"DbScheduler"`
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
dbApp Db `inject:"DbApp"`
mutex sync.Mutex
}
func (app *DbBackupApp) Init() error {
@@ -21,7 +29,7 @@ func (app *DbBackupApp) Init() error {
if err := app.backupRepo.ListToDo(&jobs); err != nil {
return err
}
if err := app.scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
return err
}
return nil
@@ -32,32 +40,111 @@ func (app *DbBackupApp) Close() {
}
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
return err
}
return app.scheduler.AddJob(ctx, jobs)
}
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
return app.scheduler.UpdateJob(ctx, job)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
return err
}
_ = app.scheduler.UpdateJob(ctx, job)
return nil
}
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库备份历史文件
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
return err
}
history := &entity.DbBackupHistory{
DbBackupId: jobId,
}
err := app.backupHistoryRepo.GetBy(history, "name")
switch {
default:
return err
case err == nil:
return fmt.Errorf("数据库备份存在历史记录【%s】无法删除该任务", history.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
}
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
return err
}
return nil
}
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.backupRepo
job := &entity.DbBackup{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
if job.IsExpired() {
return errors.New("任务已过期")
}
_ = app.scheduler.EnableJob(ctx, job)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.backupRepo
job := &entity.DbBackup{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
app.mutex.Lock()
defer app.mutex.Unlock()
job := &entity.DbBackup{}
if err := app.backupRepo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return errors.New("任务未启用")
}
_ = app.scheduler.StartJobNow(ctx, job)
return nil
}
// GetPageList 分页获取数据库备份任务
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
@@ -68,7 +155,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
// GetHistoryPageList 分页获取数据库备份历史
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
}
func NewIncUUID() (uuid.UUID, error) {
@@ -91,3 +182,41 @@ func NewIncUUID() (uuid.UUID, error) {
return uid, nil
}
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
// todo: 删除数据库备份历史文件
app.mutex.Lock()
defer app.mutex.Unlock()
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
if err != nil {
return err
}
defer func() {
_, err = app.backupHistoryRepo.UpdateDeleting(false, historyId)
if err == nil {
return
}
if retErr == nil {
retErr = err
return
}
retErr = fmt.Errorf("%w, %w", retErr, err)
}()
if !ok {
return errors.New("正在从备份历史中恢复数据库")
}
job := &entity.DbBackupHistory{}
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
return err
}
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
return err
}
return app.backupHistoryRepo.DeleteById(ctx, historyId)
}

View File

@@ -46,7 +46,7 @@ func (app *DbBinlogApp) run() {
if app.closed() {
break
}
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
}
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)

View File

@@ -2,15 +2,19 @@ package application
import (
"context"
"errors"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"sync"
)
type DbRestoreApp struct {
scheduler *dbScheduler `inject:"DbScheduler"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
mutex sync.Mutex
}
func (app *DbRestoreApp) Init() error {
@@ -18,7 +22,7 @@ func (app *DbRestoreApp) Init() error {
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
return err
}
if err := app.scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
return err
}
return nil
@@ -28,32 +32,101 @@ func (app *DbRestoreApp) Close() {
app.scheduler.Close()
}
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
return err
}
_ = app.scheduler.AddJob(ctx, jobs)
return nil
}
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
return app.scheduler.UpdateJob(ctx, job)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
return err
}
_ = app.scheduler.UpdateJob(ctx, job)
return nil
}
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库恢复历史文件
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
return err
}
history := &entity.DbRestoreHistory{
DbRestoreId: jobId,
}
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
return err
}
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
return err
}
return nil
}
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.restoreRepo
job := &entity.DbRestore{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
if job.IsExpired() {
return errors.New("任务已过期")
}
_ = app.scheduler.EnableJob(ctx, job)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.restoreRepo
job := &entity.DbRestore{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
// GetPageList 分页获取数据库恢复任务
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
// GetRestoresEnabled 获取数据库恢复任务
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
}
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)

View File

@@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/runner"
"reflect"
"sync"
@@ -35,6 +35,7 @@ func newDbScheduler() *dbScheduler {
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
)
return scheduler
}
@@ -43,27 +44,11 @@ func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
return job.Schedule()
}
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
switch typ {
case entity.DbJobTypeBackup:
return s.backupRepo
case entity.DbJobTypeRestore:
return s.restoreRepo
case entity.DbJobTypeBinlog:
return s.binlogRepo
default:
panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
}
}
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
return err
}
_ = s.runner.UpdateOrAdd(ctx, job)
_ = s.runner.Update(ctx, job)
return nil
}
@@ -71,28 +56,20 @@ func (s *dbScheduler) Close() {
s.runner.Close()
}
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if saving {
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
return err
}
}
reflectValue := reflect.ValueOf(jobs)
switch reflectValue.Kind() {
case reflect.Array, reflect.Slice:
reflectLen := reflectValue.Len()
for i := 0; i < reflectLen; i++ {
job := reflectValue.Index(i).Interface().(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job)
}
default:
job := jobs.(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job)
}
return nil
@@ -103,29 +80,16 @@ func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, j
s.mutex.Lock()
defer s.mutex.Unlock()
if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
return err
}
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
return nil
}
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
repo := s.repo(jobType)
job := entity.NewDbJob(jobType)
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
job.SetEnabled(true)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
return err
}
_ = s.runner.Add(ctx, job)
return nil
}
@@ -134,37 +98,19 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
s.mutex.Lock()
defer s.mutex.Unlock()
repo := s.repo(jobType)
job := entity.NewDbJob(jobType)
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
return err
}
_ = s.runner.Remove(ctx, job.GetKey())
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
return nil
}
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
job := entity.NewDbJob(jobType)
if err := s.repo(jobType).GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return errors.New("任务未启用")
}
_ = s.runner.StartNow(ctx, job)
return nil
}
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
id, err := NewIncUUID()
if err != nil {
return err
@@ -176,19 +122,14 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
DbInstanceId: backup.DbInstanceId,
DbName: backup.DbName,
}
conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
binlogInfo, err := dbProgram.Backup(ctx, history)
if err != nil {
return err
}
now := time.Now()
name := backup.Name
if len(name) == 0 {
name = backup.DbName
name := backup.DbName
if len(backup.Name) > 0 {
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
}
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
history.CreateTime = now
@@ -202,54 +143,59 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
return nil
}
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
restore := job.(*entity.DbRestore)
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
if restore.PointInTime.Valid {
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG")
}
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG 行模式")
}
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
if err != nil {
return err
}
if ok {
latestBinlogSequence = binlogHistory.Sequence
} else {
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
if err != nil {
return err
}
if !ok {
return nil
}
earliestBackupSequence = backupHistory.BinlogSequence
}
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
if err != nil {
return err
}
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
//if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
// return err
//} else if !enabled {
// return errors.New("数据库未启用 BINLOG")
//}
//if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
// return err
//} else if !enabled {
// return errors.New("数据库未启用 BINLOG 行模式")
//}
//
//latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
//binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
//if err != nil {
// return err
//}
//if ok {
// latestBinlogSequence = binlogHistory.Sequence
//} else {
// backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
// if err != nil {
// return err
// }
// if !ok {
// return nil
// }
// earliestBackupSequence = backupHistory.BinlogSequence
//}
//binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
//if err != nil {
// return err
//}
//if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
// return err
//}
if err := s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), true); err != nil {
return err
}
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
return err
}
} else {
if err := s.restoreBackupHistory(ctx, dbProgram, restore); err != nil {
backupHistory := &entity.DbBackupHistory{}
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("备份历史已删除")
}
return err
}
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
return err
}
}
@@ -264,76 +210,108 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
return nil
}
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
job.SetLastStatus(entity.DbJobRunning, nil)
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
logx.Errorf("failed to update job status: %v", err)
return
}
//func (s *dbScheduler) updateLastStatus(ctx context.Context, job entity.DbJob) error {
// switch typ := job.GetJobType(); typ {
// case entity.DbJobTypeBackup:
// return s.backupRepo.UpdateLastStatus(ctx, job)
// case entity.DbJobTypeRestore:
// return s.restoreRepo.UpdateLastStatus(ctx, job)
// case entity.DbJobTypeBinlog:
// return s.binlogRepo.UpdateLastStatus(ctx, job)
// default:
// panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
// }
//}
var errRun error
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
switch typ := job.GetJobType(); typ {
case entity.DbJobTypeBackup:
errRun = s.backupMysql(ctx, job)
return s.backupRepo.UpdateById(ctx, job)
case entity.DbJobTypeRestore:
errRun = s.restoreMysql(ctx, job)
return s.restoreRepo.UpdateById(ctx, job)
case entity.DbJobTypeBinlog:
errRun = s.fetchBinlogMysql(ctx, job)
return s.binlogRepo.UpdateById(ctx, job)
default:
errRun = fmt.Errorf("无效的数据库任务类型: %v", typ)
}
status := entity.DbJobSuccess
if errRun != nil {
status = entity.DbJobFailed
}
job.SetLastStatus(status, errRun)
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
logx.Errorf("failed to update job status: %v", err)
return
return fmt.Errorf("无效的数据库任务类型: %v", typ)
}
}
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) bool {
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
//job.SetLastStatus(entity.DbJobRunning, nil)
//if err := s.updateLastStatus(ctx, job); err != nil {
// logx.Errorf("failed to update job status: %v", err)
// return
//}
//var errRun error
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
switch typ := job.GetJobType(); typ {
case entity.DbJobTypeBackup:
return s.backup(ctx, dbProgram, job)
case entity.DbJobTypeRestore:
return s.restore(ctx, dbProgram, job)
case entity.DbJobTypeBinlog:
return s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), false)
default:
return fmt.Errorf("无效的数据库任务类型: %v", typ)
}
//status := entity.DbJobSuccess
//if errRun != nil {
// status = entity.DbJobFailed
//}
//job.SetLastStatus(status, errRun)
//if err := s.updateLastStatus(ctx, job); err != nil {
// logx.Errorf("failed to update job status: %v", err)
// return
//}
}
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) (bool, error) {
if job.IsExpired() {
return false, runner.ErrJobExpired
}
const maxCountByInstanceId = 4
const maxCountByDbName = 1
var countByInstanceId, countByDbName int
jobBase := job.GetJobBase()
for item, ok := next(); ok; item, ok = next() {
itemBase := item.GetJobBase()
if jobBase.DbInstanceId == itemBase.DbInstanceId {
if job.GetInstanceId() == item.GetInstanceId() {
countByInstanceId++
if countByInstanceId >= maxCountByInstanceId {
return false
return false, nil
}
if relatedToBinlog(job.GetJobType()) {
// todo: 恢复数据库前触发 BINLOG 同步BINLOG 同步完成后才能恢复数据库
if relatedToBinlog(item.GetJobType()) {
return false
return false, nil
}
}
if job.GetDbName() == item.GetDbName() {
countByDbName++
if countByDbName >= maxCountByDbName {
return false
return false, nil
}
}
}
}
return true
return true, nil
}
func relatedToBinlog(typ entity.DbJobType) bool {
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
}
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
if err != nil {
return err
}
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
if err != nil {
return err
}
@@ -362,40 +340,63 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
TargetPosition: target.Position,
TargetTime: job.PointInTime.Time,
}
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
return err
}
if err := program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
return err
}
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
backup := &entity.DbBackup{
DbJobBaseImpl: entity.NewDbBJobBase(backupHistory.DbInstanceId, entity.DbJobTypeBackup),
DbName: backupHistory.DbName,
Enabled: true,
Repeated: false,
StartTime: time.Now(),
Interval: 0,
Name: fmt.Sprintf("%s-系统自动备份", backupHistory.DbName),
DbInstanceId: backupHistory.DbInstanceId,
DbName: backupHistory.DbName,
Enabled: true,
Repeated: false,
StartTime: time.Now(),
Interval: 0,
Name: "系统备份",
}
backup.Id = backupHistory.DbBackupId
if err := s.backupMysql(ctx, backup); err != nil {
if err := s.backup(ctx, dbProgram, backup); err != nil {
return err
}
return nil
}
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
backupHistory := &entity.DbBackupHistory{}
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
if err != nil {
return err
}
defer func() {
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
if err == nil {
return
}
if retErr == nil {
retErr = err
return
}
retErr = fmt.Errorf("%w, %w", retErr, err)
}()
if !ok {
return errors.New("关联的数据库备份历史已删除")
}
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
}
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
instanceId := backup.GetJobBase().DbInstanceId
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool) error {
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG")
}
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG 行模式")
}
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
if err != nil {
@@ -413,12 +414,7 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
}
earliestBackupSequence = backupHistory.BinlogSequence
}
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
if err != nil {
return err
}

View File

@@ -19,6 +19,8 @@ type DbProgram interface {
RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error
RemoveBackupHistory(ctx context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
}

View File

@@ -142,6 +142,12 @@ func (svc *DbProgramMysql) Backup(ctx context.Context, backupHistory *entity.DbB
return binlogInfo, nil
}
func (svc *DbProgramMysql) RemoveBackupHistory(_ context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error {
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
return os.Remove(fileName)
}
func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
dbInfo := svc.dbInfo()
args := []string{

View File

@@ -9,20 +9,30 @@ var _ DbJob = (*DbBackup)(nil)
// DbBackup 数据库备份任务
type DbBackup struct {
*DbJobBaseImpl
DbJobBaseImpl
Enabled bool // 是否启用
StartTime time.Time // 开始时间
Interval time.Duration // 间隔时间
Repeated bool // 是否重复执行
DbName string // 数据库名称
Name string // 数据库备份名称
DbInstanceId uint64 // 数据库实例ID
DbName string // 数据库名称
Name string // 数据库备份名称
Enabled bool // 是否启用
EnabledDesc string // 启用状态描述
StartTime time.Time // 开始时间
Interval time.Duration // 间隔时间
Repeated bool // 是否重复执行
}
func (b *DbBackup) GetInstanceId() uint64 {
return b.DbInstanceId
}
func (b *DbBackup) GetDbName() string {
return b.DbName
}
func (b *DbBackup) GetJobType() DbJobType {
return DbJobTypeBackup
}
func (b *DbBackup) Schedule() (time.Time, error) {
if b.IsFinished() {
return time.Time{}, runner.ErrJobFinished
@@ -37,7 +47,7 @@ func (b *DbBackup) Schedule() (time.Time, error) {
lastTime = b.StartTime.Add(-b.Interval)
}
return lastTime.Add(b.Interval - lastTime.Sub(b.StartTime)%b.Interval), nil
case DbJobFailed:
case DbJobRunning, DbJobFailed:
return time.Now().Add(time.Minute), nil
default:
return b.StartTime, nil
@@ -52,8 +62,13 @@ func (b *DbBackup) IsEnabled() bool {
return b.Enabled
}
func (b *DbBackup) SetEnabled(enabled bool) {
func (b *DbBackup) IsExpired() bool {
return false
}
func (b *DbBackup) SetEnabled(enabled bool, desc string) {
b.Enabled = enabled
b.EnabledDesc = desc
}
func (b *DbBackup) Update(job runner.Job) {
@@ -65,3 +80,15 @@ func (b *DbBackup) Update(job runner.Job) {
func (b *DbBackup) GetInterval() time.Duration {
return b.Interval
}
func (b *DbBackup) SetLastStatus(status DbJobStatus, err error) {
b.setLastStatus(b.GetJobType(), status, err)
}
func (b *DbBackup) GetKey() DbJobKey {
return b.getKey(b.GetJobType())
}
func (b *DbBackup) SetStatus(status runner.JobStatus, err error) {
b.setLastStatus(b.GetJobType(), status, err)
}

View File

@@ -26,6 +26,7 @@ var _ DbJob = (*DbBinlog)(nil)
// DbBinlog 数据库备份任务
type DbBinlog struct {
DbJobBaseImpl
DbInstanceId uint64 // 数据库实例ID
}
func NewDbBinlog(instanceId uint64) *DbBinlog {
@@ -35,13 +36,17 @@ func NewDbBinlog(instanceId uint64) *DbBinlog {
return job
}
func (b *DbBinlog) GetInstanceId() uint64 {
return b.DbInstanceId
}
func (b *DbBinlog) GetDbName() string {
// binlog 是全库级别的
return ""
}
func (b *DbBinlog) Schedule() (time.Time, error) {
switch b.GetJobBase().LastStatus {
switch b.LastStatus {
case DbJobSuccess:
return time.Time{}, runner.ErrJobFinished
case DbJobFailed:
@@ -57,8 +62,28 @@ func (b *DbBinlog) IsEnabled() bool {
return true
}
func (b *DbBinlog) SetEnabled(_ bool) {}
func (b *DbBinlog) IsExpired() bool {
return false
}
func (b *DbBinlog) SetEnabled(_ bool, _ string) {}
func (b *DbBinlog) GetInterval() time.Duration {
return 0
}
func (b *DbBinlog) GetJobType() DbJobType {
return DbJobTypeBinlog
}
func (b *DbBinlog) SetLastStatus(status DbJobStatus, err error) {
b.setLastStatus(b.GetJobType(), status, err)
}
func (b *DbBinlog) GetKey() DbJobKey {
return b.getKey(b.GetJobType())
}
func (b *DbBinlog) SetStatus(status DbJobStatus, err error) {
b.setLastStatus(b.GetJobType(), status, err)
}

View File

@@ -13,12 +13,12 @@ const LastResultSize = 256
type DbJobKey = runner.JobKey
type DbJobStatus int
type DbJobStatus = runner.JobStatus
const (
DbJobRunning DbJobStatus = iota
DbJobSuccess
DbJobFailed
DbJobRunning = runner.JobRunning
DbJobSuccess = runner.JobSuccess
DbJobFailed = runner.JobFailed
)
type DbJobType string
@@ -28,12 +28,14 @@ func (typ DbJobType) String() string {
}
const (
DbJobUnknown DbJobType = "db-unknown"
DbJobTypeBackup DbJobType = "db-backup"
DbJobTypeRestore DbJobType = "db-restore"
DbJobTypeBinlog DbJobType = "db-binlog"
)
const (
DbJobNameUnknown = "未知任务"
DbJobNameBackup = "数据库备份"
DbJobNameRestore = "数据库恢复"
DbJobNameBinlog = "BINLOG同步"
@@ -43,41 +45,24 @@ var _ runner.Job = (DbJob)(nil)
type DbJobBase interface {
model.ModelI
GetKey() string
GetJobType() DbJobType
SetJobType(typ DbJobType)
GetJobBase() *DbJobBaseImpl
SetLastStatus(status DbJobStatus, err error)
GetLastStatus() DbJobStatus
}
type DbJob interface {
runner.Job
DbJobBase
GetInstanceId() uint64
GetKey() string
GetJobType() DbJobType
GetDbName() string
Schedule() (time.Time, error)
IsEnabled() bool
SetEnabled(enabled bool)
IsExpired() bool
SetEnabled(enabled bool, desc string)
Update(job runner.Job)
GetInterval() time.Duration
}
func NewDbJob(typ DbJobType) DbJob {
switch typ {
case DbJobTypeBackup:
return &DbBackup{
DbJobBaseImpl: &DbJobBaseImpl{
jobType: DbJobTypeBackup},
}
case DbJobTypeRestore:
return &DbRestore{
DbJobBaseImpl: &DbJobBaseImpl{
jobType: DbJobTypeRestore},
}
default:
panic(fmt.Sprintf("invalid DbJobType: %v", typ))
}
SetLastStatus(status DbJobStatus, err error)
}
var _ DbJobBase = (*DbJobBaseImpl)(nil)
@@ -85,30 +70,25 @@ var _ DbJobBase = (*DbJobBaseImpl)(nil)
type DbJobBaseImpl struct {
model.Model
DbInstanceId uint64 // 数据库实例ID
LastStatus DbJobStatus // 最近一次执行状态
LastResult string // 最近一次执行结果
LastTime timex.NullTime // 最近一次执行时间
jobType DbJobType
jobKey runner.JobKey
LastStatus DbJobStatus // 最近一次执行状态
LastResult string // 最近一次执行结果
LastTime timex.NullTime // 最近一次执行时间
jobKey runner.JobKey
}
func NewDbBJobBase(instanceId uint64, jobType DbJobType) *DbJobBaseImpl {
return &DbJobBaseImpl{
DbInstanceId: instanceId,
jobType: jobType,
func (d *DbJobBaseImpl) getJobType() DbJobType {
job, ok := any(d).(DbJob)
if !ok {
return DbJobUnknown
}
return job.GetJobType()
}
func (d *DbJobBaseImpl) GetJobType() DbJobType {
return d.jobType
func (d *DbJobBaseImpl) GetLastStatus() DbJobStatus {
return d.LastStatus
}
func (d *DbJobBaseImpl) SetJobType(typ DbJobType) {
d.jobType = typ
}
func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
var statusName, jobName string
switch status {
case DbJobRunning:
@@ -120,7 +100,8 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
default:
return
}
switch d.jobType {
switch jobType {
case DbJobTypeBackup:
jobName = DbJobNameBackup
case DbJobTypeRestore:
@@ -128,7 +109,7 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
case DbJobTypeBinlog:
jobName = DbJobNameBinlog
default:
jobName = d.jobType.String()
jobName = jobType.String()
}
d.LastStatus = status
var result = jobName + statusName
@@ -139,17 +120,13 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
d.LastTime = timex.NewNullTime(time.Now())
}
func (d *DbJobBaseImpl) GetJobBase() *DbJobBaseImpl {
return d
}
func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey {
return fmt.Sprintf("%v-%d", typ, jobId)
}
func (d *DbJobBaseImpl) GetKey() DbJobKey {
func (d *DbJobBaseImpl) getKey(jobType DbJobType) DbJobKey {
if len(d.jobKey) == 0 {
d.jobKey = FormatJobKey(d.jobType, d.Id)
d.jobKey = FormatJobKey(jobType, d.Id)
}
return d.jobKey
}

View File

@@ -10,10 +10,12 @@ var _ DbJob = (*DbRestore)(nil)
// DbRestore 数据库恢复任务
type DbRestore struct {
*DbJobBaseImpl
DbJobBaseImpl
DbInstanceId uint64 // 数据库实例ID
DbName string // 数据库名称
Enabled bool // 是否启用
EnabledDesc string // 启用状态描述
StartTime time.Time // 开始时间
Interval time.Duration // 间隔时间
Repeated bool // 是否重复执行
@@ -23,6 +25,10 @@ type DbRestore struct {
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称
}
func (r *DbRestore) GetInstanceId() uint64 {
return r.DbInstanceId
}
func (r *DbRestore) GetDbName() string {
return r.DbName
}
@@ -36,7 +42,7 @@ func (r *DbRestore) Schedule() (time.Time, error) {
return time.Time{}, runner.ErrJobFinished
default:
if time.Now().Sub(r.StartTime) > time.Hour {
return time.Time{}, runner.ErrJobTimeout
return time.Time{}, runner.ErrJobExpired
}
return r.StartTime, nil
}
@@ -46,8 +52,13 @@ func (r *DbRestore) IsEnabled() bool {
return r.Enabled
}
func (r *DbRestore) SetEnabled(enabled bool) {
func (r *DbRestore) SetEnabled(enabled bool, desc string) {
r.Enabled = enabled
r.EnabledDesc = desc
}
func (r *DbRestore) IsExpired() bool {
return !r.Repeated && time.Now().After(r.StartTime.Add(time.Hour))
}
func (r *DbRestore) IsFinished() bool {
@@ -63,3 +74,19 @@ func (r *DbRestore) Update(job runner.Job) {
func (r *DbRestore) GetInterval() time.Duration {
return r.Interval
}
func (r *DbRestore) GetJobType() DbJobType {
return DbJobTypeRestore
}
func (r *DbRestore) SetLastStatus(status DbJobStatus, err error) {
r.setLastStatus(r.GetJobType(), status, err)
}
func (r *DbRestore) GetKey() DbJobKey {
return r.getKey(r.GetJobType())
}
func (r *DbRestore) SetStatus(status DbJobStatus, err error) {
r.setLastStatus(r.GetJobType(), status, err)
}

View File

@@ -40,8 +40,8 @@ type DbSqlExecQuery struct {
CreatorId uint64
}
// DbJobQuery 数据库备份任务查询
type DbJobQuery struct {
// DbBackupQuery 数据库备份任务查询
type DbBackupQuery struct {
Id uint64 `json:"id" form:"id"`
DbName string `json:"dbName" form:"dbName"`
IntervalDay int `json:"intervalDay" form:"intervalDay"`
@@ -61,13 +61,13 @@ type DbBackupHistoryQuery struct {
}
// DbRestoreQuery 数据库备份任务查询
//type DbRestoreQuery struct {
// Id uint64 `json:"id" form:"id"`
// DbName string `json:"dbName" form:"dbName"`
// InDbNames []string `json:"-" form:"-"`
// DbInstanceId uint64 `json:"-" form:"-"`
// Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行
//}
type DbRestoreQuery struct {
Id uint64 `json:"id" form:"id"`
DbName string `json:"dbName" form:"dbName"`
InDbNames []string `json:"-" form:"-"`
DbInstanceId uint64 `json:"-" form:"-"`
Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行
}
// DbRestoreHistoryQuery 数据库备份任务查询
type DbRestoreHistoryQuery struct {

View File

@@ -13,5 +13,5 @@ type DbBackup interface {
GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
// GetPageList 分页获取数据库任务列表
GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}

View File

@@ -9,10 +9,15 @@ import (
type DbBackupHistory interface {
base.Repo[*entity.DbBackupHistory]
// GetHistories 分页获取数据备份历史
GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
// GetPageList 分页获取数据备份历史
GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error)
GetEarliestHistory(instanceId uint64) (*entity.DbBackupHistory, bool, error)
GetHistories(backupHistoryIds []uint64, toEntity any) error
UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error)
UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error)
}

View File

@@ -12,5 +12,7 @@ type DbRestore interface {
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
// GetPageList 分页获取数据库任务列表
GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
GetEnabledRestores(toEntity any, backupHistoryId ...uint64) error
}

View File

@@ -63,7 +63,7 @@ func (d *dbBackupRepoImpl) ListToDo(jobs any) error {
}
// GetPageList 分页获取数据库备份任务列表
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
d.GetModel()
qd := gormx.NewQuery(d.GetModel()).
Eq("id", condition.Id).
@@ -83,7 +83,12 @@ func (d *dbBackupRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enable
cond := map[string]any{
"id": jobId,
}
desc := "任务已禁用"
if enabled {
desc = "任务已启用"
}
return d.Updates(cond, map[string]any{
"enabled": enabled,
"enabled": enabled,
"enabled_desc": desc,
})
}

View File

@@ -21,8 +21,8 @@ func NewDbBackupHistoryRepo() repository.DbBackupHistory {
return &dbBackupHistoryRepoImpl{}
}
func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.DbBackupHistory)).
func (repo *dbBackupHistoryRepoImpl) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(repo.GetModel()).
Eq("id", condition.Id).
Eq0("db_instance_id", condition.DbInstanceId).
In0("db_name", condition.InDbNames).
@@ -31,6 +31,14 @@ func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHist
return gormx.PageQuery(qd, pageParam, toEntity)
}
func (repo *dbBackupHistoryRepoImpl) GetHistories(backupHistoryIds []uint64, toEntity any) error {
return global.Db.Model(repo.GetModel()).
Where("id in ?", backupHistoryIds).
Scopes(gormx.UndeleteScope).
Find(toEntity).
Error
}
func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error) {
history := &entity.DbBackupHistory{}
db := global.Db
@@ -65,3 +73,33 @@ func (repo *dbBackupHistoryRepoImpl) GetEarliestHistory(instanceId uint64) (*ent
return nil, false, err
}
}
func (repo *dbBackupHistoryRepoImpl) UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error) {
db := global.Db.Model(repo.GetModel()).
Where("id in ?", backupHistoryId).
Where("restoring = false").
Scopes(gormx.UndeleteScope).
Update("restoring", deleting)
if db.Error != nil {
return false, db.Error
}
if db.RowsAffected != int64(len(backupHistoryId)) {
return false, nil
}
return true, nil
}
func (repo *dbBackupHistoryRepoImpl) UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error) {
db := global.Db.Model(repo.GetModel()).
Where("id in ?", backupHistoryId).
Where("deleting = false").
Scopes(gormx.UndeleteScope).
Update("restoring", restoring)
if db.Error != nil {
return false, db.Error
}
if db.RowsAffected != int64(len(backupHistoryId)) {
return false, nil
}
return true, nil
}

View File

@@ -31,7 +31,7 @@ func (d *dbJobBaseImpl[T]) UpdateLastStatus(ctx context.Context, job entity.DbJo
}
func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any) error {
// refactor and jobs from any to []T
// refactor jobs from any to []T
return gormx.Tx(func(db *gorm.DB) error {
var instanceId uint64
var dbNames []string
@@ -44,11 +44,10 @@ func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any
dbNames = make([]string, 0, reflectLen)
for i := 0; i < reflectLen; i++ {
job := reflectValue.Index(i).Interface().(entity.DbJob)
jobBase := job.GetJobBase()
if instanceId == 0 {
instanceId = jobBase.DbInstanceId
instanceId = job.GetInstanceId()
}
if jobBase.DbInstanceId != instanceId {
if job.GetInstanceId() != instanceId {
return errors.New("不支持同时为多个数据库实例添加数据库任务")
}
if job.GetInterval() == 0 {
@@ -59,8 +58,7 @@ func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any
}
default:
job := jobs.(entity.DbJob)
jobBase := job.GetJobBase()
instanceId = jobBase.DbInstanceId
instanceId = job.GetInstanceId()
if job.GetInterval() > 0 {
dbNames = append(dbNames, job.GetDbName())
}

View File

@@ -54,8 +54,7 @@ func (d *dbRestoreRepoImpl) ListToDo(jobs any) error {
}
// GetPageList 分页获取数据库备份任务列表
func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
d.GetModel()
func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(d.GetModel()).
Eq("id", condition.Id).
Eq0("db_instance_id", condition.DbInstanceId).
@@ -65,6 +64,17 @@ func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam
return gormx.PageQuery(qd, pageParam, toEntity)
}
func (d *dbRestoreRepoImpl) GetEnabledRestores(toEntity any, backupHistoryId ...uint64) error {
return global.Db.Model(d.GetModel()).
Select("id", "db_backup_history_id", "last_status", "last_result", "last_time").
Where("db_backup_history_id in ?", backupHistoryId).
Where("enabled = true").
Scopes(gormx.UndeleteScope).
Order("id DESC").
Find(toEntity).
Error
}
// AddJob 添加数据库任务
func (d *dbRestoreRepoImpl) AddJob(ctx context.Context, jobs any) error {
return addJob[*entity.DbRestore](ctx, d.dbJobBaseImpl, jobs)
@@ -74,7 +84,12 @@ func (d *dbRestoreRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enabl
cond := map[string]any{
"id": jobId,
}
desc := "任务已禁用"
if enabled {
desc = "任务已启用"
}
return d.Updates(cond, map[string]any{
"enabled": enabled,
"enabled": enabled,
"enabled_desc": desc,
})
}

View File

@@ -35,6 +35,10 @@ func InitDbBackupRouter(router *gin.RouterGroup) {
// 获取数据库备份历史
req.NewGet(":dbId/backup-histories/", d.GetHistoryPageList),
// 从数据库备份历史中恢复数据库
req.NewPost(":dbId/backup-histories/:backupHistoryId/restore", d.RestoreHistories),
// 删除数据库备份历史
req.NewDelete(":dbId/backup-histories/:backupHistoryId", d.DeleteHistories),
}
req.BatchSetGroup(dbs, reqs)

View File

@@ -50,10 +50,6 @@ func (d *wrapper[T]) GetKey() string {
return d.key
}
func (d *wrapper[T]) Payload() T {
return d.job
}
func NewDelayQueue[T Delayable](cap int) *DelayQueue[T] {
singleDequeue := make(chan struct{}, 1)
singleDequeue <- struct{}{}

View File

@@ -11,18 +11,20 @@ import (
)
var (
ErrJobNotFound = errors.New("job not found")
ErrJobExist = errors.New("job already exists")
ErrJobFinished = errors.New("job already finished")
ErrJobDisabled = errors.New("job has been disabled")
ErrJobTimeout = errors.New("job has timed out")
ErrJobNotFound = errors.New("任务未找到")
ErrJobExist = errors.New("任务已存在")
ErrJobFinished = errors.New("任务已完成")
ErrJobDisabled = errors.New("任务已禁用")
ErrJobExpired = errors.New("任务已过期")
ErrJobRunning = errors.New("任务执行中")
)
type JobKey = string
type RunJobFunc[T Job] func(ctx context.Context, job T)
type RunJobFunc[T Job] func(ctx context.Context, job T) error
type NextJobFunc[T Job] func() (T, bool)
type RunnableJobFunc[T Job] func(job T, next NextJobFunc[T]) bool
type RunnableJobFunc[T Job] func(job T, next NextJobFunc[T]) (bool, error)
type ScheduleJobFunc[T Job] func(job T) (deadline time.Time, err error)
type UpdateJobFunc[T Job] func(ctx context.Context, job T) error
type JobStatus int
@@ -31,11 +33,15 @@ const (
JobDelaying
JobWaiting
JobRunning
JobSuccess
JobFailed
)
type Job interface {
GetKey() JobKey
Update(job Job)
SetStatus(status JobStatus, err error)
SetEnabled(enabled bool, desc string)
}
type iterator[T Job] struct {
@@ -114,6 +120,7 @@ type Runner[T Job] struct {
runJob RunJobFunc[T]
runnableJob RunnableJobFunc[T]
scheduleJob ScheduleJobFunc[T]
updateJob UpdateJobFunc[T]
mutex sync.Mutex
wg sync.WaitGroup
context context.Context
@@ -138,6 +145,12 @@ func WithScheduleJob[T Job](scheduleJob ScheduleJobFunc[T]) Opt[T] {
}
}
func WithUpdateJob[T Job](updateJob UpdateJobFunc[T]) Opt[T] {
return func(runner *Runner[T]) {
runner.updateJob = updateJob
}
}
func NewRunner[T Job](maxRunning int, runJob RunJobFunc[T], opts ...Opt[T]) *Runner[T] {
ctx, cancel := context.WithCancel(context.Background())
runner := &Runner[T]{
@@ -151,14 +164,15 @@ func NewRunner[T Job](maxRunning int, runJob RunJobFunc[T], opts ...Opt[T]) *Run
delayQueue: NewDelayQueue[*wrapper[T]](0),
}
runner.runJob = runJob
runner.runnableJob = func(job T, _ NextJobFunc[T]) (bool, error) {
return true, nil
}
runner.updateJob = func(ctx context.Context, job T) error {
return nil
}
for _, opt := range opts {
opt(runner)
}
if runner.runnableJob == nil {
runner.runnableJob = func(job T, _ NextJobFunc[T]) bool {
return true
}
}
runner.wg.Add(maxRunning + 1)
for i := 0; i < maxRunning; i++ {
@@ -211,10 +225,32 @@ func (r *Runner[T]) doRun(wrap *wrapper[T]) {
defer func() {
if err := recover(); err != nil {
logx.Error(fmt.Sprintf("failed to run job: %v", err))
time.Sleep(time.Millisecond * 10)
}
}()
r.runJob(r.context, wrap.job)
wrap.job.SetStatus(JobRunning, nil)
if err := r.updateJob(r.context, wrap.job); err != nil {
err = fmt.Errorf("任务状态保存失败: %w", err)
wrap.job.SetStatus(JobFailed, err)
_ = r.updateJob(r.context, wrap.job)
return
}
runErr := r.runJob(r.context, wrap.job)
if runErr != nil {
wrap.job.SetStatus(JobFailed, runErr)
} else {
wrap.job.SetStatus(JobSuccess, nil)
}
if err := r.updateJob(r.context, wrap.job); err != nil {
if runErr != nil {
err = fmt.Errorf("任务状态保存失败: %w, %w", err, runErr)
} else {
err = fmt.Errorf("任务状态保存失败: %w", err)
}
wrap.job.SetStatus(JobFailed, err)
_ = r.updateJob(r.context, wrap.job)
return
}
}
func (r *Runner[T]) afterRun(wrap *wrapper[T]) {
@@ -249,12 +285,19 @@ func (r *Runner[T]) pickRunnableJob() (*wrapper[T], bool) {
r.mutex.Lock()
defer r.mutex.Unlock()
var disabled []JobKey
iter := r.running.Iterator()
var runnable *wrapper[T]
ok := r.waiting.Any(func(key interface{}, value interface{}) bool {
wrap := value.(*wrapper[T])
iter.Begin()
if r.runnableJob(wrap.job, iter.Next) {
able, err := r.runnableJob(wrap.job, iter.Next)
if err != nil {
wrap.job.SetEnabled(false, err.Error())
r.updateJob(r.context, wrap.job)
disabled = append(disabled, key.(JobKey))
}
if able {
if r.running.Full() {
return false
}
@@ -269,6 +312,10 @@ func (r *Runner[T]) pickRunnableJob() (*wrapper[T], bool) {
}
return false
})
for _, key := range disabled {
r.waiting.Remove(key)
delete(r.all, key)
}
if !ok {
return nil, false
}
@@ -304,24 +351,23 @@ func (r *Runner[T]) Add(ctx context.Context, job T) error {
return r.schedule(ctx, deadline, job)
}
func (r *Runner[T]) UpdateOrAdd(ctx context.Context, job T) error {
func (r *Runner[T]) Update(ctx context.Context, job T) error {
r.mutex.Lock()
defer r.mutex.Unlock()
wrap, ok := r.all[job.GetKey()]
if ok {
wrap.job.Update(job)
switch wrap.status {
case JobDelaying:
r.delayQueue.Remove(ctx, wrap.key)
delete(r.all, wrap.key)
case JobWaiting:
r.waiting.Remove(wrap.key)
delete(r.all, wrap.key)
case JobRunning:
return nil
default:
}
if !ok {
return ErrJobNotFound
}
wrap.job.Update(job)
switch wrap.status {
case JobDelaying:
r.delayQueue.Remove(ctx, wrap.key)
case JobWaiting:
r.waiting.Remove(wrap.key)
case JobRunning:
return nil
default:
}
deadline, err := r.doScheduleJob(job, false)
if err != nil {
@@ -360,7 +406,7 @@ func (r *Runner[T]) Remove(ctx context.Context, key JobKey) error {
wrap, ok := r.all[key]
if !ok {
return ErrJobNotFound
return nil
}
switch wrap.status {
case JobDelaying:
@@ -372,6 +418,7 @@ func (r *Runner[T]) Remove(ctx context.Context, key JobKey) error {
case JobRunning:
// 统一标记为 removed, 待任务执行完成后再删除
wrap.removed = true
return ErrJobRunning
default:
}
return nil

View File

@@ -29,14 +29,19 @@ func (t *testJob) GetKey() JobKey {
return t.Key
}
func (t *testJob) SetStatus(status JobStatus, err error) {}
func (t *testJob) SetEnabled(enabled bool, desc string) {}
func TestRunner_Close(t *testing.T) {
signal := make(chan struct{}, 1)
waiting := sync.WaitGroup{}
waiting.Add(1)
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
waiting.Done()
timex.SleepWithContext(ctx, time.Hour)
signal <- struct{}{}
return nil
})
go func() {
job := &testJob{
@@ -78,8 +83,9 @@ func TestRunner_AddJob(t *testing.T) {
want: ErrJobExist,
},
}
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
timex.SleepWithContext(ctx, time.Hour)
return nil
})
defer runner.Close()
for _, tc := range testCases {
@@ -99,10 +105,11 @@ func TestJob_UpdateStatus(t *testing.T) {
running
finished
)
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
job.status = running
timex.SleepWithContext(ctx, d*2)
job.status = finished
return nil
})
first := newTestJob("first")
second := newTestJob("second")

View File

@@ -1,3 +1,13 @@
INSERT INTO `t_sys_resource` (`id`, `pid`, `ui_path`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `is_deleted`, `delete_time`)
VALUES (161, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL),
(160, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
ALTER TABLE `mayfly-go`.`t_db_backup`
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
ALTER TABLE `mayfly-go`.`t_db_restore`
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
ALTER TABLE `mayfly-go`.`t_db_backup_history`
ADD COLUMN `restoring` int(1) NOT NULL DEFAULT(0) COMMENT '备份历史恢复标识',
ADD COLUMN `deleting` int(1) NOT NULL DEFAULT(0) COMMENT '备份历史删除标识';