!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="plus" @click="createDbBackup()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</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="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
</template> </template>
<template #action="{ data }"> <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="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="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(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> </div>
</template> </template>
</page-table> </page-table>
@@ -49,7 +51,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue')); const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null); const pageTableRef: Ref<any> = ref(null);
@@ -72,10 +74,10 @@ const columns = [
TableColumn.new('name', '任务名称'), TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(), TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'), TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabled', '是否启用'), TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'), TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(), TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(), TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
]; ];
const emptyQuery = { const emptyQuery = {
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
await search(); await search();
ElMessage.success('备份任务启动成功'); 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> </script>
<style lang="scss"></style> <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: '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: '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 :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>
<el-dropdown-item <el-dropdown-item
:command="{ type: 'restoreDb', data }" :command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)" v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
> >
恢复 恢复任务
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -138,6 +144,16 @@
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" /> <db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
</el-dialog> </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 <el-dialog
width="80%" width="80%"
:title="`${dbRestoreDialog.title} - 数据库恢复`" :title="`${dbRestoreDialog.title} - 数据库恢复`"
@@ -192,6 +208,7 @@ import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag'; import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue'; import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue'; import DbRestoreList from './DbRestoreList.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue')); const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -263,6 +280,13 @@ const state = reactive({
dbs: [], dbs: [],
dbId: 0, dbId: 0,
}, },
// 数据库备份历史弹框
dbBackupHistoryDialog: {
title: '',
visible: false,
dbs: [],
dbId: 0,
},
// 数据库恢复弹框 // 数据库恢复弹框
dbRestoreDialog: { dbRestoreDialog: {
title: '', 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 () => { onMounted(async () => {
if (Object.keys(actionBtns).length > 0) { if (Object.keys(actionBtns).length > 0) {
@@ -359,6 +384,10 @@ const handleMoreActionCommand = (commond: any) => {
onShowDbBackupDialog(data); onShowDbBackupDialog(data);
return; return;
} }
case 'backupHistory': {
onShowDbBackupHistoryDialog(data);
return;
}
case 'restoreDb': { case 'restoreDb': {
onShowDbRestoreDialog(data); onShowDbRestoreDialog(data);
return; return;
@@ -412,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.visible = true; 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) => { const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`; state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id; state.dbRestoreDialog.dbId = row.id;

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,16 @@ import (
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx" "mayfly-go/pkg/ginx"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/timex"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type DbBackup struct { type DbBackup struct {
dbBackupApp *application.DbBackupApp `inject:"DbBackupApp"` backupApp *application.DbBackupApp `inject:"DbBackupApp"`
dbApp application.Db `inject:"DbApp"` dbApp application.Db `inject:"DbApp"`
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
} }
// todo: 鉴权,避免未经授权进行数据库备份和恢复 // 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") db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v") 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.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
rc.ResData = res rc.ResData = res
} }
@@ -50,11 +53,10 @@ func (d *DbBackup) Create(rc *req.Ctx) {
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId) biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId") db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
jobs := make([]*entity.DbBackup, 0, len(dbNames)) jobs := make([]*entity.DbBackup, 0, len(dbNames))
for _, dbName := range dbNames { for _, dbName := range dbNames {
job := &entity.DbBackup{ job := &entity.DbBackup{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup), DbInstanceId: db.InstanceId,
DbName: dbName, DbName: dbName,
Enabled: true, Enabled: true,
Repeated: backupForm.Repeated, Repeated: backupForm.Repeated,
@@ -64,7 +66,7 @@ func (d *DbBackup) Create(rc *req.Ctx) {
} }
jobs = append(jobs, job) jobs = append(jobs, job)
} }
biz.ErrIsNilAppendErr(d.dbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v") biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
} }
// Update 保存数据库备份任务 // Update 保存数据库备份任务
@@ -74,17 +76,17 @@ func (d *DbBackup) Update(rc *req.Ctx) {
ginx.BindJsonAndValid(rc.GinCtx, backupForm) ginx.BindJsonAndValid(rc.GinCtx, backupForm)
rc.ReqParam = backupForm rc.ReqParam = backupForm
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup) job := &entity.DbBackup{}
job.Id = backupForm.Id job.Id = backupForm.Id
job.Name = backupForm.Name job.Name = backupForm.Name
job.StartTime = backupForm.StartTime job.StartTime = backupForm.StartTime
job.Interval = backupForm.Interval 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 { func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, "backupId") idsStr := ginx.PathParam(rc.GinCtx, paramName)
biz.NotEmpty(idsStr, "backupId 为空") biz.NotEmpty(idsStr, paramName+" 为空")
rc.ReqParam = idsStr rc.ReqParam = idsStr
ids := strings.Fields(idsStr) ids := strings.Fields(idsStr)
for _, v := range ids { for _, v := range ids {
@@ -104,28 +106,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
// Delete 删除数据库备份任务 // Delete 删除数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId [DELETE] // @router /api/dbs/:dbId/backups/:backupId [DELETE]
func (d *DbBackup) Delete(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
} }
// Enable 启用数据库备份任务 // Enable 启用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT] // @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
func (d *DbBackup) Enable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
} }
// Disable 禁用数据库备份任务 // Disable 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT] // @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
func (d *DbBackup) Disable(rc *req.Ctx) { 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") biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
} }
// Start 禁用数据库备份任务 // Start 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/start [PUT] // @router /api/dbs/:dbId/backups/:backupId/start [PUT]
func (d *DbBackup) Start(rc *req.Ctx) { 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") 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") db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database) 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") biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutBackup 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") db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery)) backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
queryCond.DbInstanceId = db.InstanceId backupHistoryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) backupHistoryCond.InDbNames = strings.Fields(db.Database)
res, err := d.dbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory)) backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v") 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 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") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
var restores []vo.DbRestore 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.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database) queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.restoreApp.GetPageList(queryCond, page, &restores) res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
@@ -48,7 +48,8 @@ func (d *DbRestore) Create(rc *req.Ctx) {
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v") biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
job := &entity.DbRestore{ job := &entity.DbRestore{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore), DbInstanceId: db.InstanceId,
DbName: restoreForm.DbName,
Enabled: true, Enabled: true,
Repeated: restoreForm.Repeated, Repeated: restoreForm.Repeated,
StartTime: restoreForm.StartTime, StartTime: restoreForm.StartTime,
@@ -58,10 +59,13 @@ func (d *DbRestore) Create(rc *req.Ctx) {
DbBackupHistoryId: restoreForm.DbBackupHistoryId, DbBackupHistoryId: restoreForm.DbBackupHistoryId,
DbBackupHistoryName: restoreForm.DbBackupHistoryName, DbBackupHistoryName: restoreForm.DbBackupHistoryName,
} }
job.DbName = restoreForm.DbName
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v") biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
} }
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
}
// Update 保存数据库恢复任务 // Update 保存数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId [PUT] // @router /api/dbs/:dbId/restores/:restoreId [PUT]
func (d *DbRestore) Update(rc *req.Ctx) { func (d *DbRestore) Update(rc *req.Ctx) {

View File

@@ -2,6 +2,7 @@ package vo
import ( import (
"encoding/json" "encoding/json"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/utils/timex" "mayfly-go/pkg/utils/timex"
"time" "time"
) )
@@ -15,8 +16,9 @@ type DbBackup struct {
Interval time.Duration `json:"-"` // 间隔时间 Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数 IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间 LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态 LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果 LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称 Name string `json:"name"` // 备份任务名称
@@ -25,6 +27,13 @@ type DbBackup struct {
func (backup *DbBackup) MarshalJSON() ([]byte, error) { func (backup *DbBackup) MarshalJSON() ([]byte, error) {
type dbBackup DbBackup type dbBackup DbBackup
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24) 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)) return json.Marshal((*dbBackup)(backup))
} }
@@ -36,4 +45,7 @@ type DbBackupHistory struct {
DbName string `json:"dbName"` // 数据库名称 DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称 Name string `json:"name"` // 备份历史名称
BinlogFileName string `json:"binlogFileName"` 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:"-"` // 间隔时间 Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数 IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间 LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态 LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果 LastResult string `json:"lastResult"` // 最近一次执行结果
@@ -27,6 +28,13 @@ type DbRestore struct {
func (restore *DbRestore) MarshalJSON() ([]byte, error) { func (restore *DbRestore) MarshalJSON() ([]byte, error) {
type dbBackup DbRestore type dbBackup DbRestore
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24) 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)) return json.Marshal((*dbBackup)(restore))
} }

View File

@@ -3,9 +3,14 @@ package application
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"errors"
"fmt"
"gorm.io/gorm"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"sync"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -14,6 +19,9 @@ type DbBackupApp struct {
scheduler *dbScheduler `inject:"DbScheduler"` scheduler *dbScheduler `inject:"DbScheduler"`
backupRepo repository.DbBackup `inject:"DbBackupRepo"` backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"` backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
dbApp Db `inject:"DbApp"`
mutex sync.Mutex
} }
func (app *DbBackupApp) Init() error { func (app *DbBackupApp) Init() error {
@@ -21,7 +29,7 @@ func (app *DbBackupApp) Init() error {
if err := app.backupRepo.ListToDo(&jobs); err != nil { if err := app.backupRepo.ListToDo(&jobs); err != nil {
return err 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 err
} }
return nil return nil
@@ -32,32 +40,111 @@ func (app *DbBackupApp) Close() {
} }
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error { 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 { 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 { func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库备份历史文件 // 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 { 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 { 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 { func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId) 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 分页获取数据库备份任务 // 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...) return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
} }
@@ -68,7 +155,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
// GetHistoryPageList 分页获取数据库备份历史 // GetHistoryPageList 分页获取数据库备份历史
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { 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) { func NewIncUUID() (uuid.UUID, error) {
@@ -91,3 +182,41 @@ func NewIncUUID() (uuid.UUID, error) {
return uid, nil 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() { if app.closed() {
break 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()) logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
} }
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval) timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)

View File

@@ -2,15 +2,19 @@ package application
import ( import (
"context" "context"
"errors"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"sync"
) )
type DbRestoreApp struct { type DbRestoreApp struct {
scheduler *dbScheduler `inject:"DbScheduler"` scheduler *dbScheduler `inject:"DbScheduler"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"` restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"` restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
mutex sync.Mutex
} }
func (app *DbRestoreApp) Init() error { func (app *DbRestoreApp) Init() error {
@@ -18,7 +22,7 @@ func (app *DbRestoreApp) Init() error {
if err := app.restoreRepo.ListToDo(&jobs); err != nil { if err := app.restoreRepo.ListToDo(&jobs); err != nil {
return err 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 err
} }
return nil return nil
@@ -28,32 +32,101 @@ func (app *DbRestoreApp) Close() {
app.scheduler.Close() app.scheduler.Close()
} }
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error { func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job) 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 { 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 { func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库恢复历史文件 // 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 { 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 { 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 分页获取数据库恢复任务 // 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...) 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 获取未配置定时恢复的数据库名称 // GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) { func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames) return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)

View File

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

View File

@@ -19,6 +19,8 @@ type DbProgram interface {
RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error 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) 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 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 { func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
dbInfo := svc.dbInfo() dbInfo := svc.dbInfo()
args := []string{ args := []string{

View File

@@ -9,20 +9,30 @@ var _ DbJob = (*DbBackup)(nil)
// DbBackup 数据库备份任务 // DbBackup 数据库备份任务
type DbBackup struct { type DbBackup struct {
*DbJobBaseImpl DbJobBaseImpl
DbInstanceId uint64 // 数据库实例ID
DbName string // 数据库名称
Name string // 数据库备份名称
Enabled bool // 是否启用 Enabled bool // 是否启用
EnabledDesc string // 启用状态描述
StartTime time.Time // 开始时间 StartTime time.Time // 开始时间
Interval time.Duration // 间隔时间 Interval time.Duration // 间隔时间
Repeated bool // 是否重复执行 Repeated bool // 是否重复执行
DbName string // 数据库名称 }
Name string // 数据库备份名称
func (b *DbBackup) GetInstanceId() uint64 {
return b.DbInstanceId
} }
func (b *DbBackup) GetDbName() string { func (b *DbBackup) GetDbName() string {
return b.DbName return b.DbName
} }
func (b *DbBackup) GetJobType() DbJobType {
return DbJobTypeBackup
}
func (b *DbBackup) Schedule() (time.Time, error) { func (b *DbBackup) Schedule() (time.Time, error) {
if b.IsFinished() { if b.IsFinished() {
return time.Time{}, runner.ErrJobFinished return time.Time{}, runner.ErrJobFinished
@@ -37,7 +47,7 @@ func (b *DbBackup) Schedule() (time.Time, error) {
lastTime = b.StartTime.Add(-b.Interval) lastTime = b.StartTime.Add(-b.Interval)
} }
return lastTime.Add(b.Interval - lastTime.Sub(b.StartTime)%b.Interval), nil return lastTime.Add(b.Interval - lastTime.Sub(b.StartTime)%b.Interval), nil
case DbJobFailed: case DbJobRunning, DbJobFailed:
return time.Now().Add(time.Minute), nil return time.Now().Add(time.Minute), nil
default: default:
return b.StartTime, nil return b.StartTime, nil
@@ -52,8 +62,13 @@ func (b *DbBackup) IsEnabled() bool {
return b.Enabled 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.Enabled = enabled
b.EnabledDesc = desc
} }
func (b *DbBackup) Update(job runner.Job) { func (b *DbBackup) Update(job runner.Job) {
@@ -65,3 +80,15 @@ func (b *DbBackup) Update(job runner.Job) {
func (b *DbBackup) GetInterval() time.Duration { func (b *DbBackup) GetInterval() time.Duration {
return b.Interval 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 数据库备份任务 // DbBinlog 数据库备份任务
type DbBinlog struct { type DbBinlog struct {
DbJobBaseImpl DbJobBaseImpl
DbInstanceId uint64 // 数据库实例ID
} }
func NewDbBinlog(instanceId uint64) *DbBinlog { func NewDbBinlog(instanceId uint64) *DbBinlog {
@@ -35,13 +36,17 @@ func NewDbBinlog(instanceId uint64) *DbBinlog {
return job return job
} }
func (b *DbBinlog) GetInstanceId() uint64 {
return b.DbInstanceId
}
func (b *DbBinlog) GetDbName() string { func (b *DbBinlog) GetDbName() string {
// binlog 是全库级别的 // binlog 是全库级别的
return "" return ""
} }
func (b *DbBinlog) Schedule() (time.Time, error) { func (b *DbBinlog) Schedule() (time.Time, error) {
switch b.GetJobBase().LastStatus { switch b.LastStatus {
case DbJobSuccess: case DbJobSuccess:
return time.Time{}, runner.ErrJobFinished return time.Time{}, runner.ErrJobFinished
case DbJobFailed: case DbJobFailed:
@@ -57,8 +62,28 @@ func (b *DbBinlog) IsEnabled() bool {
return true 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 { func (b *DbBinlog) GetInterval() time.Duration {
return 0 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 DbJobKey = runner.JobKey
type DbJobStatus int type DbJobStatus = runner.JobStatus
const ( const (
DbJobRunning DbJobStatus = iota DbJobRunning = runner.JobRunning
DbJobSuccess DbJobSuccess = runner.JobSuccess
DbJobFailed DbJobFailed = runner.JobFailed
) )
type DbJobType string type DbJobType string
@@ -28,12 +28,14 @@ func (typ DbJobType) String() string {
} }
const ( const (
DbJobUnknown DbJobType = "db-unknown"
DbJobTypeBackup DbJobType = "db-backup" DbJobTypeBackup DbJobType = "db-backup"
DbJobTypeRestore DbJobType = "db-restore" DbJobTypeRestore DbJobType = "db-restore"
DbJobTypeBinlog DbJobType = "db-binlog" DbJobTypeBinlog DbJobType = "db-binlog"
) )
const ( const (
DbJobNameUnknown = "未知任务"
DbJobNameBackup = "数据库备份" DbJobNameBackup = "数据库备份"
DbJobNameRestore = "数据库恢复" DbJobNameRestore = "数据库恢复"
DbJobNameBinlog = "BINLOG同步" DbJobNameBinlog = "BINLOG同步"
@@ -43,41 +45,24 @@ var _ runner.Job = (DbJob)(nil)
type DbJobBase interface { type DbJobBase interface {
model.ModelI model.ModelI
GetLastStatus() DbJobStatus
GetKey() string
GetJobType() DbJobType
SetJobType(typ DbJobType)
GetJobBase() *DbJobBaseImpl
SetLastStatus(status DbJobStatus, err error)
} }
type DbJob interface { type DbJob interface {
runner.Job runner.Job
DbJobBase DbJobBase
GetInstanceId() uint64
GetKey() string
GetJobType() DbJobType
GetDbName() string GetDbName() string
Schedule() (time.Time, error) Schedule() (time.Time, error)
IsEnabled() bool IsEnabled() bool
SetEnabled(enabled bool) IsExpired() bool
SetEnabled(enabled bool, desc string)
Update(job runner.Job) Update(job runner.Job)
GetInterval() time.Duration GetInterval() time.Duration
} SetLastStatus(status DbJobStatus, err error)
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))
}
} }
var _ DbJobBase = (*DbJobBaseImpl)(nil) var _ DbJobBase = (*DbJobBaseImpl)(nil)
@@ -85,30 +70,25 @@ var _ DbJobBase = (*DbJobBaseImpl)(nil)
type DbJobBaseImpl struct { type DbJobBaseImpl struct {
model.Model model.Model
DbInstanceId uint64 // 数据库实例ID
LastStatus DbJobStatus // 最近一次执行状态 LastStatus DbJobStatus // 最近一次执行状态
LastResult string // 最近一次执行结果 LastResult string // 最近一次执行结果
LastTime timex.NullTime // 最近一次执行时间 LastTime timex.NullTime // 最近一次执行时间
jobType DbJobType
jobKey runner.JobKey jobKey runner.JobKey
} }
func NewDbBJobBase(instanceId uint64, jobType DbJobType) *DbJobBaseImpl { func (d *DbJobBaseImpl) getJobType() DbJobType {
return &DbJobBaseImpl{ job, ok := any(d).(DbJob)
DbInstanceId: instanceId, if !ok {
jobType: jobType, return DbJobUnknown
} }
return job.GetJobType()
} }
func (d *DbJobBaseImpl) GetJobType() DbJobType { func (d *DbJobBaseImpl) GetLastStatus() DbJobStatus {
return d.jobType return d.LastStatus
} }
func (d *DbJobBaseImpl) SetJobType(typ DbJobType) { func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
d.jobType = typ
}
func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
var statusName, jobName string var statusName, jobName string
switch status { switch status {
case DbJobRunning: case DbJobRunning:
@@ -120,7 +100,8 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
default: default:
return return
} }
switch d.jobType {
switch jobType {
case DbJobTypeBackup: case DbJobTypeBackup:
jobName = DbJobNameBackup jobName = DbJobNameBackup
case DbJobTypeRestore: case DbJobTypeRestore:
@@ -128,7 +109,7 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
case DbJobTypeBinlog: case DbJobTypeBinlog:
jobName = DbJobNameBinlog jobName = DbJobNameBinlog
default: default:
jobName = d.jobType.String() jobName = jobType.String()
} }
d.LastStatus = status d.LastStatus = status
var result = jobName + statusName var result = jobName + statusName
@@ -139,17 +120,13 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
d.LastTime = timex.NewNullTime(time.Now()) d.LastTime = timex.NewNullTime(time.Now())
} }
func (d *DbJobBaseImpl) GetJobBase() *DbJobBaseImpl {
return d
}
func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey { func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey {
return fmt.Sprintf("%v-%d", typ, jobId) return fmt.Sprintf("%v-%d", typ, jobId)
} }
func (d *DbJobBaseImpl) GetKey() DbJobKey { func (d *DbJobBaseImpl) getKey(jobType DbJobType) DbJobKey {
if len(d.jobKey) == 0 { if len(d.jobKey) == 0 {
d.jobKey = FormatJobKey(d.jobType, d.Id) d.jobKey = FormatJobKey(jobType, d.Id)
} }
return d.jobKey return d.jobKey
} }

View File

@@ -10,10 +10,12 @@ var _ DbJob = (*DbRestore)(nil)
// DbRestore 数据库恢复任务 // DbRestore 数据库恢复任务
type DbRestore struct { type DbRestore struct {
*DbJobBaseImpl DbJobBaseImpl
DbInstanceId uint64 // 数据库实例ID
DbName string // 数据库名称 DbName string // 数据库名称
Enabled bool // 是否启用 Enabled bool // 是否启用
EnabledDesc string // 启用状态描述
StartTime time.Time // 开始时间 StartTime time.Time // 开始时间
Interval time.Duration // 间隔时间 Interval time.Duration // 间隔时间
Repeated bool // 是否重复执行 Repeated bool // 是否重复执行
@@ -23,6 +25,10 @@ type DbRestore struct {
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称 DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称
} }
func (r *DbRestore) GetInstanceId() uint64 {
return r.DbInstanceId
}
func (r *DbRestore) GetDbName() string { func (r *DbRestore) GetDbName() string {
return r.DbName return r.DbName
} }
@@ -36,7 +42,7 @@ func (r *DbRestore) Schedule() (time.Time, error) {
return time.Time{}, runner.ErrJobFinished return time.Time{}, runner.ErrJobFinished
default: default:
if time.Now().Sub(r.StartTime) > time.Hour { if time.Now().Sub(r.StartTime) > time.Hour {
return time.Time{}, runner.ErrJobTimeout return time.Time{}, runner.ErrJobExpired
} }
return r.StartTime, nil return r.StartTime, nil
} }
@@ -46,8 +52,13 @@ func (r *DbRestore) IsEnabled() bool {
return r.Enabled return r.Enabled
} }
func (r *DbRestore) SetEnabled(enabled bool) { func (r *DbRestore) SetEnabled(enabled bool, desc string) {
r.Enabled = enabled 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 { func (r *DbRestore) IsFinished() bool {
@@ -63,3 +74,19 @@ func (r *DbRestore) Update(job runner.Job) {
func (r *DbRestore) GetInterval() time.Duration { func (r *DbRestore) GetInterval() time.Duration {
return r.Interval 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 CreatorId uint64
} }
// DbJobQuery 数据库备份任务查询 // DbBackupQuery 数据库备份任务查询
type DbJobQuery struct { type DbBackupQuery struct {
Id uint64 `json:"id" form:"id"` Id uint64 `json:"id" form:"id"`
DbName string `json:"dbName" form:"dbName"` DbName string `json:"dbName" form:"dbName"`
IntervalDay int `json:"intervalDay" form:"intervalDay"` IntervalDay int `json:"intervalDay" form:"intervalDay"`
@@ -61,13 +61,13 @@ type DbBackupHistoryQuery struct {
} }
// DbRestoreQuery 数据库备份任务查询 // DbRestoreQuery 数据库备份任务查询
//type DbRestoreQuery struct { type DbRestoreQuery struct {
// Id uint64 `json:"id" form:"id"` Id uint64 `json:"id" form:"id"`
// DbName string `json:"dbName" form:"dbName"` DbName string `json:"dbName" form:"dbName"`
// InDbNames []string `json:"-" form:"-"` InDbNames []string `json:"-" form:"-"`
// DbInstanceId uint64 `json:"-" form:"-"` DbInstanceId uint64 `json:"-" form:"-"`
// Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行 Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行
//} }
// DbRestoreHistoryQuery 数据库备份任务查询 // DbRestoreHistoryQuery 数据库备份任务查询
type DbRestoreHistoryQuery struct { type DbRestoreHistoryQuery struct {

View File

@@ -13,5 +13,5 @@ type DbBackup interface {
GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
// GetPageList 分页获取数据库任务列表 // 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 { type DbBackupHistory interface {
base.Repo[*entity.DbBackupHistory] base.Repo[*entity.DbBackupHistory]
// GetHistories 分页获取数据备份历史 // GetPageList 分页获取数据备份历史
GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) 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) GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error)
GetEarliestHistory(instanceId uint64) (*entity.DbBackupHistory, bool, 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) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
// GetPageList 分页获取数据库任务列表 // 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 分页获取数据库备份任务列表 // 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() d.GetModel()
qd := gormx.NewQuery(d.GetModel()). qd := gormx.NewQuery(d.GetModel()).
Eq("id", condition.Id). Eq("id", condition.Id).
@@ -83,7 +83,12 @@ func (d *dbBackupRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enable
cond := map[string]any{ cond := map[string]any{
"id": jobId, "id": jobId,
} }
desc := "任务已禁用"
if enabled {
desc = "任务已启用"
}
return d.Updates(cond, map[string]any{ 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{} return &dbBackupHistoryRepoImpl{}
} }
func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { func (repo *dbBackupHistoryRepoImpl) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.DbBackupHistory)). qd := gormx.NewQuery(repo.GetModel()).
Eq("id", condition.Id). Eq("id", condition.Id).
Eq0("db_instance_id", condition.DbInstanceId). Eq0("db_instance_id", condition.DbInstanceId).
In0("db_name", condition.InDbNames). In0("db_name", condition.InDbNames).
@@ -31,6 +31,14 @@ func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHist
return gormx.PageQuery(qd, pageParam, toEntity) 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) { func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error) {
history := &entity.DbBackupHistory{} history := &entity.DbBackupHistory{}
db := global.Db db := global.Db
@@ -65,3 +73,33 @@ func (repo *dbBackupHistoryRepoImpl) GetEarliestHistory(instanceId uint64) (*ent
return nil, false, err 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 { 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 { return gormx.Tx(func(db *gorm.DB) error {
var instanceId uint64 var instanceId uint64
var dbNames []string 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) dbNames = make([]string, 0, reflectLen)
for i := 0; i < reflectLen; i++ { for i := 0; i < reflectLen; i++ {
job := reflectValue.Index(i).Interface().(entity.DbJob) job := reflectValue.Index(i).Interface().(entity.DbJob)
jobBase := job.GetJobBase()
if instanceId == 0 { if instanceId == 0 {
instanceId = jobBase.DbInstanceId instanceId = job.GetInstanceId()
} }
if jobBase.DbInstanceId != instanceId { if job.GetInstanceId() != instanceId {
return errors.New("不支持同时为多个数据库实例添加数据库任务") return errors.New("不支持同时为多个数据库实例添加数据库任务")
} }
if job.GetInterval() == 0 { if job.GetInterval() == 0 {
@@ -59,8 +58,7 @@ func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any
} }
default: default:
job := jobs.(entity.DbJob) job := jobs.(entity.DbJob)
jobBase := job.GetJobBase() instanceId = job.GetInstanceId()
instanceId = jobBase.DbInstanceId
if job.GetInterval() > 0 { if job.GetInterval() > 0 {
dbNames = append(dbNames, job.GetDbName()) dbNames = append(dbNames, job.GetDbName())
} }

View File

@@ -54,8 +54,7 @@ func (d *dbRestoreRepoImpl) ListToDo(jobs any) error {
} }
// GetPageList 分页获取数据库备份任务列表 // GetPageList 分页获取数据库备份任务列表
func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) { func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
d.GetModel()
qd := gormx.NewQuery(d.GetModel()). qd := gormx.NewQuery(d.GetModel()).
Eq("id", condition.Id). Eq("id", condition.Id).
Eq0("db_instance_id", condition.DbInstanceId). Eq0("db_instance_id", condition.DbInstanceId).
@@ -65,6 +64,17 @@ func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam
return gormx.PageQuery(qd, pageParam, toEntity) 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 添加数据库任务 // AddJob 添加数据库任务
func (d *dbRestoreRepoImpl) AddJob(ctx context.Context, jobs any) error { func (d *dbRestoreRepoImpl) AddJob(ctx context.Context, jobs any) error {
return addJob[*entity.DbRestore](ctx, d.dbJobBaseImpl, jobs) 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{ cond := map[string]any{
"id": jobId, "id": jobId,
} }
desc := "任务已禁用"
if enabled {
desc = "任务已启用"
}
return d.Updates(cond, map[string]any{ 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.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) req.BatchSetGroup(dbs, reqs)

View File

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

View File

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

View File

@@ -29,14 +29,19 @@ func (t *testJob) GetKey() JobKey {
return t.Key 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) { func TestRunner_Close(t *testing.T) {
signal := make(chan struct{}, 1) signal := make(chan struct{}, 1)
waiting := sync.WaitGroup{} waiting := sync.WaitGroup{}
waiting.Add(1) 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() waiting.Done()
timex.SleepWithContext(ctx, time.Hour) timex.SleepWithContext(ctx, time.Hour)
signal <- struct{}{} signal <- struct{}{}
return nil
}) })
go func() { go func() {
job := &testJob{ job := &testJob{
@@ -78,8 +83,9 @@ func TestRunner_AddJob(t *testing.T) {
want: ErrJobExist, 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) timex.SleepWithContext(ctx, time.Hour)
return nil
}) })
defer runner.Close() defer runner.Close()
for _, tc := range testCases { for _, tc := range testCases {
@@ -99,10 +105,11 @@ func TestJob_UpdateStatus(t *testing.T) {
running running
finished 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 job.status = running
timex.SleepWithContext(ctx, d*2) timex.SleepWithContext(ctx, d*2)
job.status = finished job.status = finished
return nil
}) })
first := newTestJob("first") first := newTestJob("first")
second := newTestJob("second") 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`) 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), 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); (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 '备份历史删除标识';