fix: task management bugs rustfs/rustfs#170

This commit is contained in:
马登山
2025-07-11 14:40:35 +08:00
parent 174cba03b7
commit b3194ad1b4
4 changed files with 65 additions and 45 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"semi": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
@@ -9,4 +9,4 @@
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false
}
}
+2 -1
View File
@@ -394,5 +394,6 @@
"Configuration saved successfully": "Configuration saved successfully",
"Configuration reset successfully": "Configuration reset successfully",
"Reset to default successfully": "Reset to default successfully",
"Leave empty to use current host as default": "Leave empty to use current host as default"
"Leave empty to use current host as default": "Leave empty to use current host as default",
"All files are already in Tasks.": "All files are already in Tasks."
}
+2 -1
View File
@@ -587,5 +587,6 @@
"Configuration saved successfully": "配置保存成功",
"Configuration reset successfully": "配置重置成功",
"Reset to default successfully": "重置为默认成功",
"Leave empty to use current host as default": "留空表示使用当前主机作为默认值"
"Leave empty to use current host as default": "留空表示使用当前主机作为默认值",
"All files are already in Tasks.": "所有文件都已经在任务中。"
}
+59 -41
View File
@@ -1,11 +1,17 @@
import { CompleteMultipartUploadCommand, CreateMultipartUploadCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
import {
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
export interface UploadTask {
id: string;
file: File;
bucketName: string;
prefix?: string;
status: "pending" | "uploading" | "completed" | "failed" | "canceled";
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'canceled';
progress: number;
error?: string;
abortController?: AbortController;
@@ -38,29 +44,32 @@ class UploadTaskManager {
}
addFiles(files: File[], bucketName: string, prefix?: string) {
const newTasks = files.map((file) => ({
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${file.name}`,
file,
status: "pending" as const,
progress: 0,
bucketName,
prefix: prefix ?? "",
retryCount: 0,
}));
// 队列中所有任务(不管状态)
const existKeys = new Set(
this.tasks.map(task => `${task.bucketName}/${task.prefix || ''}${task.file.name}`)
);
// 过滤已存在的任务
const newTasks = files
.filter(file => !existKeys.has(`${bucketName}/${prefix || ''}${file.name}`))
.map(file => ({
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${file.name}`,
file,
status: 'pending' as const,
progress: 0,
bucketName,
prefix: prefix ?? '',
retryCount: 0,
}));
// 批量添加任务
this.tasks.push(...newTasks);
console.debug("Added files to upload queue", files.length, "files");
// 启动处理(如果尚未启动)
this.start();
}
start() {
if (!this.isStarted) {
this.isStarted = true;
this.processQueue();
}
this.processQueue();
}
stop() {
@@ -70,36 +79,36 @@ class UploadTaskManager {
cancel() {
// 取消所有待处理的任务
this.tasks
.filter((task) => task.status === "pending")
.forEach((task) => {
task.status = "canceled";
.filter(task => task.status === 'pending')
.forEach(task => {
task.status = 'canceled';
});
// 取消所有正在进行的上传
this.tasks
.filter((task) => task.status === "uploading" && task.abortController)
.forEach((task) => {
.filter(task => task.status === 'uploading' && task.abortController)
.forEach(task => {
task.abortController?.abort();
task.status = "canceled";
task.status = 'canceled';
});
}
cancelTask(taskId: string) {
const task = this.tasks.find((task) => task.id === taskId);
const task = this.tasks.find(task => task.id === taskId);
if (task) {
if (task.abortController) {
task.abortController.abort();
}
task.status = "canceled";
task.status = 'canceled';
}
}
removeTask(taskId: string) {
const taskIndex = this.tasks.findIndex((task) => task.id === taskId);
const taskIndex = this.tasks.findIndex(task => task.id === taskId);
if (taskIndex !== -1) {
// 如果任务正在进行,先取消它
const task = this.tasks[taskIndex];
if (task.status === "uploading" && task.abortController) {
if (task.status === 'uploading' && task.abortController) {
task.abortController.abort();
}
this.tasks.splice(taskIndex, 1);
@@ -128,7 +137,7 @@ class UploadTaskManager {
// 启动新的上传任务
while (this.activeUploads < this.maxConcurrentUploads) {
const nextTask = this.tasks.find((task) => task.status === "pending");
const nextTask = this.tasks.find(task => task.status === 'pending');
if (!nextTask) {
break;
}
@@ -139,7 +148,7 @@ class UploadTaskManager {
}
// 如果还有待处理的任务,继续处理
if (this.tasks.some(task => task.status === "pending")) {
if (this.tasks.some(task => task.status === 'pending')) {
// 使用 setTimeout 避免阻塞主线程
setTimeout(() => {
this.processQueue();
@@ -148,7 +157,7 @@ class UploadTaskManager {
}
private async startUpload(task: UploadTask) {
task.status = "uploading";
task.status = 'uploading';
task.progress = 0;
try {
@@ -157,22 +166,22 @@ class UploadTaskManager {
} else {
await this.putObject(task);
}
task.status = "completed";
task.status = 'completed';
task.progress = 100;
} catch (error: any) {
// 检查是否是取消操作
if (error.name === 'AbortError' || error.message?.includes('canceled')) {
task.status = "canceled";
task.status = 'canceled';
return;
}
console.error("Upload failed:", error.message, "Task:", task.id);
console.error('Upload failed:', error.message, 'Task:', task.id);
// 检查是否应该重试
if (this.shouldRetry(task, error)) {
await this.retryTask(task);
} else {
task.status = "failed";
task.status = 'failed';
task.error = error.message;
}
} finally {
@@ -186,7 +195,7 @@ class UploadTaskManager {
private shouldRetry(task: UploadTask, error: any): boolean {
// 如果任务被取消,不重试
if (task.status === "canceled") {
if (task.status === 'canceled') {
return false;
}
@@ -195,6 +204,16 @@ class UploadTaskManager {
return false;
}
// 如果是 S3 返回的 4xx 错误,直接 failed
if (
error.$metadata &&
error.$metadata.httpStatusCode &&
error.$metadata.httpStatusCode >= 400 &&
error.$metadata.httpStatusCode < 500
) {
return false;
}
// 检查错误类型,某些错误不应该重试
const errorMessage = error.message?.toLowerCase() || '';
const nonRetryableErrors = [
@@ -202,7 +221,7 @@ class UploadTaskManager {
'forbidden',
'invalid credentials',
'bucket not found',
'file not found'
'file not found',
];
return !nonRetryableErrors.some(msg => errorMessage.includes(msg));
@@ -210,7 +229,7 @@ class UploadTaskManager {
private async retryTask(task: UploadTask) {
task.retryCount = (task.retryCount || 0) + 1;
task.status = "pending";
task.status = 'pending';
task.progress = 0;
task.error = undefined;
@@ -254,7 +273,7 @@ class UploadTaskManager {
});
const createResponse = await this.s3Client.send(createCommand, {
abortSignal: abortController.signal
abortSignal: abortController.signal,
});
uploadId = createResponse.UploadId;
@@ -285,7 +304,7 @@ class UploadTaskManager {
});
const { ETag } = await this.s3Client.send(uploadPartCommand, {
abortSignal: abortController.signal
abortSignal: abortController.signal,
});
if (!ETag) {
@@ -305,14 +324,13 @@ class UploadTaskManager {
});
await this.s3Client.send(completeCommand, {
abortSignal: abortController.signal
abortSignal: abortController.signal,
});
} catch (error: any) {
// 如果上传失败,尝试清理分片上传
if (uploadId) {
try {
const { AbortMultipartUploadCommand } = await import("@aws-sdk/client-s3");
const { AbortMultipartUploadCommand } = await import('@aws-sdk/client-s3');
const abortCommand = new AbortMultipartUploadCommand({
Bucket: bucketName,
Key: Key,