mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
!96 删除数据库备份和恢复历史
* feat: 删除数据库备份历史 * refactor dbScheduler * feat: 从数据库备份历史中恢复数据库 * feat: 删除数据库恢复历史记录 * refactor dbScheuler
This commit is contained in:
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal 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>
|
||||
@@ -21,6 +21,7 @@
|
||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -29,6 +30,7 @@
|
||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
||||
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</page-table>
|
||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -72,10 +74,10 @@ const columns = [
|
||||
TableColumn.new('name', '任务名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('intervalDay', '备份周期'),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
|
||||
await search();
|
||||
ElMessage.success('备份任务启动成功');
|
||||
};
|
||||
|
||||
const deleteDbBackup = async (data: any) => {
|
||||
let backupId: string;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库备份任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -63,13 +63,19 @@
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||
备份
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -138,6 +144,16 @@
|
||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbBackupHistoryDialog.visible"
|
||||
>
|
||||
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||
@@ -192,6 +208,7 @@ import { getDbDialect } from './dialect/index';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import DbBackupList from './DbBackupList.vue';
|
||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||
import DbRestoreList from './DbRestoreList.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
@@ -263,6 +280,13 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份历史弹框
|
||||
dbBackupHistoryDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库恢复弹框
|
||||
dbRestoreDialog: {
|
||||
title: '',
|
||||
@@ -295,7 +319,8 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
|
||||
toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -359,6 +384,10 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
onShowDbBackupDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'backupHistory': {
|
||||
onShowDbBackupHistoryDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'restoreDb': {
|
||||
onShowDbRestoreDialog(data);
|
||||
return;
|
||||
@@ -412,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -161,7 +161,7 @@ const state = reactive({
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbName: null as any,
|
||||
intervalDay: 1,
|
||||
intervalDay: 0,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
dbBackupId: null as any,
|
||||
@@ -233,7 +233,8 @@ const init = async (data: any) => {
|
||||
} else {
|
||||
state.form.dbName = '';
|
||||
state.editOrCreate = false;
|
||||
state.form.intervalDay = 1;
|
||||
state.form.intervalDay = 0;
|
||||
state.form.repeated = false;
|
||||
state.form.pointInTime = new Date();
|
||||
state.form.startTime = new Date();
|
||||
state.histories = [];
|
||||
@@ -252,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
|
||||
const btnOk = async () => {
|
||||
restoreForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
if (state.restoreMode == 'point-in-time') {
|
||||
state.form.dbBackupId = 0;
|
||||
state.form.dbBackupHistoryId = 0;
|
||||
@@ -260,13 +267,14 @@ const btnOk = async () => {
|
||||
state.form.pointInTime = null;
|
||||
}
|
||||
state.form.repeated = false;
|
||||
state.form.intervalDay = 0;
|
||||
const reqForm = { ...state.form };
|
||||
let api = dbApi.createDbRestore;
|
||||
if (props.data) {
|
||||
api = dbApi.saveDbRestore;
|
||||
}
|
||||
api.request(reqForm).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -49,7 +51,7 @@
|
||||
infoDialog.data.dbBackupHistoryName
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
|
||||
state.dbRestoreEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteDbRestore = async (data: any) => {
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
const showDbRestore = async (data: any) => {
|
||||
state.infoDialog.data = data;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const enableDbRestore = async (data: any) => {
|
||||
let restoreId: String;
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要启用的恢复任务');
|
||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
|
||||
};
|
||||
|
||||
const disableDbRestore = async (data: any) => {
|
||||
let restoreId: String;
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要禁用的恢复任务');
|
||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
|
||||
@@ -49,16 +49,20 @@ export const dbApi = {
|
||||
// 获取数据库备份列表
|
||||
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
||||
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
|
||||
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
|
||||
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
||||
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
|
||||
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
|
||||
|
||||
// 获取数据库备份列表
|
||||
// 获取数据库恢复列表
|
||||
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
||||
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
|
||||
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
|
||||
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbBackup struct {
|
||||
dbBackupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
||||
@@ -28,10 +31,10 @@ func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.dbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -50,21 +53,20 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
job := &entity.DbBackup{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
|
||||
DbName: dbName,
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: dbName,
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.dbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Update 保存数据库备份任务
|
||||
@@ -74,17 +76,17 @@ func (d *DbBackup) Update(rc *req.Ctx) {
|
||||
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||
rc.ReqParam = backupForm
|
||||
|
||||
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
|
||||
job := &entity.DbBackup{}
|
||||
job.Id = backupForm.Id
|
||||
job.Name = backupForm.Name
|
||||
job.StartTime = backupForm.StartTime
|
||||
job.Interval = backupForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.dbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
|
||||
biz.NotEmpty(idsStr, "backupId 为空")
|
||||
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, paramName)
|
||||
biz.NotEmpty(idsStr, paramName+" 为空")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Fields(idsStr)
|
||||
for _, v := range ids {
|
||||
@@ -104,28 +106,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
|
||||
// Delete 删除数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
||||
func (d *DbBackup) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.dbBackupApp.Delete)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
||||
func (d *DbBackup) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.dbBackupApp.Enable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
||||
func (d *DbBackup) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.dbBackupApp.Disable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Start 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
||||
func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.dbBackupApp.Start)
|
||||
err := d.walk(rc, "backupId", d.backupApp.StartNow)
|
||||
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
@@ -136,7 +138,7 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutBackup, err := d.dbBackupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutBackup
|
||||
}
|
||||
@@ -149,10 +151,71 @@ func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.dbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
|
||||
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
backupHistoryCond.DbInstanceId = db.InstanceId
|
||||
backupHistoryCond.InDbNames = strings.Fields(db.Database)
|
||||
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
|
||||
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
historyIds := make([]uint64, 0, len(backupHistories))
|
||||
for _, history := range backupHistories {
|
||||
historyIds = append(historyIds, history.Id)
|
||||
}
|
||||
restores := make([]*entity.DbRestore, 0, page.PageSize)
|
||||
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
|
||||
}
|
||||
for _, history := range backupHistories {
|
||||
for _, restore := range restores {
|
||||
if restore.DbBackupHistoryId == history.Id {
|
||||
history.LastStatus = restore.LastStatus
|
||||
history.LastResult = restore.LastResult
|
||||
history.LastTime = restore.LastTime
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// RestoreHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
||||
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
||||
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
|
||||
biz.NotEmpty(pm, "backupHistoryId 为空")
|
||||
idsStr := strings.Fields(pm)
|
||||
ids := make([]uint64, 0, len(idsStr))
|
||||
for _, s := range idsStr {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
|
||||
ids = append(ids, id)
|
||||
}
|
||||
histories := make([]*entity.DbBackupHistory, 0, len(ids))
|
||||
err := d.backupApp.GetHistories(ids, &histories)
|
||||
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
|
||||
restores := make([]*entity.DbRestore, 0, len(histories))
|
||||
now := time.Now()
|
||||
for _, history := range histories {
|
||||
job := &entity.DbRestore{
|
||||
DbInstanceId: history.DbInstanceId,
|
||||
DbName: history.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: now,
|
||||
Interval: 0,
|
||||
PointInTime: timex.NewNullTime(time.Time{}),
|
||||
DbBackupId: history.DbBackupId,
|
||||
DbBackupHistoryId: history.Id,
|
||||
DbBackupHistoryName: history.Name,
|
||||
}
|
||||
restores = append(restores, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// DeleteHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
|
||||
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
var restores []vo.DbRestore
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
|
||||
@@ -48,7 +48,8 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
job := &entity.DbRestore{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: restoreForm.DbName,
|
||||
Enabled: true,
|
||||
Repeated: restoreForm.Repeated,
|
||||
StartTime: restoreForm.StartTime,
|
||||
@@ -58,10 +59,13 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||
}
|
||||
job.DbName = restoreForm.DbName
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
|
||||
|
||||
}
|
||||
|
||||
// Update 保存数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
|
||||
func (d *DbRestore) Update(rc *req.Ctx) {
|
||||
|
||||
@@ -2,38 +2,50 @@ package vo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
}
|
||||
|
||||
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbBackup
|
||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||
if len(backup.EnabledDesc) == 0 {
|
||||
if backup.Enabled {
|
||||
backup.EnabledDesc = "任务已启用"
|
||||
} else {
|
||||
backup.EnabledDesc = "任务已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(backup))
|
||||
}
|
||||
|
||||
// DbBackupHistory 数据库备份历史
|
||||
type DbBackupHistory struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
|
||||
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type DbRestore struct {
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
@@ -27,6 +28,13 @@ type DbRestore struct {
|
||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbRestore
|
||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
||||
if len(restore.EnabledDesc) == 0 {
|
||||
if restore.Enabled {
|
||||
restore.EnabledDesc = "任务已启用"
|
||||
} else {
|
||||
restore.EnabledDesc = "任务已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(restore))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -14,6 +19,9 @@ type DbBackupApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Init() error {
|
||||
@@ -21,7 +29,7 @@ func (app *DbBackupApp) Init() error {
|
||||
if err := app.backupRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -32,32 +40,111 @@ func (app *DbBackupApp) Close() {
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.scheduler.AddJob(ctx, jobs)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库备份历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbBackupHistory{
|
||||
DbBackupId: jobId,
|
||||
}
|
||||
err := app.backupHistoryRepo.GetBy(history, "name")
|
||||
switch {
|
||||
default:
|
||||
return err
|
||||
case err == nil:
|
||||
return fmt.Errorf("数据库备份存在历史记录【%s】,无法删除该任务", history.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
}
|
||||
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
|
||||
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
job := &entity.DbBackup{}
|
||||
if err := app.backupRepo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = app.scheduler.StartJobNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
@@ -68,7 +155,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
|
||||
|
||||
// GetHistoryPageList 分页获取数据库备份历史
|
||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
|
||||
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
|
||||
}
|
||||
|
||||
func NewIncUUID() (uuid.UUID, error) {
|
||||
@@ -91,3 +182,41 @@ func NewIncUUID() (uuid.UUID, error) {
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
||||
// todo: 删除数据库备份历史文件
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = app.backupHistoryRepo.UpdateDeleting(false, historyId)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("正在从备份历史中恢复数据库")
|
||||
}
|
||||
job := &entity.DbBackupHistory{}
|
||||
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.backupHistoryRepo.DeleteById(ctx, historyId)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (app *DbBinlogApp) run() {
|
||||
if app.closed() {
|
||||
break
|
||||
}
|
||||
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
|
||||
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||
}
|
||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
||||
|
||||
@@ -2,15 +2,19 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DbRestoreApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Init() error {
|
||||
@@ -18,7 +22,7 @@ func (app *DbRestoreApp) Init() error {
|
||||
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -28,32 +32,101 @@ func (app *DbRestoreApp) Close() {
|
||||
app.scheduler.Close()
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.AddJob(ctx, jobs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库恢复历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbRestoreHistory{
|
||||
DbRestoreId: jobId,
|
||||
}
|
||||
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
// GetRestoresEnabled 获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
|
||||
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/runner"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -35,6 +35,7 @@ func newDbScheduler() *dbScheduler {
|
||||
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
||||
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
||||
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
||||
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
|
||||
)
|
||||
return scheduler
|
||||
}
|
||||
@@ -43,27 +44,11 @@ func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
||||
return job.Schedule()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
|
||||
switch typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
return s.backupRepo
|
||||
case entity.DbJobTypeRestore:
|
||||
return s.restoreRepo
|
||||
case entity.DbJobTypeBinlog:
|
||||
return s.binlogRepo
|
||||
default:
|
||||
panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.UpdateOrAdd(ctx, job)
|
||||
_ = s.runner.Update(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,28 +56,20 @@ func (s *dbScheduler) Close() {
|
||||
s.runner.Close()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if saving {
|
||||
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reflectValue := reflect.ValueOf(jobs)
|
||||
switch reflectValue.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
reflectLen := reflectValue.Len()
|
||||
for i := 0; i < reflectLen; i++ {
|
||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
default:
|
||||
job := jobs.(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
return nil
|
||||
@@ -103,29 +80,16 @@ func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, j
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
|
||||
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
job.SetEnabled(true)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Add(ctx, job)
|
||||
return nil
|
||||
}
|
||||
@@ -134,37 +98,19 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, job.GetKey())
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := s.repo(jobType).GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = s.runner.StartNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
||||
id, err := NewIncUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -176,19 +122,14 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
DbInstanceId: backup.DbInstanceId,
|
||||
DbName: backup.DbName,
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
name := backup.Name
|
||||
if len(name) == 0 {
|
||||
name = backup.DbName
|
||||
name := backup.DbName
|
||||
if len(backup.Name) > 0 {
|
||||
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
||||
}
|
||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||
history.CreateTime = now
|
||||
@@ -202,54 +143,59 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
|
||||
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
||||
restore := job.(*entity.DbRestore)
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
if restore.PointInTime.Valid {
|
||||
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG")
|
||||
}
|
||||
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG 行模式")
|
||||
}
|
||||
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
latestBinlogSequence = binlogHistory.Sequence
|
||||
} else {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
|
||||
//if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
// return err
|
||||
//} else if !enabled {
|
||||
// return errors.New("数据库未启用 BINLOG")
|
||||
//}
|
||||
//if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
// return err
|
||||
//} else if !enabled {
|
||||
// return errors.New("数据库未启用 BINLOG 行模式")
|
||||
//}
|
||||
//
|
||||
//latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
//binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//if ok {
|
||||
// latestBinlogSequence = binlogHistory.Sequence
|
||||
//} else {
|
||||
// backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if !ok {
|
||||
// return nil
|
||||
// }
|
||||
// earliestBackupSequence = backupHistory.BinlogSequence
|
||||
//}
|
||||
//binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
|
||||
// return err
|
||||
//}
|
||||
if err := s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, restore); err != nil {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("备份历史已删除")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -264,76 +210,108 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
|
||||
job.SetLastStatus(entity.DbJobRunning, nil)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
}
|
||||
//func (s *dbScheduler) updateLastStatus(ctx context.Context, job entity.DbJob) error {
|
||||
// switch typ := job.GetJobType(); typ {
|
||||
// case entity.DbJobTypeBackup:
|
||||
// return s.backupRepo.UpdateLastStatus(ctx, job)
|
||||
// case entity.DbJobTypeRestore:
|
||||
// return s.restoreRepo.UpdateLastStatus(ctx, job)
|
||||
// case entity.DbJobTypeBinlog:
|
||||
// return s.binlogRepo.UpdateLastStatus(ctx, job)
|
||||
// default:
|
||||
// panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
|
||||
// }
|
||||
//}
|
||||
|
||||
var errRun error
|
||||
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||
switch typ := job.GetJobType(); typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
errRun = s.backupMysql(ctx, job)
|
||||
return s.backupRepo.UpdateById(ctx, job)
|
||||
case entity.DbJobTypeRestore:
|
||||
errRun = s.restoreMysql(ctx, job)
|
||||
return s.restoreRepo.UpdateById(ctx, job)
|
||||
case entity.DbJobTypeBinlog:
|
||||
errRun = s.fetchBinlogMysql(ctx, job)
|
||||
return s.binlogRepo.UpdateById(ctx, job)
|
||||
default:
|
||||
errRun = fmt.Errorf("无效的数据库任务类型: %v", typ)
|
||||
}
|
||||
status := entity.DbJobSuccess
|
||||
if errRun != nil {
|
||||
status = entity.DbJobFailed
|
||||
}
|
||||
job.SetLastStatus(status, errRun)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) bool {
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
||||
//job.SetLastStatus(entity.DbJobRunning, nil)
|
||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
||||
// logx.Errorf("failed to update job status: %v", err)
|
||||
// return
|
||||
//}
|
||||
|
||||
//var errRun error
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
switch typ := job.GetJobType(); typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
return s.backup(ctx, dbProgram, job)
|
||||
case entity.DbJobTypeRestore:
|
||||
return s.restore(ctx, dbProgram, job)
|
||||
case entity.DbJobTypeBinlog:
|
||||
return s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), false)
|
||||
default:
|
||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
||||
}
|
||||
//status := entity.DbJobSuccess
|
||||
//if errRun != nil {
|
||||
// status = entity.DbJobFailed
|
||||
//}
|
||||
//job.SetLastStatus(status, errRun)
|
||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
||||
// logx.Errorf("failed to update job status: %v", err)
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
||||
if job.IsExpired() {
|
||||
return false, runner.ErrJobExpired
|
||||
}
|
||||
const maxCountByInstanceId = 4
|
||||
const maxCountByDbName = 1
|
||||
var countByInstanceId, countByDbName int
|
||||
jobBase := job.GetJobBase()
|
||||
for item, ok := next(); ok; item, ok = next() {
|
||||
itemBase := item.GetJobBase()
|
||||
if jobBase.DbInstanceId == itemBase.DbInstanceId {
|
||||
if job.GetInstanceId() == item.GetInstanceId() {
|
||||
countByInstanceId++
|
||||
if countByInstanceId >= maxCountByInstanceId {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if relatedToBinlog(job.GetJobType()) {
|
||||
// todo: 恢复数据库前触发 BINLOG 同步,BINLOG 同步完成后才能恢复数据库
|
||||
if relatedToBinlog(item.GetJobType()) {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if job.GetDbName() == item.GetDbName() {
|
||||
countByDbName++
|
||||
if countByDbName >= maxCountByDbName {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func relatedToBinlog(typ entity.DbJobType) bool {
|
||||
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
||||
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -362,40 +340,63 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
||||
TargetPosition: target.Position,
|
||||
TargetTime: job.PointInTime.Time,
|
||||
}
|
||||
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
|
||||
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
|
||||
backup := &entity.DbBackup{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(backupHistory.DbInstanceId, entity.DbJobTypeBackup),
|
||||
DbName: backupHistory.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: time.Now(),
|
||||
Interval: 0,
|
||||
Name: fmt.Sprintf("%s-系统自动备份", backupHistory.DbName),
|
||||
DbInstanceId: backupHistory.DbInstanceId,
|
||||
DbName: backupHistory.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: time.Now(),
|
||||
Interval: 0,
|
||||
Name: "系统备份",
|
||||
}
|
||||
backup.Id = backupHistory.DbBackupId
|
||||
if err := s.backupMysql(ctx, backup); err != nil {
|
||||
if err := s.backup(ctx, dbProgram, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
||||
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("关联的数据库备份历史已删除")
|
||||
}
|
||||
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||
}
|
||||
|
||||
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
|
||||
instanceId := backup.GetJobBase().DbInstanceId
|
||||
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool) error {
|
||||
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG")
|
||||
}
|
||||
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG 行模式")
|
||||
}
|
||||
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||
if err != nil {
|
||||
@@ -413,12 +414,7 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ type DbProgram interface {
|
||||
|
||||
RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||
|
||||
RemoveBackupHistory(ctx context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||
|
||||
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,12 @@ func (svc *DbProgramMysql) Backup(ctx context.Context, backupHistory *entity.DbB
|
||||
return binlogInfo, nil
|
||||
}
|
||||
|
||||
func (svc *DbProgramMysql) RemoveBackupHistory(_ context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
||||
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
||||
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
||||
return os.Remove(fileName)
|
||||
}
|
||||
|
||||
func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
||||
dbInfo := svc.dbInfo()
|
||||
args := []string{
|
||||
|
||||
@@ -9,20 +9,30 @@ var _ DbJob = (*DbBackup)(nil)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
*DbJobBaseImpl
|
||||
DbJobBaseImpl
|
||||
|
||||
Enabled bool // 是否启用
|
||||
StartTime time.Time // 开始时间
|
||||
Interval time.Duration // 间隔时间
|
||||
Repeated bool // 是否重复执行
|
||||
DbName string // 数据库名称
|
||||
Name string // 数据库备份名称
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
DbName string // 数据库名称
|
||||
Name string // 数据库备份名称
|
||||
Enabled bool // 是否启用
|
||||
EnabledDesc string // 启用状态描述
|
||||
StartTime time.Time // 开始时间
|
||||
Interval time.Duration // 间隔时间
|
||||
Repeated bool // 是否重复执行
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetInstanceId() uint64 {
|
||||
return b.DbInstanceId
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetDbName() string {
|
||||
return b.DbName
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetJobType() DbJobType {
|
||||
return DbJobTypeBackup
|
||||
}
|
||||
|
||||
func (b *DbBackup) Schedule() (time.Time, error) {
|
||||
if b.IsFinished() {
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
@@ -37,7 +47,7 @@ func (b *DbBackup) Schedule() (time.Time, error) {
|
||||
lastTime = b.StartTime.Add(-b.Interval)
|
||||
}
|
||||
return lastTime.Add(b.Interval - lastTime.Sub(b.StartTime)%b.Interval), nil
|
||||
case DbJobFailed:
|
||||
case DbJobRunning, DbJobFailed:
|
||||
return time.Now().Add(time.Minute), nil
|
||||
default:
|
||||
return b.StartTime, nil
|
||||
@@ -52,8 +62,13 @@ func (b *DbBackup) IsEnabled() bool {
|
||||
return b.Enabled
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetEnabled(enabled bool) {
|
||||
func (b *DbBackup) IsExpired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetEnabled(enabled bool, desc string) {
|
||||
b.Enabled = enabled
|
||||
b.EnabledDesc = desc
|
||||
}
|
||||
|
||||
func (b *DbBackup) Update(job runner.Job) {
|
||||
@@ -65,3 +80,15 @@ func (b *DbBackup) Update(job runner.Job) {
|
||||
func (b *DbBackup) GetInterval() time.Duration {
|
||||
return b.Interval
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetLastStatus(status DbJobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetKey() DbJobKey {
|
||||
return b.getKey(b.GetJobType())
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetStatus(status runner.JobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ var _ DbJob = (*DbBinlog)(nil)
|
||||
// DbBinlog 数据库备份任务
|
||||
type DbBinlog struct {
|
||||
DbJobBaseImpl
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
}
|
||||
|
||||
func NewDbBinlog(instanceId uint64) *DbBinlog {
|
||||
@@ -35,13 +36,17 @@ func NewDbBinlog(instanceId uint64) *DbBinlog {
|
||||
return job
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetInstanceId() uint64 {
|
||||
return b.DbInstanceId
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetDbName() string {
|
||||
// binlog 是全库级别的
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *DbBinlog) Schedule() (time.Time, error) {
|
||||
switch b.GetJobBase().LastStatus {
|
||||
switch b.LastStatus {
|
||||
case DbJobSuccess:
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
case DbJobFailed:
|
||||
@@ -57,8 +62,28 @@ func (b *DbBinlog) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetEnabled(_ bool) {}
|
||||
func (b *DbBinlog) IsExpired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetEnabled(_ bool, _ string) {}
|
||||
|
||||
func (b *DbBinlog) GetInterval() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetJobType() DbJobType {
|
||||
return DbJobTypeBinlog
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetLastStatus(status DbJobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetKey() DbJobKey {
|
||||
return b.getKey(b.GetJobType())
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetStatus(status DbJobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ const LastResultSize = 256
|
||||
|
||||
type DbJobKey = runner.JobKey
|
||||
|
||||
type DbJobStatus int
|
||||
type DbJobStatus = runner.JobStatus
|
||||
|
||||
const (
|
||||
DbJobRunning DbJobStatus = iota
|
||||
DbJobSuccess
|
||||
DbJobFailed
|
||||
DbJobRunning = runner.JobRunning
|
||||
DbJobSuccess = runner.JobSuccess
|
||||
DbJobFailed = runner.JobFailed
|
||||
)
|
||||
|
||||
type DbJobType string
|
||||
@@ -28,12 +28,14 @@ func (typ DbJobType) String() string {
|
||||
}
|
||||
|
||||
const (
|
||||
DbJobUnknown DbJobType = "db-unknown"
|
||||
DbJobTypeBackup DbJobType = "db-backup"
|
||||
DbJobTypeRestore DbJobType = "db-restore"
|
||||
DbJobTypeBinlog DbJobType = "db-binlog"
|
||||
)
|
||||
|
||||
const (
|
||||
DbJobNameUnknown = "未知任务"
|
||||
DbJobNameBackup = "数据库备份"
|
||||
DbJobNameRestore = "数据库恢复"
|
||||
DbJobNameBinlog = "BINLOG同步"
|
||||
@@ -43,41 +45,24 @@ var _ runner.Job = (DbJob)(nil)
|
||||
|
||||
type DbJobBase interface {
|
||||
model.ModelI
|
||||
|
||||
GetKey() string
|
||||
GetJobType() DbJobType
|
||||
SetJobType(typ DbJobType)
|
||||
GetJobBase() *DbJobBaseImpl
|
||||
SetLastStatus(status DbJobStatus, err error)
|
||||
GetLastStatus() DbJobStatus
|
||||
}
|
||||
|
||||
type DbJob interface {
|
||||
runner.Job
|
||||
DbJobBase
|
||||
|
||||
GetInstanceId() uint64
|
||||
GetKey() string
|
||||
GetJobType() DbJobType
|
||||
GetDbName() string
|
||||
Schedule() (time.Time, error)
|
||||
IsEnabled() bool
|
||||
SetEnabled(enabled bool)
|
||||
IsExpired() bool
|
||||
SetEnabled(enabled bool, desc string)
|
||||
Update(job runner.Job)
|
||||
GetInterval() time.Duration
|
||||
}
|
||||
|
||||
func NewDbJob(typ DbJobType) DbJob {
|
||||
switch typ {
|
||||
case DbJobTypeBackup:
|
||||
return &DbBackup{
|
||||
DbJobBaseImpl: &DbJobBaseImpl{
|
||||
jobType: DbJobTypeBackup},
|
||||
}
|
||||
case DbJobTypeRestore:
|
||||
return &DbRestore{
|
||||
DbJobBaseImpl: &DbJobBaseImpl{
|
||||
jobType: DbJobTypeRestore},
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid DbJobType: %v", typ))
|
||||
}
|
||||
SetLastStatus(status DbJobStatus, err error)
|
||||
}
|
||||
|
||||
var _ DbJobBase = (*DbJobBaseImpl)(nil)
|
||||
@@ -85,30 +70,25 @@ var _ DbJobBase = (*DbJobBaseImpl)(nil)
|
||||
type DbJobBaseImpl struct {
|
||||
model.Model
|
||||
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
LastStatus DbJobStatus // 最近一次执行状态
|
||||
LastResult string // 最近一次执行结果
|
||||
LastTime timex.NullTime // 最近一次执行时间
|
||||
jobType DbJobType
|
||||
jobKey runner.JobKey
|
||||
LastStatus DbJobStatus // 最近一次执行状态
|
||||
LastResult string // 最近一次执行结果
|
||||
LastTime timex.NullTime // 最近一次执行时间
|
||||
jobKey runner.JobKey
|
||||
}
|
||||
|
||||
func NewDbBJobBase(instanceId uint64, jobType DbJobType) *DbJobBaseImpl {
|
||||
return &DbJobBaseImpl{
|
||||
DbInstanceId: instanceId,
|
||||
jobType: jobType,
|
||||
func (d *DbJobBaseImpl) getJobType() DbJobType {
|
||||
job, ok := any(d).(DbJob)
|
||||
if !ok {
|
||||
return DbJobUnknown
|
||||
}
|
||||
return job.GetJobType()
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) GetJobType() DbJobType {
|
||||
return d.jobType
|
||||
func (d *DbJobBaseImpl) GetLastStatus() DbJobStatus {
|
||||
return d.LastStatus
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) SetJobType(typ DbJobType) {
|
||||
d.jobType = typ
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
|
||||
func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
|
||||
var statusName, jobName string
|
||||
switch status {
|
||||
case DbJobRunning:
|
||||
@@ -120,7 +100,8 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
|
||||
default:
|
||||
return
|
||||
}
|
||||
switch d.jobType {
|
||||
|
||||
switch jobType {
|
||||
case DbJobTypeBackup:
|
||||
jobName = DbJobNameBackup
|
||||
case DbJobTypeRestore:
|
||||
@@ -128,7 +109,7 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
|
||||
case DbJobTypeBinlog:
|
||||
jobName = DbJobNameBinlog
|
||||
default:
|
||||
jobName = d.jobType.String()
|
||||
jobName = jobType.String()
|
||||
}
|
||||
d.LastStatus = status
|
||||
var result = jobName + statusName
|
||||
@@ -139,17 +120,13 @@ func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
|
||||
d.LastTime = timex.NewNullTime(time.Now())
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) GetJobBase() *DbJobBaseImpl {
|
||||
return d
|
||||
}
|
||||
|
||||
func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey {
|
||||
return fmt.Sprintf("%v-%d", typ, jobId)
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) GetKey() DbJobKey {
|
||||
func (d *DbJobBaseImpl) getKey(jobType DbJobType) DbJobKey {
|
||||
if len(d.jobKey) == 0 {
|
||||
d.jobKey = FormatJobKey(d.jobType, d.Id)
|
||||
d.jobKey = FormatJobKey(jobType, d.Id)
|
||||
}
|
||||
return d.jobKey
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ var _ DbJob = (*DbRestore)(nil)
|
||||
|
||||
// DbRestore 数据库恢复任务
|
||||
type DbRestore struct {
|
||||
*DbJobBaseImpl
|
||||
DbJobBaseImpl
|
||||
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
DbName string // 数据库名称
|
||||
Enabled bool // 是否启用
|
||||
EnabledDesc string // 启用状态描述
|
||||
StartTime time.Time // 开始时间
|
||||
Interval time.Duration // 间隔时间
|
||||
Repeated bool // 是否重复执行
|
||||
@@ -23,6 +25,10 @@ type DbRestore struct {
|
||||
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetInstanceId() uint64 {
|
||||
return r.DbInstanceId
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetDbName() string {
|
||||
return r.DbName
|
||||
}
|
||||
@@ -36,7 +42,7 @@ func (r *DbRestore) Schedule() (time.Time, error) {
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
default:
|
||||
if time.Now().Sub(r.StartTime) > time.Hour {
|
||||
return time.Time{}, runner.ErrJobTimeout
|
||||
return time.Time{}, runner.ErrJobExpired
|
||||
}
|
||||
return r.StartTime, nil
|
||||
}
|
||||
@@ -46,8 +52,13 @@ func (r *DbRestore) IsEnabled() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
func (r *DbRestore) SetEnabled(enabled bool) {
|
||||
func (r *DbRestore) SetEnabled(enabled bool, desc string) {
|
||||
r.Enabled = enabled
|
||||
r.EnabledDesc = desc
|
||||
}
|
||||
|
||||
func (r *DbRestore) IsExpired() bool {
|
||||
return !r.Repeated && time.Now().After(r.StartTime.Add(time.Hour))
|
||||
}
|
||||
|
||||
func (r *DbRestore) IsFinished() bool {
|
||||
@@ -63,3 +74,19 @@ func (r *DbRestore) Update(job runner.Job) {
|
||||
func (r *DbRestore) GetInterval() time.Duration {
|
||||
return r.Interval
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetJobType() DbJobType {
|
||||
return DbJobTypeRestore
|
||||
}
|
||||
|
||||
func (r *DbRestore) SetLastStatus(status DbJobStatus, err error) {
|
||||
r.setLastStatus(r.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetKey() DbJobKey {
|
||||
return r.getKey(r.GetJobType())
|
||||
}
|
||||
|
||||
func (r *DbRestore) SetStatus(status DbJobStatus, err error) {
|
||||
r.setLastStatus(r.GetJobType(), status, err)
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ type DbSqlExecQuery struct {
|
||||
CreatorId uint64
|
||||
}
|
||||
|
||||
// DbJobQuery 数据库备份任务查询
|
||||
type DbJobQuery struct {
|
||||
// DbBackupQuery 数据库备份任务查询
|
||||
type DbBackupQuery struct {
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
DbName string `json:"dbName" form:"dbName"`
|
||||
IntervalDay int `json:"intervalDay" form:"intervalDay"`
|
||||
@@ -61,13 +61,13 @@ type DbBackupHistoryQuery struct {
|
||||
}
|
||||
|
||||
// DbRestoreQuery 数据库备份任务查询
|
||||
//type DbRestoreQuery struct {
|
||||
// Id uint64 `json:"id" form:"id"`
|
||||
// DbName string `json:"dbName" form:"dbName"`
|
||||
// InDbNames []string `json:"-" form:"-"`
|
||||
// DbInstanceId uint64 `json:"-" form:"-"`
|
||||
// Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行
|
||||
//}
|
||||
type DbRestoreQuery struct {
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
DbName string `json:"dbName" form:"dbName"`
|
||||
InDbNames []string `json:"-" form:"-"`
|
||||
DbInstanceId uint64 `json:"-" form:"-"`
|
||||
Repeated bool `json:"repeated" form:"repeated"` // 是否重复执行
|
||||
}
|
||||
|
||||
// DbRestoreHistoryQuery 数据库备份任务查询
|
||||
type DbRestoreHistoryQuery struct {
|
||||
|
||||
@@ -13,5 +13,5 @@ type DbBackup interface {
|
||||
GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
|
||||
|
||||
// GetPageList 分页获取数据库任务列表
|
||||
GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ import (
|
||||
type DbBackupHistory interface {
|
||||
base.Repo[*entity.DbBackupHistory]
|
||||
|
||||
// GetHistories 分页获取数据备份历史
|
||||
GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
// GetPageList 分页获取数据备份历史
|
||||
GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error)
|
||||
|
||||
GetEarliestHistory(instanceId uint64) (*entity.DbBackupHistory, bool, error)
|
||||
|
||||
GetHistories(backupHistoryIds []uint64, toEntity any) error
|
||||
|
||||
UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error)
|
||||
UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ type DbRestore interface {
|
||||
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
|
||||
|
||||
// GetPageList 分页获取数据库任务列表
|
||||
GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
GetEnabledRestores(toEntity any, backupHistoryId ...uint64) error
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (d *dbBackupRepoImpl) ListToDo(jobs any) error {
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务列表
|
||||
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
||||
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
||||
d.GetModel()
|
||||
qd := gormx.NewQuery(d.GetModel()).
|
||||
Eq("id", condition.Id).
|
||||
@@ -83,7 +83,12 @@ func (d *dbBackupRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enable
|
||||
cond := map[string]any{
|
||||
"id": jobId,
|
||||
}
|
||||
desc := "任务已禁用"
|
||||
if enabled {
|
||||
desc = "任务已启用"
|
||||
}
|
||||
return d.Updates(cond, map[string]any{
|
||||
"enabled": enabled,
|
||||
"enabled": enabled,
|
||||
"enabled_desc": desc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ func NewDbBackupHistoryRepo() repository.DbBackupHistory {
|
||||
return &dbBackupHistoryRepoImpl{}
|
||||
}
|
||||
|
||||
func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(new(entity.DbBackupHistory)).
|
||||
func (repo *dbBackupHistoryRepoImpl) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(repo.GetModel()).
|
||||
Eq("id", condition.Id).
|
||||
Eq0("db_instance_id", condition.DbInstanceId).
|
||||
In0("db_name", condition.InDbNames).
|
||||
@@ -31,6 +31,14 @@ func (repo *dbBackupHistoryRepoImpl) GetHistories(condition *entity.DbBackupHist
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (repo *dbBackupHistoryRepoImpl) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||
return global.Db.Model(repo.GetModel()).
|
||||
Where("id in ?", backupHistoryIds).
|
||||
Scopes(gormx.UndeleteScope).
|
||||
Find(toEntity).
|
||||
Error
|
||||
}
|
||||
|
||||
func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error) {
|
||||
history := &entity.DbBackupHistory{}
|
||||
db := global.Db
|
||||
@@ -65,3 +73,33 @@ func (repo *dbBackupHistoryRepoImpl) GetEarliestHistory(instanceId uint64) (*ent
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *dbBackupHistoryRepoImpl) UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error) {
|
||||
db := global.Db.Model(repo.GetModel()).
|
||||
Where("id in ?", backupHistoryId).
|
||||
Where("restoring = false").
|
||||
Scopes(gormx.UndeleteScope).
|
||||
Update("restoring", deleting)
|
||||
if db.Error != nil {
|
||||
return false, db.Error
|
||||
}
|
||||
if db.RowsAffected != int64(len(backupHistoryId)) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (repo *dbBackupHistoryRepoImpl) UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error) {
|
||||
db := global.Db.Model(repo.GetModel()).
|
||||
Where("id in ?", backupHistoryId).
|
||||
Where("deleting = false").
|
||||
Scopes(gormx.UndeleteScope).
|
||||
Update("restoring", restoring)
|
||||
if db.Error != nil {
|
||||
return false, db.Error
|
||||
}
|
||||
if db.RowsAffected != int64(len(backupHistoryId)) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (d *dbJobBaseImpl[T]) UpdateLastStatus(ctx context.Context, job entity.DbJo
|
||||
}
|
||||
|
||||
func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any) error {
|
||||
// refactor and jobs from any to []T
|
||||
// refactor jobs from any to []T
|
||||
return gormx.Tx(func(db *gorm.DB) error {
|
||||
var instanceId uint64
|
||||
var dbNames []string
|
||||
@@ -44,11 +44,10 @@ func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any
|
||||
dbNames = make([]string, 0, reflectLen)
|
||||
for i := 0; i < reflectLen; i++ {
|
||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||
jobBase := job.GetJobBase()
|
||||
if instanceId == 0 {
|
||||
instanceId = jobBase.DbInstanceId
|
||||
instanceId = job.GetInstanceId()
|
||||
}
|
||||
if jobBase.DbInstanceId != instanceId {
|
||||
if job.GetInstanceId() != instanceId {
|
||||
return errors.New("不支持同时为多个数据库实例添加数据库任务")
|
||||
}
|
||||
if job.GetInterval() == 0 {
|
||||
@@ -59,8 +58,7 @@ func addJob[T entity.DbJob](ctx context.Context, repo dbJobBaseImpl[T], jobs any
|
||||
}
|
||||
default:
|
||||
job := jobs.(entity.DbJob)
|
||||
jobBase := job.GetJobBase()
|
||||
instanceId = jobBase.DbInstanceId
|
||||
instanceId = job.GetInstanceId()
|
||||
if job.GetInterval() > 0 {
|
||||
dbNames = append(dbNames, job.GetDbName())
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ func (d *dbRestoreRepoImpl) ListToDo(jobs any) error {
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务列表
|
||||
func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
||||
d.GetModel()
|
||||
func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(d.GetModel()).
|
||||
Eq("id", condition.Id).
|
||||
Eq0("db_instance_id", condition.DbInstanceId).
|
||||
@@ -65,6 +64,17 @@ func (d *dbRestoreRepoImpl) GetPageList(condition *entity.DbJobQuery, pageParam
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (d *dbRestoreRepoImpl) GetEnabledRestores(toEntity any, backupHistoryId ...uint64) error {
|
||||
return global.Db.Model(d.GetModel()).
|
||||
Select("id", "db_backup_history_id", "last_status", "last_result", "last_time").
|
||||
Where("db_backup_history_id in ?", backupHistoryId).
|
||||
Where("enabled = true").
|
||||
Scopes(gormx.UndeleteScope).
|
||||
Order("id DESC").
|
||||
Find(toEntity).
|
||||
Error
|
||||
}
|
||||
|
||||
// AddJob 添加数据库任务
|
||||
func (d *dbRestoreRepoImpl) AddJob(ctx context.Context, jobs any) error {
|
||||
return addJob[*entity.DbRestore](ctx, d.dbJobBaseImpl, jobs)
|
||||
@@ -74,7 +84,12 @@ func (d *dbRestoreRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enabl
|
||||
cond := map[string]any{
|
||||
"id": jobId,
|
||||
}
|
||||
desc := "任务已禁用"
|
||||
if enabled {
|
||||
desc = "任务已启用"
|
||||
}
|
||||
return d.Updates(cond, map[string]any{
|
||||
"enabled": enabled,
|
||||
"enabled": enabled,
|
||||
"enabled_desc": desc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ func InitDbBackupRouter(router *gin.RouterGroup) {
|
||||
|
||||
// 获取数据库备份历史
|
||||
req.NewGet(":dbId/backup-histories/", d.GetHistoryPageList),
|
||||
// 从数据库备份历史中恢复数据库
|
||||
req.NewPost(":dbId/backup-histories/:backupHistoryId/restore", d.RestoreHistories),
|
||||
// 删除数据库备份历史
|
||||
req.NewDelete(":dbId/backup-histories/:backupHistoryId", d.DeleteHistories),
|
||||
}
|
||||
|
||||
req.BatchSetGroup(dbs, reqs)
|
||||
|
||||
@@ -50,10 +50,6 @@ func (d *wrapper[T]) GetKey() string {
|
||||
return d.key
|
||||
}
|
||||
|
||||
func (d *wrapper[T]) Payload() T {
|
||||
return d.job
|
||||
}
|
||||
|
||||
func NewDelayQueue[T Delayable](cap int) *DelayQueue[T] {
|
||||
singleDequeue := make(chan struct{}, 1)
|
||||
singleDequeue <- struct{}{}
|
||||
|
||||
@@ -11,18 +11,20 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrJobNotFound = errors.New("job not found")
|
||||
ErrJobExist = errors.New("job already exists")
|
||||
ErrJobFinished = errors.New("job already finished")
|
||||
ErrJobDisabled = errors.New("job has been disabled")
|
||||
ErrJobTimeout = errors.New("job has timed out")
|
||||
ErrJobNotFound = errors.New("任务未找到")
|
||||
ErrJobExist = errors.New("任务已存在")
|
||||
ErrJobFinished = errors.New("任务已完成")
|
||||
ErrJobDisabled = errors.New("任务已禁用")
|
||||
ErrJobExpired = errors.New("任务已过期")
|
||||
ErrJobRunning = errors.New("任务执行中")
|
||||
)
|
||||
|
||||
type JobKey = string
|
||||
type RunJobFunc[T Job] func(ctx context.Context, job T)
|
||||
type RunJobFunc[T Job] func(ctx context.Context, job T) error
|
||||
type NextJobFunc[T Job] func() (T, bool)
|
||||
type RunnableJobFunc[T Job] func(job T, next NextJobFunc[T]) bool
|
||||
type RunnableJobFunc[T Job] func(job T, next NextJobFunc[T]) (bool, error)
|
||||
type ScheduleJobFunc[T Job] func(job T) (deadline time.Time, err error)
|
||||
type UpdateJobFunc[T Job] func(ctx context.Context, job T) error
|
||||
|
||||
type JobStatus int
|
||||
|
||||
@@ -31,11 +33,15 @@ const (
|
||||
JobDelaying
|
||||
JobWaiting
|
||||
JobRunning
|
||||
JobSuccess
|
||||
JobFailed
|
||||
)
|
||||
|
||||
type Job interface {
|
||||
GetKey() JobKey
|
||||
Update(job Job)
|
||||
SetStatus(status JobStatus, err error)
|
||||
SetEnabled(enabled bool, desc string)
|
||||
}
|
||||
|
||||
type iterator[T Job] struct {
|
||||
@@ -114,6 +120,7 @@ type Runner[T Job] struct {
|
||||
runJob RunJobFunc[T]
|
||||
runnableJob RunnableJobFunc[T]
|
||||
scheduleJob ScheduleJobFunc[T]
|
||||
updateJob UpdateJobFunc[T]
|
||||
mutex sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
context context.Context
|
||||
@@ -138,6 +145,12 @@ func WithScheduleJob[T Job](scheduleJob ScheduleJobFunc[T]) Opt[T] {
|
||||
}
|
||||
}
|
||||
|
||||
func WithUpdateJob[T Job](updateJob UpdateJobFunc[T]) Opt[T] {
|
||||
return func(runner *Runner[T]) {
|
||||
runner.updateJob = updateJob
|
||||
}
|
||||
}
|
||||
|
||||
func NewRunner[T Job](maxRunning int, runJob RunJobFunc[T], opts ...Opt[T]) *Runner[T] {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
runner := &Runner[T]{
|
||||
@@ -151,14 +164,15 @@ func NewRunner[T Job](maxRunning int, runJob RunJobFunc[T], opts ...Opt[T]) *Run
|
||||
delayQueue: NewDelayQueue[*wrapper[T]](0),
|
||||
}
|
||||
runner.runJob = runJob
|
||||
runner.runnableJob = func(job T, _ NextJobFunc[T]) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
runner.updateJob = func(ctx context.Context, job T) error {
|
||||
return nil
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(runner)
|
||||
}
|
||||
if runner.runnableJob == nil {
|
||||
runner.runnableJob = func(job T, _ NextJobFunc[T]) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
runner.wg.Add(maxRunning + 1)
|
||||
for i := 0; i < maxRunning; i++ {
|
||||
@@ -211,10 +225,32 @@ func (r *Runner[T]) doRun(wrap *wrapper[T]) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logx.Error(fmt.Sprintf("failed to run job: %v", err))
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
r.runJob(r.context, wrap.job)
|
||||
wrap.job.SetStatus(JobRunning, nil)
|
||||
if err := r.updateJob(r.context, wrap.job); err != nil {
|
||||
err = fmt.Errorf("任务状态保存失败: %w", err)
|
||||
wrap.job.SetStatus(JobFailed, err)
|
||||
_ = r.updateJob(r.context, wrap.job)
|
||||
return
|
||||
}
|
||||
runErr := r.runJob(r.context, wrap.job)
|
||||
if runErr != nil {
|
||||
wrap.job.SetStatus(JobFailed, runErr)
|
||||
} else {
|
||||
wrap.job.SetStatus(JobSuccess, nil)
|
||||
}
|
||||
if err := r.updateJob(r.context, wrap.job); err != nil {
|
||||
if runErr != nil {
|
||||
err = fmt.Errorf("任务状态保存失败: %w, %w", err, runErr)
|
||||
} else {
|
||||
err = fmt.Errorf("任务状态保存失败: %w", err)
|
||||
}
|
||||
wrap.job.SetStatus(JobFailed, err)
|
||||
_ = r.updateJob(r.context, wrap.job)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner[T]) afterRun(wrap *wrapper[T]) {
|
||||
@@ -249,12 +285,19 @@ func (r *Runner[T]) pickRunnableJob() (*wrapper[T], bool) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
var disabled []JobKey
|
||||
iter := r.running.Iterator()
|
||||
var runnable *wrapper[T]
|
||||
ok := r.waiting.Any(func(key interface{}, value interface{}) bool {
|
||||
wrap := value.(*wrapper[T])
|
||||
iter.Begin()
|
||||
if r.runnableJob(wrap.job, iter.Next) {
|
||||
able, err := r.runnableJob(wrap.job, iter.Next)
|
||||
if err != nil {
|
||||
wrap.job.SetEnabled(false, err.Error())
|
||||
r.updateJob(r.context, wrap.job)
|
||||
disabled = append(disabled, key.(JobKey))
|
||||
}
|
||||
if able {
|
||||
if r.running.Full() {
|
||||
return false
|
||||
}
|
||||
@@ -269,6 +312,10 @@ func (r *Runner[T]) pickRunnableJob() (*wrapper[T], bool) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
for _, key := range disabled {
|
||||
r.waiting.Remove(key)
|
||||
delete(r.all, key)
|
||||
}
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
@@ -304,24 +351,23 @@ func (r *Runner[T]) Add(ctx context.Context, job T) error {
|
||||
return r.schedule(ctx, deadline, job)
|
||||
}
|
||||
|
||||
func (r *Runner[T]) UpdateOrAdd(ctx context.Context, job T) error {
|
||||
func (r *Runner[T]) Update(ctx context.Context, job T) error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
wrap, ok := r.all[job.GetKey()]
|
||||
if ok {
|
||||
wrap.job.Update(job)
|
||||
switch wrap.status {
|
||||
case JobDelaying:
|
||||
r.delayQueue.Remove(ctx, wrap.key)
|
||||
delete(r.all, wrap.key)
|
||||
case JobWaiting:
|
||||
r.waiting.Remove(wrap.key)
|
||||
delete(r.all, wrap.key)
|
||||
case JobRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
if !ok {
|
||||
return ErrJobNotFound
|
||||
}
|
||||
wrap.job.Update(job)
|
||||
switch wrap.status {
|
||||
case JobDelaying:
|
||||
r.delayQueue.Remove(ctx, wrap.key)
|
||||
case JobWaiting:
|
||||
r.waiting.Remove(wrap.key)
|
||||
case JobRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
deadline, err := r.doScheduleJob(job, false)
|
||||
if err != nil {
|
||||
@@ -360,7 +406,7 @@ func (r *Runner[T]) Remove(ctx context.Context, key JobKey) error {
|
||||
|
||||
wrap, ok := r.all[key]
|
||||
if !ok {
|
||||
return ErrJobNotFound
|
||||
return nil
|
||||
}
|
||||
switch wrap.status {
|
||||
case JobDelaying:
|
||||
@@ -372,6 +418,7 @@ func (r *Runner[T]) Remove(ctx context.Context, key JobKey) error {
|
||||
case JobRunning:
|
||||
// 统一标记为 removed, 待任务执行完成后再删除
|
||||
wrap.removed = true
|
||||
return ErrJobRunning
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -29,14 +29,19 @@ func (t *testJob) GetKey() JobKey {
|
||||
return t.Key
|
||||
}
|
||||
|
||||
func (t *testJob) SetStatus(status JobStatus, err error) {}
|
||||
|
||||
func (t *testJob) SetEnabled(enabled bool, desc string) {}
|
||||
|
||||
func TestRunner_Close(t *testing.T) {
|
||||
signal := make(chan struct{}, 1)
|
||||
waiting := sync.WaitGroup{}
|
||||
waiting.Add(1)
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
|
||||
waiting.Done()
|
||||
timex.SleepWithContext(ctx, time.Hour)
|
||||
signal <- struct{}{}
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
job := &testJob{
|
||||
@@ -78,8 +83,9 @@ func TestRunner_AddJob(t *testing.T) {
|
||||
want: ErrJobExist,
|
||||
},
|
||||
}
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
|
||||
timex.SleepWithContext(ctx, time.Hour)
|
||||
return nil
|
||||
})
|
||||
defer runner.Close()
|
||||
for _, tc := range testCases {
|
||||
@@ -99,10 +105,11 @@ func TestJob_UpdateStatus(t *testing.T) {
|
||||
running
|
||||
finished
|
||||
)
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) {
|
||||
runner := NewRunner[*testJob](1, func(ctx context.Context, job *testJob) error {
|
||||
job.status = running
|
||||
timex.SleepWithContext(ctx, d*2)
|
||||
job.status = finished
|
||||
return nil
|
||||
})
|
||||
first := newTestJob("first")
|
||||
second := newTestJob("second")
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `ui_path`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `is_deleted`, `delete_time`)
|
||||
VALUES (161, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL),
|
||||
(160, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||
|
||||
ALTER TABLE `mayfly-go`.`t_db_backup`
|
||||
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
||||
|
||||
ALTER TABLE `mayfly-go`.`t_db_restore`
|
||||
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
||||
|
||||
ALTER TABLE `mayfly-go`.`t_db_backup_history`
|
||||
ADD COLUMN `restoring` int(1) NOT NULL DEFAULT(0) COMMENT '备份历史恢复标识',
|
||||
ADD COLUMN `deleting` int(1) NOT NULL DEFAULT(0) COMMENT '备份历史删除标识';
|
||||
|
||||
Reference in New Issue
Block a user