@nocobase/plugin-async-task-manager 2.0.0-alpha.9 → 2.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,14 +36,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
  var base_task_manager_exports = {};
38
38
  __export(base_task_manager_exports, {
39
- BaseTaskManager: () => BaseTaskManager
39
+ BaseTaskManager: () => BaseTaskManager,
40
+ ConcurrencyMonitorDelegate: () => ConcurrencyMonitorDelegate
40
41
  });
41
42
  module.exports = __toCommonJS(base_task_manager_exports);
42
43
  var import_lodash = require("lodash");
43
44
  var import_constants = require("../common/constants");
44
45
  var import_crypto = require("crypto");
45
46
  var import_plugin = __toESM(require("./plugin"));
47
+ var import_base_concurrency_monitor = require("./base-concurrency-monitor");
46
48
  const WORKER_JOB_ASYNC_TASK_PROCESS = "async-task:process";
49
+ const CONCURRENCY = process.env.ASYNC_TASK_MAX_CONCURRENCY ? Number.parseInt(process.env.ASYNC_TASK_MAX_CONCURRENCY, 10) : 3;
50
+ const CONCURRENCY_MODE = process.env.ASYNC_TASK_CONCURRENCY_MODE ?? "app";
51
+ const PROCESS_CONCURRENCY_MONITOR = new import_base_concurrency_monitor.BaseConcurrencyMonitor(CONCURRENCY);
47
52
  class BaseTaskManager {
48
53
  taskTypes = /* @__PURE__ */ new Map();
49
54
  tasks = /* @__PURE__ */ new Map();
@@ -53,13 +58,25 @@ class BaseTaskManager {
53
58
  logger;
54
59
  app;
55
60
  progressThrottles = /* @__PURE__ */ new Map();
56
- concurrency = process.env.ASYNC_TASK_MAX_CONCURRENCY ? Number.parseInt(process.env.ASYNC_TASK_MAX_CONCURRENCY, 10) : 3;
57
- idle = () => {
58
- return this.app.serving(WORKER_JOB_ASYNC_TASK_PROCESS) && this.tasks.size < this.concurrency;
59
- };
60
- onQueueTask = async ({ id }) => {
61
+ concurrencyMonitor = new ConcurrencyMonitorDelegate();
62
+ get concurrency() {
63
+ return this.concurrencyMonitor.concurrency;
64
+ }
65
+ set concurrency(concurrency) {
66
+ this.concurrencyMonitor.concurrency = concurrency;
67
+ }
68
+ idle = () => this.app.serving(WORKER_JOB_ASYNC_TASK_PROCESS) && this.concurrencyMonitor.idle();
69
+ onQueueTask = async ({ id }, { queueOptions }) => {
61
70
  const task = await this.prepareTask(id);
62
- await this.runTask(task);
71
+ if (!this.concurrencyMonitor.increase(task.record.id)) {
72
+ this.enqueueTask(task, queueOptions);
73
+ return;
74
+ }
75
+ try {
76
+ await this.runTask(task);
77
+ } finally {
78
+ this.concurrencyMonitor.decrease(task.record.id);
79
+ }
63
80
  };
64
81
  onTaskProgress = (item) => {
65
82
  const userId = item.createdById;
@@ -105,6 +122,7 @@ class BaseTaskManager {
105
122
  if (task.doneAt) {
106
123
  this.progressThrottles.delete(task.id);
107
124
  this.tasks.delete(task.id);
125
+ this.concurrencyMonitor.decrease(task.id);
108
126
  }
109
127
  if (task.status === import_constants.TASK_STATUS.SUCCEEDED) {
110
128
  this.app.emit("workflow:dispatch");
@@ -113,6 +131,7 @@ class BaseTaskManager {
113
131
  onTaskAfterDelete = (task) => {
114
132
  this.tasks.delete(task.id);
115
133
  this.progressThrottles.delete(task.id);
134
+ this.concurrencyMonitor.decrease(task.id);
116
135
  const userId = task.createdById;
117
136
  if (userId) {
118
137
  this.app.emit("ws:sendToUser", {
@@ -130,31 +149,44 @@ class BaseTaskManager {
130
149
  cleanup = async () => {
131
150
  this.logger.debug("Running cleanup for completed tasks...");
132
151
  const TaskRepo = this.app.db.getRepository("asyncTasks");
133
- const tasksToCleanup = await TaskRepo.find({
134
- fields: ["id"],
135
- filter: {
136
- $or: [
137
- {
138
- status: [import_constants.TASK_STATUS.SUCCEEDED, import_constants.TASK_STATUS.FAILED],
139
- doneAt: {
140
- $lt: new Date(Date.now() - this.cleanupDelay)
152
+ try {
153
+ const tasksToCleanup = await TaskRepo.find({
154
+ fields: ["id"],
155
+ filter: {
156
+ $or: [
157
+ {
158
+ status: [import_constants.TASK_STATUS.SUCCEEDED, import_constants.TASK_STATUS.FAILED],
159
+ doneAt: {
160
+ $lt: new Date(Date.now() - this.cleanupDelay)
161
+ }
162
+ },
163
+ {
164
+ status: import_constants.TASK_STATUS.CANCELED
141
165
  }
142
- },
143
- {
144
- status: import_constants.TASK_STATUS.CANCELED
145
- }
146
- ]
166
+ ]
167
+ }
168
+ });
169
+ this.logger.debug(`Found ${tasksToCleanup.length} tasks to cleanup`);
170
+ if (tasksToCleanup.length) {
171
+ for (const task of tasksToCleanup) {
172
+ this.tasks.delete(task.id);
173
+ this.progressThrottles.delete(task.id);
174
+ this.concurrencyMonitor.decrease(task.id);
175
+ }
176
+ await TaskRepo.destroy({
177
+ filterByTk: tasksToCleanup.map((task) => task.id),
178
+ individualHooks: true
179
+ });
180
+ }
181
+ } catch (error) {
182
+ this.logger.error(`DB error during cleanup: ${error.message}. Will stop cleanup timer.`, { error });
183
+ if (error.name === "SequelizeConnectionAcquireTimeoutError") {
184
+ if (this.cleanupTimer) {
185
+ clearInterval(this.cleanupTimer);
186
+ this.cleanupTimer = null;
187
+ }
147
188
  }
148
- });
149
- this.logger.debug(`Found ${tasksToCleanup.length} tasks to cleanup`);
150
- for (const task of tasksToCleanup) {
151
- this.tasks.delete(task.id);
152
- this.progressThrottles.delete(task.id);
153
189
  }
154
- await TaskRepo.destroy({
155
- filterByTk: tasksToCleanup.map((task) => task.id),
156
- individualHooks: true
157
- });
158
190
  };
159
191
  getThrottledProgressEmitter(taskId, userId) {
160
192
  if (!this.progressThrottles.has(taskId)) {
@@ -314,7 +346,33 @@ class BaseTaskManager {
314
346
  }
315
347
  }
316
348
  }
349
+ class ConcurrencyMonitorDelegate {
350
+ constructor(mode = CONCURRENCY_MODE, appConcurrencyMonitor = new import_base_concurrency_monitor.BaseConcurrencyMonitor(CONCURRENCY), processConcurrencyMonitor = PROCESS_CONCURRENCY_MONITOR) {
351
+ this.mode = mode;
352
+ this.appConcurrencyMonitor = appConcurrencyMonitor;
353
+ this.processConcurrencyMonitor = processConcurrencyMonitor;
354
+ }
355
+ get concurrencyMonitor() {
356
+ return this.mode === "process" ? this.processConcurrencyMonitor : this.appConcurrencyMonitor;
357
+ }
358
+ idle() {
359
+ return this.concurrencyMonitor.idle();
360
+ }
361
+ get concurrency() {
362
+ return this.concurrencyMonitor.concurrency;
363
+ }
364
+ set concurrency(concurrency) {
365
+ this.concurrencyMonitor.concurrency = concurrency;
366
+ }
367
+ increase(taskId) {
368
+ return this.concurrencyMonitor.increase(taskId);
369
+ }
370
+ decrease(taskId) {
371
+ this.concurrencyMonitor.decrease(taskId);
372
+ }
373
+ }
317
374
  // Annotate the CommonJS export names for ESM import in node:
318
375
  0 && (module.exports = {
319
- BaseTaskManager
376
+ BaseTaskManager,
377
+ ConcurrencyMonitorDelegate
320
378
  });
@@ -9,6 +9,7 @@
9
9
  /// <reference types="node" />
10
10
  import { Worker } from 'worker_threads';
11
11
  import { TaskType } from './task-type';
12
+ export declare function parseArgv(list: string[]): any;
12
13
  export declare class CommandTaskType extends TaskType {
13
14
  static type: string;
14
15
  workerThread: Worker;
@@ -36,7 +36,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
  var command_task_type_exports = {};
38
38
  __export(command_task_type_exports, {
39
- CommandTaskType: () => CommandTaskType
39
+ CommandTaskType: () => CommandTaskType,
40
+ parseArgv: () => parseArgv
40
41
  });
41
42
  module.exports = __toCommonJS(command_task_type_exports);
42
43
  var import_async_task_manager = require("./interfaces/async-task-manager");
@@ -44,6 +45,51 @@ var import_node_process = __toESM(require("node:process"));
44
45
  var import_worker_threads = require("worker_threads");
45
46
  var import_path = __toESM(require("path"));
46
47
  var import_task_type = require("./task-type");
48
+ const getResourceLimitsFromEnv = () => {
49
+ let resourceLimitsUndefined = true;
50
+ const resourceLimits = {};
51
+ if (import_node_process.default.env.ASYNC_TASK_WORKER_MAX_OLD) {
52
+ resourceLimits.maxOldGenerationSizeMb = Number.parseInt(import_node_process.default.env.ASYNC_TASK_WORKER_MAX_OLD, 10);
53
+ resourceLimitsUndefined = false;
54
+ }
55
+ if (import_node_process.default.env.ASYNC_TASK_WORKER_MAX_YOUNG) {
56
+ resourceLimits.maxYoungGenerationSizeMb = Number.parseInt(import_node_process.default.env.ASYNC_TASK_WORKER_MAX_YOUNG, 10);
57
+ resourceLimitsUndefined = false;
58
+ }
59
+ return resourceLimitsUndefined ? void 0 : resourceLimits;
60
+ };
61
+ const RESOURCE_LIMITS = getResourceLimitsFromEnv();
62
+ function parseArgv(list) {
63
+ const argv = {};
64
+ for (const item of list) {
65
+ const match = item.match(/^--([^=]+)=(.*)$/);
66
+ if (match) {
67
+ const key = match[1];
68
+ let value = match[2];
69
+ if (value.startsWith("{") || value.startsWith("[")) {
70
+ try {
71
+ value = JSON.parse(value);
72
+ } catch (err) {
73
+ }
74
+ } else {
75
+ if (value === "true") {
76
+ value = true;
77
+ } else if (value === "false") {
78
+ value = false;
79
+ }
80
+ }
81
+ argv[key] = value;
82
+ continue;
83
+ }
84
+ const parts = item.split(":");
85
+ if (parts.length === 2) {
86
+ const command = parts[0];
87
+ const commandValue = parts[1];
88
+ argv[command] = commandValue;
89
+ }
90
+ }
91
+ return argv;
92
+ }
47
93
  class CommandTaskType extends import_task_type.TaskType {
48
94
  static type = "command";
49
95
  workerThread;
@@ -55,6 +101,7 @@ class CommandTaskType extends import_task_type.TaskType {
55
101
  async execute() {
56
102
  var _a;
57
103
  const { argv } = this.record.params;
104
+ const parsedArgv = parseArgv(argv);
58
105
  const isDev = (((_a = import_node_process.default.argv[1]) == null ? void 0 : _a.endsWith(".ts")) || import_node_process.default.argv[1].includes("tinypool")) ?? false;
59
106
  const appRoot = import_node_process.default.env.APP_PACKAGE_ROOT || "packages/core/app";
60
107
  const workerPath = import_path.default.resolve(import_node_process.default.cwd(), appRoot, isDev ? "src/index.ts" : "lib/index.js");
@@ -73,8 +120,10 @@ class CommandTaskType extends import_task_type.TaskType {
73
120
  },
74
121
  env: {
75
122
  ...import_node_process.default.env,
76
- WORKER_MODE: "-"
77
- }
123
+ WORKER_MODE: "-",
124
+ ...parsedArgv.app && parsedArgv.app !== "main" ? { STARTUP_SUBAPP: parsedArgv.app } : {}
125
+ },
126
+ resourceLimits: RESOURCE_LIMITS
78
127
  });
79
128
  this.workerThread = worker;
80
129
  (_b = this.logger) == null ? void 0 : _b.debug(`Worker created successfully for task ${this.record.id}`);
@@ -128,5 +177,6 @@ class CommandTaskType extends import_task_type.TaskType {
128
177
  }
129
178
  // Annotate the CommonJS export names for ESM import in node:
130
179
  0 && (module.exports = {
131
- CommandTaskType
180
+ CommandTaskType,
181
+ parseArgv
132
182
  });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { TaskId } from '../../common/types';
10
+ export type ConcurrencyMode = 'app' | 'process';
11
+ export interface ConcurrencyMonitor {
12
+ idle(): boolean;
13
+ concurrency: number;
14
+ increase(taskId: TaskId): boolean;
15
+ decrease(taskId: TaskId): void;
16
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
+ var concurrency_monitor_exports = {};
24
+ module.exports = __toCommonJS(concurrency_monitor_exports);
@@ -116,11 +116,12 @@ class TaskType {
116
116
  try {
117
117
  const result = await this.execute();
118
118
  (_c = this.logger) == null ? void 0 : _c.info(`Task ${this.record.id} completed successfully with result: ${JSON.stringify(result)}`);
119
- await this.record.update({
119
+ this.record.set({
120
120
  status: import_constants.TASK_STATUS.SUCCEEDED,
121
121
  doneAt: /* @__PURE__ */ new Date(),
122
122
  result
123
123
  });
124
+ await this.record.save();
124
125
  } catch (error) {
125
126
  if (error instanceof import_async_task_manager.CancelError) {
126
127
  (_d = this.logger) == null ? void 0 : _d.info(`Task ${this.record.id} was cancelled during execution`);
@@ -129,7 +130,7 @@ class TaskType {
129
130
  await this.record.update({
130
131
  status: import_constants.TASK_STATUS.FAILED,
131
132
  doneAt: /* @__PURE__ */ new Date(),
132
- error: error.toString()
133
+ result: { ...error, message: error.message }
133
134
  });
134
135
  (_e = this.logger) == null ? void 0 : _e.error(`Task ${this.record.id} failed with error: ${error.message}`);
135
136
  throw error;
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@nocobase/plugin-async-task-manager",
3
3
  "displayName": "Async task manager",
4
+ "displayName.ru-RU": "Менеджер асинхронных задач",
4
5
  "displayName.zh-CN": "异步任务管理器",
5
6
  "description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.",
7
+ "description.ru-RU": "Управление асинхронными задачами и мониторинг (например, импорт/экспорт данных). Поддержка отслеживания прогресса и уведомлений о задачах.",
6
8
  "description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。",
7
- "version": "2.0.0-alpha.9",
9
+ "version": "2.0.0-beta.10",
8
10
  "main": "dist/server/index.js",
9
11
  "peerDependencies": {
10
12
  "@nocobase/client": "2.x",
@@ -15,5 +17,5 @@
15
17
  "dependencies": {
16
18
  "p-queue": "^6.6.2"
17
19
  },
18
- "gitHead": "4a9acf96f21a3aa35bccbd188b942595b09da0a9"
20
+ "gitHead": "9943dc4b0fdedcac3f304714b58635ae441e2560"
19
21
  }