diff --git a/apps/daas/src/i18n/langs/en.js b/apps/daas/src/i18n/langs/en.js index ed88f9476..a7b6c8d34 100644 --- a/apps/daas/src/i18n/langs/en.js +++ b/apps/daas/src/i18n/langs/en.js @@ -2248,6 +2248,7 @@ export default { daas_role_role_gongnengquanxian: 'Function Permission', daas_role_role_yemianquanxian: 'Page Permission', daas_role_role_gongnengmokuai: 'Function Module', + daas_role_role_no_matching_module: 'No matching modules', daas_role_role_gouxuanxiangyingmo: 'Check the corresponding module to indicate that the navigation is visible to users under the current role, and enable [View All Data] to indicate that the role can view and operate all the data under this module, and if it is not checked, it can only view and operate the data created and authorized by itself data. ', daas_feature_unavailable: 'Restricted Feature', @@ -2277,6 +2278,76 @@ export default { daas_cluster_engine_hostname: 'Hostname/IP', daas_cluster_connection_count: 'TCP Connection Count', daas_cluser_keyword_placeholder: 'Search for hostnames', + daas_task_rebalance_button: 'Task Rebalance', + daas_task_rebalance_title: 'Task Rebalance Plan', + daas_task_rebalance_subtitle: + 'Drag task cards onto a target engine to adjust the task distribution across engines', + daas_task_rebalance_online: 'Online', + daas_task_rebalance_offline: 'Offline', + daas_task_rebalance_memory: 'Memory Usage', + daas_task_rebalance_task_count: 'Tasks', + daas_task_rebalance_legend_move: 'Tasks to be moved', + daas_task_rebalance_legend_locked: 'Tasks that cannot be rebalanced', + daas_task_rebalance_cancel: 'Cancel', + daas_task_rebalance_confirm: 'Confirm Rebalance', + daas_task_rebalance_create: 'Create Rebalance', + daas_task_rebalance_empty: 'No tasks available for rebalancing', + daas_task_rebalance_create_success: 'Rebalance plan created, migration started', + daas_task_rebalance_type_initial_sync: 'Full', + daas_task_rebalance_type_cdc: 'Incremental', + daas_task_rebalance_type_full_cdc: 'Full + Incremental', + daas_task_rebalance_reason_only_one_agent: + 'At least two available engines are required', + daas_task_rebalance_reason_agent_offline: 'The current engine is offline', + daas_task_rebalance_reason_status_error: + 'The task is not running and cannot be migrated', + daas_task_rebalance_reason_manual_agent: + 'A fixed engine is specified and cannot be migrated', + daas_task_rebalance_reason_incremental: + 'Initial sync in progress, cannot be migrated', + daas_task_rebalance_history_title: 'Task Rebalance Records', + daas_task_rebalance_history_list_title: 'History', + daas_task_rebalance_history_empty: 'No rebalance records', + daas_task_rebalance_history_empty_desc: + 'Records will appear here after a rebalance', + daas_task_rebalance_history_detail_empty: 'Select a record on the left', + daas_task_rebalance_history_task_total: '{count} tasks', + daas_task_rebalance_history_record_creating: 'Creating', + daas_task_rebalance_history_record_running: 'Running', + daas_task_rebalance_history_record_done: 'Completed', + daas_task_rebalance_history_record_partial: 'Partially Completed', + daas_task_rebalance_history_record_cancelled: 'Cancelled', + daas_task_rebalance_history_record_failed: 'Failed', + daas_task_rebalance_history_job_pending: 'Pending', + daas_task_rebalance_history_job_starting: 'Starting', + daas_task_rebalance_history_job_stopping: 'Stopping', + daas_task_rebalance_history_job_ok: 'Completed', + daas_task_rebalance_history_job_cancelled: 'Cancelled', + daas_task_rebalance_history_job_invalid_agent: 'Invalid Agent', + daas_task_rebalance_history_job_status_error: 'Status Error', + daas_task_rebalance_history_job_stop_timeout: 'Stop Timeout', + daas_task_rebalance_history_job_start_timeout: 'Start Timeout', + daas_task_rebalance_history_job_failed: 'Failed', + daas_task_rebalance_history_stat_success: 'Success', + daas_task_rebalance_history_stat_failed: 'Failed', + daas_task_rebalance_history_stat_skipped: 'Skipped', + daas_task_rebalance_history_stat_running: 'Running', + daas_task_rebalance_history_stat_pending: 'Pending', + daas_task_rebalance_history_col_task: 'Task Name', + daas_task_rebalance_history_col_source: 'Source Engine', + daas_task_rebalance_history_col_target: 'Target Engine', + daas_task_rebalance_history_col_status: 'Status', + daas_task_rebalance_history_col_error: 'Error', + daas_task_rebalance_history_col_action: 'Action', + daas_task_rebalance_history_cancel: 'Cancel', + daas_task_rebalance_history_cancel_remaining: 'Cancel Remaining', + daas_task_rebalance_history_cancel_remaining_confirm: + 'Cancel the remaining {count} pending tasks? This cannot be undone.', + daas_task_rebalance_history_cancel_success: 'Task cancelled', + daas_task_rebalance_history_cancel_remaining_success: + '{count} tasks cancelled', + daas_task_rebalance_history_progress: 'Execution Progress', + page_title_task_rebalance: 'Task Rebalance', webhook_alerts: 'Webhook Alerts', webhook_alerts_detail: 'Webhook Alert Details', diff --git a/apps/daas/src/i18n/langs/zh-CN.js b/apps/daas/src/i18n/langs/zh-CN.js index fd3da4fff..b981e9681 100644 --- a/apps/daas/src/i18n/langs/zh-CN.js +++ b/apps/daas/src/i18n/langs/zh-CN.js @@ -2131,6 +2131,7 @@ export default { daas_role_role_gongnengmokuai: '功能模块', daas_role_role_gouxuanxiangyingmo: '勾选相应模块表示此导航对当前角色下用户可见,开启【查看全部数据】则表示角色可以查看和操作该模块下所有的数据,不勾选则只能查看和操作自己创建和被授权的数据。', + daas_role_role_no_matching_module: '没有匹配的模块', daas_feature_unavailable: '升级版本,解锁更多功能!', daas_feature_unavailable_subtitle: @@ -2157,6 +2158,69 @@ export default { daas_cluster_engine_hostname: '主机名/IP', daas_cluster_connection_count: 'TCP 连接数', daas_cluser_keyword_placeholder: '搜索主机名', + daas_task_rebalance_button: '任务调度均衡', + daas_task_rebalance_title: '任务均衡方案', + daas_task_rebalance_subtitle: '拖动任务卡片到目标引擎,调整各引擎的任务分布', + daas_task_rebalance_online: '在线', + daas_task_rebalance_offline: '离线', + daas_task_rebalance_memory: '内存使用率', + daas_task_rebalance_task_count: '任务数', + daas_task_rebalance_legend_move: '将被调走的任务', + daas_task_rebalance_legend_locked: '不可均衡的任务', + daas_task_rebalance_cancel: '取消', + daas_task_rebalance_confirm: '确认均衡', + daas_task_rebalance_create: '创建均衡', + daas_task_rebalance_empty: '暂无可均衡的任务', + daas_task_rebalance_create_success: '均衡方案已创建,开始执行任务迁移', + daas_task_rebalance_type_initial_sync: '全量', + daas_task_rebalance_type_cdc: '增量', + daas_task_rebalance_type_full_cdc: '全量+增量', + daas_task_rebalance_reason_only_one_agent: '可用引擎不足 2 个,无法均衡', + daas_task_rebalance_reason_agent_offline: '当前引擎已离线', + daas_task_rebalance_reason_status_error: '任务非运行中,无法迁移', + daas_task_rebalance_reason_manual_agent: '已指定固定引擎,无法迁移', + daas_task_rebalance_reason_incremental: '全量同步中,无法迁移', + daas_task_rebalance_history_title: '任务均衡记录', + daas_task_rebalance_history_list_title: '历史记录', + daas_task_rebalance_history_empty: '暂无均衡记录', + daas_task_rebalance_history_empty_desc: '任务均衡后记录将显示在这里', + daas_task_rebalance_history_detail_empty: '请在左侧选择一条记录', + daas_task_rebalance_history_task_total: '{count} 个任务', + daas_task_rebalance_history_record_creating: '创建中', + daas_task_rebalance_history_record_running: '执行中', + daas_task_rebalance_history_record_done: '已完成', + daas_task_rebalance_history_record_partial: '部分完成', + daas_task_rebalance_history_record_cancelled: '已取消', + daas_task_rebalance_history_record_failed: '失败', + daas_task_rebalance_history_job_pending: '等待中', + daas_task_rebalance_history_job_starting: '启动中', + daas_task_rebalance_history_job_stopping: '停止中', + daas_task_rebalance_history_job_ok: '已完成', + daas_task_rebalance_history_job_cancelled: '已取消', + daas_task_rebalance_history_job_invalid_agent: '引擎不可用', + daas_task_rebalance_history_job_status_error: '状态异常', + daas_task_rebalance_history_job_stop_timeout: '停止超时', + daas_task_rebalance_history_job_start_timeout: '启动超时', + daas_task_rebalance_history_job_failed: '失败', + daas_task_rebalance_history_stat_success: '成功', + daas_task_rebalance_history_stat_failed: '失败', + daas_task_rebalance_history_stat_skipped: '跳过', + daas_task_rebalance_history_stat_running: '运行中', + daas_task_rebalance_history_stat_pending: '等待中', + daas_task_rebalance_history_col_task: '任务名称', + daas_task_rebalance_history_col_source: '源引擎', + daas_task_rebalance_history_col_target: '目标引擎', + daas_task_rebalance_history_col_status: '状态', + daas_task_rebalance_history_col_error: '错误原因', + daas_task_rebalance_history_col_action: '操作', + daas_task_rebalance_history_cancel: '取消', + daas_task_rebalance_history_cancel_remaining: '取消剩余任务', + daas_task_rebalance_history_cancel_remaining_confirm: + '确定取消剩余 {count} 个等待中的任务吗?此操作不可撤销。', + daas_task_rebalance_history_cancel_success: '任务已取消', + daas_task_rebalance_history_cancel_remaining_success: '已取消 {count} 个任务', + daas_task_rebalance_history_progress: '执行进度', + page_title_task_rebalance: '任务均衡', webhook_alerts: 'Webhook 告警', webhook_alerts_detail: 'Webhook 告警详情', diff --git a/apps/daas/src/i18n/langs/zh-TW.js b/apps/daas/src/i18n/langs/zh-TW.js index a411931fe..071f038a6 100644 --- a/apps/daas/src/i18n/langs/zh-TW.js +++ b/apps/daas/src/i18n/langs/zh-TW.js @@ -2115,6 +2115,7 @@ export default { daas_role_role_gongnengquanxian: '功能權限', daas_role_role_yemianquanxian: '頁面權限', daas_role_role_gongnengmokuai: '功能模塊', + daas_role_role_no_matching_module: '沒有匹配的模塊', daas_role_role_gouxuanxiangyingmo: '勾選相應模塊表示此導航對當前角色下用戶可見,開啟【查看全部數據】則表示角色可以查看和操作該模塊下所有的數據,不勾選則只能查看和操作自己創建和被授權的數據。 ', daas_feature_unavailable: '功能暫不可用', @@ -2142,6 +2143,69 @@ export default { daas_cluster_engine_hostname: '主機名/IP', daas_cluster_connection_count: 'TCP 連接數', daas_cluser_keyword_placeholder: '搜索主機名', + daas_task_rebalance_button: '任務調度均衡', + daas_task_rebalance_title: '任務均衡方案', + daas_task_rebalance_subtitle: '拖動任務卡片到目標引擎,調整各引擎的任務分佈', + daas_task_rebalance_online: '在線', + daas_task_rebalance_offline: '離線', + daas_task_rebalance_memory: '內存使用率', + daas_task_rebalance_task_count: '任務數', + daas_task_rebalance_legend_move: '將被調走的任務', + daas_task_rebalance_legend_locked: '不可均衡的任務', + daas_task_rebalance_cancel: '取消', + daas_task_rebalance_confirm: '確認均衡', + daas_task_rebalance_create: '創建均衡', + daas_task_rebalance_empty: '暫無可均衡的任務', + daas_task_rebalance_create_success: '均衡方案已創建,開始執行任務遷移', + daas_task_rebalance_type_initial_sync: '全量', + daas_task_rebalance_type_cdc: '增量', + daas_task_rebalance_type_full_cdc: '全量+增量', + daas_task_rebalance_reason_only_one_agent: '可用引擎不足 2 個,無法均衡', + daas_task_rebalance_reason_agent_offline: '當前引擎已離線', + daas_task_rebalance_reason_status_error: '任務非運行中,無法遷移', + daas_task_rebalance_reason_manual_agent: '已指定固定引擎,無法遷移', + daas_task_rebalance_reason_incremental: '全量同步中,無法遷移', + daas_task_rebalance_history_title: '任務均衡記錄', + daas_task_rebalance_history_list_title: '歷史記錄', + daas_task_rebalance_history_empty: '暫無均衡記錄', + daas_task_rebalance_history_empty_desc: '任務均衡後記錄將顯示在這裡', + daas_task_rebalance_history_detail_empty: '請在左側選擇一條記錄', + daas_task_rebalance_history_task_total: '{count} 個任務', + daas_task_rebalance_history_record_creating: '創建中', + daas_task_rebalance_history_record_running: '執行中', + daas_task_rebalance_history_record_done: '已完成', + daas_task_rebalance_history_record_partial: '部分完成', + daas_task_rebalance_history_record_cancelled: '已取消', + daas_task_rebalance_history_record_failed: '失敗', + daas_task_rebalance_history_job_pending: '等待中', + daas_task_rebalance_history_job_starting: '啟動中', + daas_task_rebalance_history_job_stopping: '停止中', + daas_task_rebalance_history_job_ok: '已完成', + daas_task_rebalance_history_job_cancelled: '已取消', + daas_task_rebalance_history_job_invalid_agent: '引擎不可用', + daas_task_rebalance_history_job_status_error: '狀態異常', + daas_task_rebalance_history_job_stop_timeout: '停止超時', + daas_task_rebalance_history_job_start_timeout: '啟動超時', + daas_task_rebalance_history_job_failed: '失敗', + daas_task_rebalance_history_stat_success: '成功', + daas_task_rebalance_history_stat_failed: '失敗', + daas_task_rebalance_history_stat_skipped: '跳過', + daas_task_rebalance_history_stat_running: '運行中', + daas_task_rebalance_history_stat_pending: '等待中', + daas_task_rebalance_history_col_task: '任務名稱', + daas_task_rebalance_history_col_source: '源引擎', + daas_task_rebalance_history_col_target: '目標引擎', + daas_task_rebalance_history_col_status: '狀態', + daas_task_rebalance_history_col_error: '錯誤原因', + daas_task_rebalance_history_col_action: '操作', + daas_task_rebalance_history_cancel: '取消', + daas_task_rebalance_history_cancel_remaining: '取消剩餘任務', + daas_task_rebalance_history_cancel_remaining_confirm: + '確定取消剩餘 {count} 個等待中的任務嗎?此操作不可撤銷。', + daas_task_rebalance_history_cancel_success: '任務已取消', + daas_task_rebalance_history_cancel_remaining_success: '已取消 {count} 個任務', + daas_task_rebalance_history_progress: '執行進度', + page_title_task_rebalance: '任務均衡', webhook_alerts: 'Webhook 告警', webhook_alerts_detail: 'Webhook 告警詳情', diff --git a/apps/daas/src/router/menu.ts b/apps/daas/src/router/menu.ts index 137caefb2..fbee1f777 100644 --- a/apps/daas/src/router/menu.ts +++ b/apps/daas/src/router/menu.ts @@ -83,6 +83,11 @@ export const MENU = [ { name: 'dataEncryption' }, { name: 'projectManagement' }, { name: 'dataImportExport' }, + { + name: 'taskRebalanceHistory', + parent: 'taskRebalance', + code: 'v2_task_rebalance', + }, ], }, { diff --git a/apps/daas/src/router/routes.ts b/apps/daas/src/router/routes.ts index 422154889..12494f2db 100644 --- a/apps/daas/src/router/routes.ts +++ b/apps/daas/src/router/routes.ts @@ -482,6 +482,29 @@ export const routes = [ }, ], }, + /* ---------- 任务均衡 ----------*/ + { + path: '/task-rebalance', + name: 'taskRebalance', + component: Layout, + redirect: { + name: 'taskRebalanceHistory', + }, + meta: { + title: 'page_title_task_rebalance', + }, + children: [ + { + path: '', + name: 'taskRebalanceHistory', + component: () => import('@/views/task-rebalance/History.vue'), + meta: { + hideTitle: true, + title: 'page_title_task_rebalance', + }, + }, + ], + }, /* ---------- 函数管理 ----------*/ { path: '/function', diff --git a/apps/daas/src/views/cluster/Cluster.vue b/apps/daas/src/views/cluster/Cluster.vue index 9927ce666..5d7060a5f 100644 --- a/apps/daas/src/views/cluster/Cluster.vue +++ b/apps/daas/src/views/cluster/Cluster.vue @@ -28,6 +28,7 @@ import { } from '@tap/api/src/core/workers' import { useRequest } from '@tap/api/src/request' import PageContainer from '@tap/business/src/components/PageContainer.vue' +import { useHas } from '@tap/business/src/composables' import { dayjs, makeDragNodeImage } from '@tap/business/src/shared' import { FilterBar } from '@tap/component/src/filter-bar' import { IconButton } from '@tap/component/src/icon-button' @@ -35,14 +36,28 @@ import { Modal } from '@tap/component/src/modal' import { useI18n } from '@tap/i18n' import { downloadJson } from '@tap/shared' import Cookie from '@tap/shared/src/cookie' -import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue' +import { + computed, + nextTick, + onMounted, + onUnmounted, + reactive, + ref, + watch, +} from 'vue' import { useRoute, useRouter } from 'vue-router' import SetTag from '@/views/cluster/SetTag.vue' import AddServe from './AddServe.vue' import { STATUS_MAP } from './const' +import TaskRebalanceDrawer from './TaskRebalanceDrawer.vue' import UpdateLicense from './UpdateLicense.vue' const { t } = useI18n() +const $has = useHas() + +const hasRebalancePermission = computed(() => { + return $has('v2_task_rebalance_Edit') +}) // Types interface TagDialog { @@ -169,6 +184,7 @@ const filterItems = ref([ ]) const bindWorkerMap = ref({}) const viewType = ref('cluster') +const showRebalanceDrawer = ref(false) const netStatDialog = reactive({ visible: false, data: [], @@ -264,6 +280,9 @@ const init = async () => { await getDataApi(true) await loadTags() handleFilterAgent() + // Start polling usage rates independently so CPU/Mem stay fresh + // without re-fetching the full cluster list every time. + timer.value = setInterval(refreshUsageRate, 10000) as unknown as null } const submitForm = async () => { @@ -436,6 +455,45 @@ const getUsageRate = (processId: string[]) => { }) } +/** + * Poll only the metric values (CPU / Mem) and patch them onto the existing + * waterfallData items so the rest of the page state is not disturbed. + */ +const refreshUsageRate = async () => { + const data = waterfallData.value as any[] + if (!data.length) return + const processIds = data + .map((it) => it?.systemInfo?.process_id) + .filter(Boolean) + if (!processIds.length) return + + try { + const workerResponse = await getUsageRate(processIds) + const metricValuesData: Record< + string, + { CpuUsage: string; HeapMemoryUsage: string } + > = {} + for (const w of workerResponse?.items || []) { + if (w.process_id && w.metricValues) { + metricValuesData[w.process_id] = { + CpuUsage: `${(Number(w.metricValues.CpuUsage) * 100).toFixed(2)}%`, + HeapMemoryUsage: `${(Number(w.metricValues.HeapMemoryUsage) * 100).toFixed(2)}%`, + } + } + } + for (const item of data) { + const pid = item.systemInfo?.process_id + if (item?.engine?.status === 'running' && pid && metricValuesData[pid]) { + item.metricValues = metricValuesData[pid] + } + } + // Re-assign to trigger Vue reactivity on nested metricValues changes + waterfallData.value = [...data] as never[] + } catch { + // ignore refresh errors silently + } +} + const getAllBindWorker = async () => { try { const data = await queryAllBindWorker() @@ -630,12 +688,44 @@ const getStatus = (type: string) => { return STATUS_MAP[type] || '-' } -const navigateToTaskList = (item: any, syncType: 'migrate' | 'sync') => { +const parseUsage = (val?: string) => { + if (!val || val === '-') return 0 + const num = Number.parseFloat(val) + return Number.isNaN(num) + ? 0 + : Math.min(100, Math.max(0, Number(num.toFixed(2)))) +} + +const rebalanceAgents = computed(() => + (waterfallData.value as any[]).map((item) => ({ + agentId: item.systemInfo?.process_id, + name: item.agentName || item.systemInfo?.hostname, + online: item.status === 'running', + cpuUsage: parseUsage(item.metricValues?.CpuUsage), + memUsage: parseUsage(item.metricValues?.HeapMemoryUsage), + })), +) + +type TaskType = + | 'migrate' + | 'sync' + | 'logCollector' + | 'mem_cache' + | 'connHeartbeat' + +const TASK_TYPE_ROUTE_MAP: Record = { + migrate: 'migrateList', + sync: 'dataflowList', + logCollector: 'HeartbeatTableList', + mem_cache: 'sharedCacheList', + connHeartbeat: 'HeartbeatTableList', +} + +const navigateToTaskList = (item: any, syncType: TaskType) => { const processId = item.systemInfo?.process_id if (!processId) return - const routeName = syncType === 'migrate' ? 'migrateList' : 'dataflowList' router.push({ - name: routeName, + name: TASK_TYPE_ROUTE_MAP[syncType], query: { agentId: processId, status: 'running' }, }) } @@ -1133,11 +1223,20 @@ const onUpdateLicenseSuccess = () => {
@@ -1499,16 +1598,19 @@ const onUpdateLicenseSuccess = () => { :class="item.status !== 'running' ? 'bgred' : 'bggreen'" />
-

+

{{ item.agentName || item.systemInfo.hostname }} + {{ + item.custIP ? item.custIP : item.systemInfo.ip + }}

{{ item.systemInfo.uuid }}
-
- {{ - item.custIP ? item.custIP : item.systemInfo.ip - }} +
{ item.runningTaskNum?.sync || 0 }} + + {{ $t('public_task_type_log_collector') + }}{{ + item.runningTaskNum?.logCollector || 0 + }} + + + {{ $t('page_title_shared_cache') + }}{{ + item.runningTaskNum?.mem_cache || 0 + }} + + + {{ $t('public_task_type_heartbeat') + }}{{ + item.runningTaskNum?.connHeartbeat || 0 + }} +
@@ -2293,6 +2433,11 @@ const onUpdateLicenseSuccess = () => { :service-id="updateLicenseDialog.serviceId" @success="onUpdateLicenseSuccess" /> + + diff --git a/apps/daas/src/views/cluster/TaskRebalanceDrawer.vue b/apps/daas/src/views/cluster/TaskRebalanceDrawer.vue new file mode 100644 index 000000000..25f788671 --- /dev/null +++ b/apps/daas/src/views/cluster/TaskRebalanceDrawer.vue @@ -0,0 +1,591 @@ + + + + + + + diff --git a/apps/daas/src/views/role/Role.vue b/apps/daas/src/views/role/Role.vue index c9648acbb..c7fd1a327 100644 --- a/apps/daas/src/views/role/Role.vue +++ b/apps/daas/src/views/role/Role.vue @@ -249,6 +249,16 @@ const pageSort = [ { name: 'v2_shared_cache' }, { name: 'v2_project_management' }, { name: 'v2_project_import_and_export' }, + { + name: 'v2_task_rebalance', + buttons: [ + { + label: t('daas_task_rebalance_create'), + name: 'v2_task_rebalance_Edit', + checked: false, + }, + ], + }, ], }, { @@ -736,7 +746,13 @@ onMounted(() => {
- +
diff --git a/apps/daas/src/views/task-rebalance/History.vue b/apps/daas/src/views/task-rebalance/History.vue new file mode 100644 index 000000000..1253decca --- /dev/null +++ b/apps/daas/src/views/task-rebalance/History.vue @@ -0,0 +1,805 @@ + + + + + diff --git a/apps/daas/src/views/task-rebalance/openapi.yaml b/apps/daas/src/views/task-rebalance/openapi.yaml new file mode 100644 index 000000000..b964447c8 --- /dev/null +++ b/apps/daas/src/views/task-rebalance/openapi.yaml @@ -0,0 +1,389 @@ +openapi: 3.0.3 +info: + title: Tapdata Task Rebalance API + version: 1.0.0 + description: | + 任务均衡 (Task Rebalance) 模块对外接口。云版会直接拒绝所有调用并返回 `task.rebalance.disabled`。 + + **统一响应包装**:所有接口均返回 `ResponseMessage`,结构为: + ```json + { "reqId": "uuid", "ts": 1700000000000, "code": "ok", "message": null, "data": } + ``` + 业务异常时 `code` 为错误 key(如 `task.rebalance.disabled`),`message` 为已按请求 `Accept-Language` 解析的本地化文案,`data` 为 null。HTTP 状态码默认 200;仅 `NotLogin`/`NotAuthorized` 返回 401。 +servers: + - url: / + description: 当前部署的 TM 实例(同源调用) +security: + - userIdHeader: [] + - accessToken: [] + - basicAuth: [] +tags: + - name: TaskRebalance + description: 任务均衡 (创建 / 查询 / 取消) +paths: + /api/task/rebalance/preview: + post: + tags: [TaskRebalance] + summary: 生成均衡预览 + description: | + 基于当前在线 agent 与运行中任务,模拟一次均衡分配,返回每个任务的可调走状态与评分,但不落库、不执行。 + 前端可按 `data.tasks[].priorityScore` 降序展示,根据 `movable` / `schedulableStatus` 渲染状态。 + 当可用 agent 不足 2 个时返回 `data.reason = "task.rebalance.onlyOneAgent"` 且 `data.tasks` 为空。 + operationId: previewTaskRebalance + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfPreview' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + /api/task/rebalance: + post: + tags: [TaskRebalance] + summary: 创建并启动均衡 + description: | + 以最新预览为依据落库 `TaskRebalance` 记录及一组 `TaskRebalanceJob` 子任务,并异步开始执行。 + 若当前已有 `RUNNING` 均衡,返回 `task.rebalance.alreadyRunning`; + 若没有可调走的任务,返回 `task.rebalance.noTask`; + 若预览本身有阻断原因(如仅一个 agent),返回该 key。 + operationId: createTaskRebalance + responses: + '200': + description: 成功创建 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfRebalance' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + get: + tags: [TaskRebalance] + summary: 历史列表 (分页) + description: | + 通过项目通用 `Filter` JSON 进行分页/排序/过滤。例: + `?filter={"limit":20,"skip":0,"order":"createTime DESC","where":{"status":"OK"}}` + operationId: listTaskRebalance + parameters: + - in: query + name: filter + required: false + schema: + type: string + description: Filter JSON 字符串,URL encode 后传入 + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfPageRebalance' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + /api/task/rebalance/active: + get: + tags: [TaskRebalance] + summary: 是否有正在运行的均衡 + operationId: hasActiveTaskRebalance + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfActive' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + /api/task/rebalance/{id}: + get: + tags: [TaskRebalance] + summary: 均衡详情 (含子任务) + operationId: getTaskRebalanceDetail + parameters: + - $ref: '#/components/parameters/RebalanceId' + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfDetail' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + /api/task/rebalance/{id}/cancel: + post: + tags: [TaskRebalance] + summary: 取消该均衡下所有 PENDING 子任务 + description: 已进入 `STOPPING / STARTING` 的子任务会继续跑完自己的周期,不会被打断。 + operationId: cancelTaskRebalance + parameters: + - $ref: '#/components/parameters/RebalanceId' + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfVoid' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } + /api/task/rebalance/{id}/cancel/{taskId}: + post: + tags: [TaskRebalance] + summary: 取消单个 PENDING 子任务 + operationId: cancelTaskRebalanceJob + parameters: + - $ref: '#/components/parameters/RebalanceId' + - in: path + name: taskId + required: true + schema: { type: string } + description: 子任务对应的业务 task id + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseOfVoid' + '401': { $ref: '#/components/responses/Unauthorized' } + default: { $ref: '#/components/responses/BusinessError' } +components: + securitySchemes: + userIdHeader: + type: apiKey + in: header + name: user_id + description: 网关/反向代理透传的用户 id 头,TM 由此查用户 + accessToken: + type: apiKey + in: query + name: access_token + description: 个人 access token,可作为查询参数 + basicAuth: + type: http + scheme: basic + description: HTTP Basic(仅运维/工具链使用) + parameters: + RebalanceId: + in: path + name: id + required: true + schema: { type: string } + description: TaskRebalance 主键 (ObjectId hex) + responses: + Unauthorized: + description: 未登录 / 登录态失效 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + examples: + notLogin: + value: + reqId: 5a2c-... + ts: 1700000000000 + code: NotLogin + message: Not login + data: null + BusinessError: + description: 业务异常 (HTTP 200,code 为 i18n key) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + examples: + disabled: + summary: 云版禁用 + value: { reqId: "...", ts: 1700000000000, code: task.rebalance.disabled, message: "Task rebalance is disabled.", data: null } + alreadyRunning: + summary: 已有运行中均衡 + value: { reqId: "...", ts: 1700000000000, code: task.rebalance.alreadyRunning, message: "A task rebalance is already running.", data: null } + noTask: + summary: 没有可调走任务 + value: { reqId: "...", ts: 1700000000000, code: task.rebalance.noTask, message: "No task needs to be rebalanced.", data: null } + onlyOneAgent: + summary: 可用 agent 不足 2 个 + value: { reqId: "...", ts: 1700000000000, code: task.rebalance.onlyOneAgent, message: "At least two available agents are required for task rebalance.", data: null } + taskRebalancing: + summary: 调用任务 start/pause 时该任务正被均衡 + value: { reqId: "...", ts: 1700000000000, code: task.rebalance.taskRebalancing, message: "The task is being rebalanced. Please try again later.", data: null } + notAuthorized: + summary: 无 v2_task_rebalance 菜单权限 + value: { reqId: "...", ts: 1700000000000, code: NotAuthorized, message: "No permission", data: null } + schemas: + ApiResponseBase: + type: object + required: [reqId, ts, code] + properties: + reqId: { type: string, description: 请求 trace id, example: "5a2c-...-ff" } + ts: { type: integer, format: int64, description: 服务端时间戳 ms, example: 1700000000000 } + code: { type: string, description: "成功为 \"ok\",失败为 i18n key", example: ok } + message: { type: string, nullable: true, description: 业务异常时的本地化文案 } + stack: { type: string, nullable: true, description: 服务端堆栈(调试用,可忽略) } + ApiErrorResponse: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: + code: { type: string, enum: [task.rebalance.disabled, task.rebalance.taskRebalancing, task.rebalance.alreadyRunning, task.rebalance.noTask, task.rebalance.onlyOneAgent, task.rebalance.offlineTaskOnAgent, NotLogin, NotAuthorized, SystemError, IllegalArgument, IllegalState] } + data: { nullable: true } + ApiResponseOfPreview: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: { data: { $ref: '#/components/schemas/TaskRebalancePreviewVo' } } + ApiResponseOfRebalance: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: { data: { $ref: '#/components/schemas/TaskRebalanceVo' } } + ApiResponseOfPageRebalance: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: { data: { $ref: '#/components/schemas/PageOfTaskRebalanceVo' } } + ApiResponseOfActive: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: + data: + type: object + required: [active] + properties: { active: { type: boolean, example: true } } + ApiResponseOfDetail: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: { data: { $ref: '#/components/schemas/TaskRebalanceDetailVo' } } + ApiResponseOfVoid: + allOf: + - $ref: '#/components/schemas/ApiResponseBase' + - type: object + properties: { data: { nullable: true } } + PageOfTaskRebalanceVo: + type: object + required: [total, items] + properties: + total: { type: integer, format: int64, example: 12 } + items: + type: array + items: { $ref: '#/components/schemas/TaskRebalanceVo' } + + + TaskRebalancePreviewVo: + type: object + description: 预览结果,整体阻断时只填 reason,否则 tasks 包含全部统计任务(含不可调走的) + properties: + tasks: + type: array + items: { $ref: '#/components/schemas/TaskPreview' } + moveCount: { type: integer, description: 将被实际迁移的任务数 (changed=true 的计数), example: 3 } + reason: + type: string + nullable: true + description: 整体阻断原因(i18n key),有值则忽略 tasks + example: task.rebalance.onlyOneAgent + TaskPreview: + type: object + properties: + taskId: { type: string, example: 6630e9c8a2c5b80012a1b2c3 } + taskName: { type: string, example: 订单同步 } + type: { type: string, enum: [initial_sync, cdc, "initial_sync+cdc"] } + syncType: { type: string, enum: [sync, migrate, logCollector, mem_cache, connHeartbeat] } + status: { type: string, example: running, description: 来自 TaskDto.status } + sourceAgentId: { type: string, example: agent-01 } + targetAgentId: { type: string, description: 算法建议迁移到的 agent;不可调走时等于 sourceAgentId, example: agent-02 } + movable: { type: boolean } + schedulableStatus: { $ref: '#/components/schemas/SchedulableStatus' } + changed: { type: boolean, description: 本次是否被规划迁移 } + reason: { type: string, nullable: true, description: 不可调走的中文/英文说明 } + priorityScore: { type: integer, description: 值越大越优先迁移, example: 5230 } + priorityScoreItems: + type: object + description: 评分明细(前端可做 tooltip) + properties: + syncType: { type: integer } + nodeCount: { type: integer } + startTime: { type: integer } + nodeCount: { type: integer, description: DAG 节点数 } + startTime: { type: integer, format: int64, nullable: true, description: 任务启动毫秒时间戳 } + TaskRebalanceVo: + type: object + properties: + id: { type: string } + name: { type: string, example: Task rebalance 1700000000000 } + status: { $ref: '#/components/schemas/RebalanceStatus' } + createTime: { type: string, format: date-time } + lastUpdated: { type: string, format: date-time } + finishAt: { type: string, format: date-time, nullable: true } + totalCount: { type: integer } + pendingCount: { type: integer } + stoppingCount: { type: integer } + startingCount: { type: integer } + okCount: { type: integer } + failedCount: { type: integer } + cancelledCount: { type: integer } + errorMesg: { type: string, nullable: true, description: 整体中止原因(例如目标 agent 离线导致 abort) } + userId: { type: string } + createUser: { type: string } + lastUpdBy: { type: string } + customId: { type: string } + TaskRebalanceDetailVo: + type: object + required: [rebalance, jobs] + properties: + rebalance: { $ref: '#/components/schemas/TaskRebalanceVo' } + jobs: + type: array + items: { $ref: '#/components/schemas/TaskRebalanceJobVo' } + TaskRebalanceJobVo: + type: object + properties: + id: { type: string } + rebalanceId: { type: string } + taskId: { type: string } + taskName: { type: string } + status: { $ref: '#/components/schemas/JobStatus' } + errorMesg: { type: string, nullable: true } + sourceAgentId: { type: string } + targetAgentId: { type: string } + beginAt: { type: string, format: date-time, nullable: true } + finishAt: { type: string, format: date-time, nullable: true } + createTime: { type: string, format: date-time } + lastUpdated: { type: string, format: date-time } + userId: { type: string } + createUser: { type: string } + lastUpdBy: { type: string } + customId: { type: string } + RebalanceStatus: + type: string + enum: [RUNNING, OK, FAILED] + description: | + - RUNNING:调度中 + - OK:全部子任务成功 + - FAILED:存在失败或取消的子任务 + JobStatus: + type: string + enum: [PENDING, STOPPING, STARTING, OK, CANCELLED, INVALID_AGENT, STATUS_ERROR, STOP_TIMEOUT, START_TIMEOUT] + description: | + - PENDING:等待调度 + - STOPPING:已下发暂停 + - STARTING:暂停完成,已切换 agentId,下发启动 + - OK:成功 + - CANCELLED:用户取消,或被目标 agent 离线中止兜底 + - INVALID_AGENT:目标 agent 离线(触发整次均衡 abort) + - STATUS_ERROR:执行期任务状态/agent 已变化,不能迁移 + - STOP_TIMEOUT:等 STATUS_STOP 超时;任务可能滞留在源 agent + - START_TIMEOUT:等 STATUS_RUNNING 超时;agentId 已切到 target,需人工确认 + SchedulableStatus: + type: string + enum: [OK, AGENT_OFFLINE, STATUS_ERROR, MANUAL_AGENT, INCREMENTAL_NOT_STARTED] + description: | + - OK:可调走 + - AGENT_OFFLINE:当前 agent 已离线 + - STATUS_ERROR:任务非 running + - MANUAL_AGENT:accessNodeType 为手动指定 agent 或 agent 组 + - INCREMENTAL_NOT_STARTED:initial_sync+cdc 任务还未进入增量阶段 diff --git a/packages/api/src/core/index.ts b/packages/api/src/core/index.ts index e73304928..055e46f4f 100644 --- a/packages/api/src/core/index.ts +++ b/packages/api/src/core/index.ts @@ -57,3 +57,4 @@ export * from './live-data-platform' export * from './group-info' export * from './ai' export * from './data-trace' +export * from './task-rebalance' diff --git a/packages/api/src/core/task-rebalance.ts b/packages/api/src/core/task-rebalance.ts new file mode 100644 index 000000000..c31797a35 --- /dev/null +++ b/packages/api/src/core/task-rebalance.ts @@ -0,0 +1,152 @@ +import { requestClient, type Filter, type PageFetchResult } from '../request' + +const BASE_URL = '/api/task/rebalance' + +export type RebalanceStatus = 'RUNNING' | 'OK' | 'FAILED' | 'CANCELLED' + +export type JobStatus = + | 'PENDING' + | 'STOPPING' + | 'STARTING' + | 'OK' + | 'CANCELLED' + | 'INVALID_AGENT' + | 'STATUS_ERROR' + | 'STOP_TIMEOUT' + | 'START_TIMEOUT' + | 'FAILED' + +export type SchedulableStatus = + | 'OK' + | 'AGENT_OFFLINE' + | 'STATUS_ERROR' + | 'MANUAL_AGENT' + | 'INCREMENTAL_NOT_STARTED' + +export interface TaskPreview { + taskId: string + taskName: string + type: 'initial_sync' | 'cdc' | 'initial_sync+cdc' + syncType: 'sync' | 'migrate' | 'logCollector' | 'mem_cache' | 'connHeartbeat' + status: string + sourceAgentId: string + targetAgentId: string + movable: boolean + schedulableStatus: SchedulableStatus + changed: boolean + reason?: string | null + priorityScore: number + priorityScoreItems?: { + syncType?: number + nodeCount?: number + startTime?: number + } + nodeCount: number + startTime?: number | null +} + +export interface TaskRebalancePreviewVo { + tasks?: TaskPreview[] + moveCount?: number + reason?: string | null +} + +export interface TaskRebalanceVo { + id: string + name: string + status: RebalanceStatus + createTime: string + lastUpdated: string + finishAt?: string | null + totalCount: number + pendingCount: number + stoppingCount: number + startingCount: number + okCount: number + failedCount: number + cancelledCount: number + errorMesg?: string | null + userId: string + createUser: string + lastUpdBy: string + customId: string +} + +export interface TaskRebalanceJobVo { + id: string + rebalanceId: string + taskId: string + taskName: string + status: JobStatus + errorMesg?: string | null + sourceAgentId: string + targetAgentId: string + beginAt?: string | null + finishAt?: string | null + createTime: string + lastUpdated: string + userId: string + createUser: string + lastUpdBy: string + customId: string +} + +export interface TaskRebalanceDetailVo { + rebalance: TaskRebalanceVo + jobs: TaskRebalanceJobVo[] +} + +export interface TaskRebalanceActiveVo { + active: boolean +} + +/** + * 生成均衡预览,不落库、不执行 + */ +export function previewTaskRebalance() { + return requestClient.post(`${BASE_URL}/preview`) +} + +/** + * 创建并启动均衡 + */ +export function createTaskRebalance(data?: TaskRebalancePreviewVo) { + return requestClient.post(BASE_URL, data) +} + +/** + * 历史列表 (分页) + */ +export function fetchTaskRebalances(filter?: Filter) { + return requestClient.get>(BASE_URL, { + params: { filter: filter ? JSON.stringify(filter) : undefined }, + }) +} + +/** + * 是否有正在运行的均衡 + */ +export function hasActiveTaskRebalance() { + return requestClient.get(`${BASE_URL}/active`) +} + +/** + * 均衡详情 (含子任务) + */ +export function getTaskRebalanceDetail(id: string) { + return requestClient.get(`${BASE_URL}/${id}`) +} + +/** + * 取消该均衡下所有 PENDING 子任务 + */ +export function cancelTaskRebalance(id: string) { + return requestClient.post(`${BASE_URL}/${id}/cancel`) +} + +/** + * 取消单个 PENDING 子任务 + */ +export function cancelTaskRebalanceJob(id: string, taskId: string) { + return requestClient.post(`${BASE_URL}/${id}/cancel/${taskId}`) +} diff --git a/packages/business/src/views/heartbeat-table/List.vue b/packages/business/src/views/heartbeat-table/List.vue index 5050ee4a7..45641677b 100644 --- a/packages/business/src/views/heartbeat-table/List.vue +++ b/packages/business/src/views/heartbeat-table/List.vue @@ -1,4 +1,5 @@ - + - +
diff --git a/packages/business/src/views/shared-mining/List.vue b/packages/business/src/views/shared-mining/List.vue index 4f7745504..01437f639 100644 --- a/packages/business/src/views/shared-mining/List.vue +++ b/packages/business/src/views/shared-mining/List.vue @@ -1,4 +1,5 @@ - @@ -597,7 +592,7 @@ export default { :close-on-click-modal="false" > - +