From 5ef8621db795e3101ff2fec9422904921937385d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 12 Sep 2025 23:53:10 +0000 Subject: [PATCH] feat(task-execution): implement task cancellation handling and improve UI feedback for canceling tasks --- ts/manager.task/classes.taskexecution.ts | 2 +- ts/manager.task/classes.taskmanager.ts | 39 ++++++++++++----- ts/manager.task/predefinedtasks.ts | 54 +++++++++++++++++++++++- ts_web/elements/cloudly-view-tasks.ts | 30 +++++++++---- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/ts/manager.task/classes.taskexecution.ts b/ts/manager.task/classes.taskexecution.ts index 7c2f672..03c4650 100644 --- a/ts/manager.task/classes.taskexecution.ts +++ b/ts/manager.task/classes.taskexecution.ts @@ -63,7 +63,7 @@ export class TaskExecution extends plugins.smartdata.SmartDataDbDoc< } // INSTANCE - @plugins.smartdata.svDb() + @plugins.smartdata.unI() public id: string; @plugins.smartdata.svDb() diff --git a/ts/manager.task/classes.taskmanager.ts b/ts/manager.task/classes.taskmanager.ts index fb410c0..b04da0d 100644 --- a/ts/manager.task/classes.taskmanager.ts +++ b/ts/manager.task/classes.taskmanager.ts @@ -22,6 +22,7 @@ export class CloudlyTaskManager { private taskRegistry = new Map(); private taskInfo = new Map(); private currentExecutions = new Map(); + private cancellationRequests = new Set(); // Database connection helper get db() { @@ -107,9 +108,14 @@ export class CloudlyTaskManager { // Execute the task const result = await task.trigger(); - // Task completed successfully - await execution.complete(result); - await execution.addLog(`Task completed successfully`, 'success'); + // If a cancellation was requested during execution, don't mark as completed + if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) { + await execution.addLog('Task cancelled during execution', 'warning'); + } else { + // Task completed successfully + await execution.complete(result); + await execution.addLog(`Task completed successfully`, 'success'); + } // Update last run time if (info) { @@ -117,13 +123,19 @@ export class CloudlyTaskManager { } } catch (error) { - // Task failed - await execution.fail(error); - await execution.addLog(`Task failed: ${error.message}`, 'error'); - logger.log('error', `Task ${taskName} failed: ${error.message}`); + // If already cancelled, don't mark as failed + if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) { + await execution.addLog('Task was cancelled', 'warning'); + } else { + // Task failed + await execution.fail(error as any); + await execution.addLog(`Task failed: ${(error as any).message}`, 'error'); + logger.log('error', `Task ${taskName} failed: ${(error as any).message}`); + } } finally { // Clean up current execution this.currentExecutions.delete(taskName); + this.cancellationRequests.delete(execution.id); } return execution; @@ -168,12 +180,19 @@ export class CloudlyTaskManager { await execution.cancel(); await execution.addLog('Task cancelled by user', 'warning'); - - // TODO: Implement actual task cancellation in taskbuffer + // mark cancellation request so running task functions can react cooperatively + this.cancellationRequests.add(execution.id); return true; } + /** + * Check if cancellation is requested for an execution + */ + public isCancellationRequested(executionId: string): boolean { + return this.cancellationRequests.has(executionId); + } + /** * Get all registered tasks */ @@ -327,4 +346,4 @@ export class CloudlyTaskManager { await this.taskBufferManager.stop(); logger.log('info', 'Task Manager stopped'); } -} \ No newline at end of file +} diff --git a/ts/manager.task/predefinedtasks.ts b/ts/manager.task/predefinedtasks.ts index 8ad1b9a..e1b01bd 100644 --- a/ts/manager.task/predefinedtasks.ts +++ b/ts/manager.task/predefinedtasks.ts @@ -16,6 +16,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Starting DNS synchronization...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting DNS sync...', 'warning'); + return; + } // Get all DNS entries marked as external const dnsEntries = await dnsManager.CDnsEntry.getInstances({ @@ -29,6 +33,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { let failedCount = 0; for (const entry of dnsEntries) { + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Stopping loop...', 'warning'); + break; + } try { // TODO: Implement actual sync with external DNS provider await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info'); @@ -66,6 +74,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Checking certificates for renewal...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting certificate renewal...', 'warning'); + return; + } // Get all domains const domains = await taskManager.cloudlyRef.domainManager.CDomain.getInstances({}); @@ -75,6 +87,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { let upToDateCount = 0; for (const domain of domains) { + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Stopping loop...', 'warning'); + break; + } // TODO: Check certificate expiry and renew if needed await execution?.addLog(`Checking certificate for ${domain.data.name}`, 'info'); @@ -117,17 +133,23 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Starting cleanup tasks...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting cleanup...', 'warning'); + return; + } // Clean up old task executions await execution?.addLog('Cleaning old task executions...', 'info'); const deletedExecutions = await taskManager.CTaskExecution.cleanupOldExecutions(30); await execution?.setMetric('deletedExecutions', deletedExecutions); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return; // TODO: Clean up old logs await execution?.addLog('Cleaning old logs...', 'info'); // Placeholder const deletedLogs = 0; await execution?.setMetric('deletedLogs', deletedLogs); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return; // TODO: Clean up Docker images await execution?.addLog('Cleaning unused Docker images...', 'info'); @@ -164,6 +186,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Starting health checks...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting health checks...', 'warning'); + return; + } // Check all deployments const deployments = await taskManager.cloudlyRef.deploymentManager.getAllDeployments(); @@ -174,6 +200,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { const issues = []; for (const deployment of deployments) { + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Stopping loop...', 'warning'); + break; + } if (deployment.status === 'running') { // TODO: Actual health check logic const isHealthy = Math.random() > 0.1; // 90% healthy for demo @@ -225,6 +255,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Generating resource usage report...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting report...', 'warning'); + return; + } // Get all nodes const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({}); @@ -238,6 +272,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { }; for (const node of nodes) { + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Stopping loop...', 'warning'); + break; + } // TODO: Get actual resource usage const nodeUsage = { nodeId: node.id, @@ -289,10 +327,16 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Starting database maintenance...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting DB maintenance...', 'warning'); + return; + } // TODO: Implement actual database maintenance await execution?.addLog('Analyzing indexes...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return; await execution?.addLog('Compacting collections...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return; await execution?.addLog('Updating statistics...', 'info'); await execution?.setMetric('collectionsOptimized', 5); // Placeholder @@ -323,6 +367,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { try { await execution?.addLog('Starting security scan...', 'info'); + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Aborting security scan...', 'warning'); + return; + } const vulnerabilities = []; @@ -335,6 +383,10 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { const images = await taskManager.cloudlyRef.imageManager.CImage.getInstances({}); for (const image of images) { + if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) { + await execution.addLog('Cancellation requested. Stopping loop...', 'warning'); + break; + } // TODO: Check if image is outdated const isOutdated = Math.random() > 0.7; // 30% outdated for demo @@ -429,4 +481,4 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) { }); logger.log('info', 'Predefined tasks registered successfully'); -} \ No newline at end of file +} diff --git a/ts_web/elements/cloudly-view-tasks.ts b/ts_web/elements/cloudly-view-tasks.ts index bb89fda..2da6146 100644 --- a/ts_web/elements/cloudly-view-tasks.ts +++ b/ts_web/elements/cloudly-view-tasks.ts @@ -37,6 +37,8 @@ export class CloudlyViewTasks extends DeesElement { private autoRefresh: boolean = true; private _refreshHandle: any = null; + @state() + private canceling: Record = {}; constructor() { super(); @@ -492,14 +494,19 @@ export class CloudlyViewTasks extends DeesElement { const running = executions[0]; if (!running) return; - await appstate.dataState.dispatchAction( - appstate.taskActions.cancelTask, { executionId: running.id } - ); - plugins.deesCatalog.DeesToast.createAndShow({ - message: `Cancelled ${taskName}`, - type: 'success', - }); - await this.loadExecutionsWithFilter(); + this.canceling = { ...this.canceling, [running.id]: true }; + try { + await appstate.dataState.dispatchAction( + appstate.taskActions.cancelTask, { executionId: running.id } + ); + plugins.deesCatalog.DeesToast.createAndShow({ + message: `Cancelled ${taskName}`, + type: 'success', + }); + } finally { + this.canceling = { ...this.canceling, [running.id]: false }; + await this.loadExecutionsWithFilter(); + } } catch (err) { console.error('Failed to cancel task:', err); plugins.deesCatalog.DeesToast.createAndShow({ @@ -642,7 +649,12 @@ export class CloudlyViewTasks extends DeesElement { ${lastExecution ? html`${lastExecution.data.status}` : html`idle`} ${isRunning ? html` - this.cancelTaskFor(task.name)}> + this.cancelTaskFor(task.name)} + > ` : html` this.triggerTask(task.name)}> `}