@adminforth/background-jobs 1.4.0 → 1.5.0
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.
- package/build.log +2 -2
- package/custom/JobInfoPopup.vue +2 -2
- package/dist/custom/JobInfoPopup.vue +2 -2
- package/dist/index.js +31 -1
- package/index.ts +35 -3
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -11,5 +11,5 @@ custom/StateToIcon.vue
|
|
|
11
11
|
custom/tsconfig.json
|
|
12
12
|
custom/utils.ts
|
|
13
13
|
|
|
14
|
-
sent 12,
|
|
15
|
-
total size is 12,
|
|
14
|
+
sent 12,535 bytes received 134 bytes 25,338.00 bytes/sec
|
|
15
|
+
total size is 12,050 speedup is 0.95
|
package/custom/JobInfoPopup.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex flex-col w-full min-w-96">
|
|
3
3
|
<div class="flex items-center mb-1">
|
|
4
|
-
<div class="flex flex-col items-start">
|
|
4
|
+
<div class="flex flex-col items-start justify-end h-12">
|
|
5
5
|
<h2 class="text-lg font-semibold dark:text-white">{{ job.name }}</h2>
|
|
6
6
|
<Tooltip>
|
|
7
7
|
<p class="text-xs text-gray-600 dark:text-gray-200 h-full">{{ t('Created:') }} {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</Tooltip>
|
|
12
12
|
</div>
|
|
13
|
-
<div class="ml-auto flex flex-col items-start">
|
|
13
|
+
<div class="ml-auto flex flex-col items-start justify-end h-12">
|
|
14
14
|
<div class="flex items-center">
|
|
15
15
|
<p class=" text-gray-800 dark:text-white h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
|
|
16
16
|
<StateToIcon :job="job" />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex flex-col w-full min-w-96">
|
|
3
3
|
<div class="flex items-center mb-1">
|
|
4
|
-
<div class="flex flex-col items-start">
|
|
4
|
+
<div class="flex flex-col items-start justify-end h-12">
|
|
5
5
|
<h2 class="text-lg font-semibold dark:text-white">{{ job.name }}</h2>
|
|
6
6
|
<Tooltip>
|
|
7
7
|
<p class="text-xs text-gray-600 dark:text-gray-200 h-full">{{ t('Created:') }} {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</Tooltip>
|
|
12
12
|
</div>
|
|
13
|
-
<div class="ml-auto flex flex-col items-start">
|
|
13
|
+
<div class="ml-auto flex flex-col items-start justify-end h-12">
|
|
14
14
|
<div class="flex items-center">
|
|
15
15
|
<p class=" text-gray-800 dark:text-white h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
|
|
16
16
|
<StateToIcon :job="job" />
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,6 @@ import pLimit from 'p-limit';
|
|
|
13
13
|
import { Level } from 'level';
|
|
14
14
|
import fs from 'fs/promises';
|
|
15
15
|
import { Mutex } from 'async-mutex';
|
|
16
|
-
const mutex = new Mutex();
|
|
17
16
|
export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
18
17
|
constructor(options) {
|
|
19
18
|
super(options, import.meta.url);
|
|
@@ -21,6 +20,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
21
20
|
this.jobCustomComponents = {};
|
|
22
21
|
this.jobParallelLimits = {};
|
|
23
22
|
this.levelDbInstances = {};
|
|
23
|
+
this.jobStateMutexes = {};
|
|
24
24
|
this.options = options;
|
|
25
25
|
this.shouldHaveSingleInstancePerWholeApp = () => true;
|
|
26
26
|
}
|
|
@@ -66,6 +66,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
66
66
|
yield jobLevelDb.close();
|
|
67
67
|
delete this.levelDbInstances[recordId];
|
|
68
68
|
}
|
|
69
|
+
// cleanup per-job mutex as well
|
|
70
|
+
delete this.jobStateMutexes[recordId];
|
|
69
71
|
//delete level db folder for the job
|
|
70
72
|
yield fs.rm(levelDbPath, {
|
|
71
73
|
recursive: true,
|
|
@@ -75,6 +77,12 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
75
77
|
}));
|
|
76
78
|
});
|
|
77
79
|
}
|
|
80
|
+
cleanupJobMutexIfTerminalStatus(jobId, status) {
|
|
81
|
+
// Keep mutex while job is active to preserve atomicity between concurrent tasks.
|
|
82
|
+
if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
|
|
83
|
+
delete this.jobStateMutexes[jobId];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
78
86
|
checkIfFieldInResource(resourceConfig, fieldName, fieldString) {
|
|
79
87
|
if (!fieldName) {
|
|
80
88
|
throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
|
|
@@ -247,6 +255,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
247
255
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
248
256
|
});
|
|
249
257
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
|
|
258
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
|
|
250
259
|
}
|
|
251
260
|
else if (failedTasks > 0) {
|
|
252
261
|
yield this.adminforth.resource(this.getResourceId()).update(jobId, {
|
|
@@ -254,6 +263,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
254
263
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
255
264
|
});
|
|
256
265
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
|
|
266
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
|
|
257
267
|
}
|
|
258
268
|
});
|
|
259
269
|
}
|
|
@@ -344,6 +354,26 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
344
354
|
return JSON.parse(state);
|
|
345
355
|
});
|
|
346
356
|
}
|
|
357
|
+
updateJobFieldsAtomicly(jobId, updateFunction) {
|
|
358
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
359
|
+
if (!jobId) {
|
|
360
|
+
throw new Error('updateJobFieldsAtomicly: jobId is required');
|
|
361
|
+
}
|
|
362
|
+
if (typeof updateFunction !== 'function') {
|
|
363
|
+
throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
|
|
364
|
+
}
|
|
365
|
+
// Ensure updates are atomic per jobId.
|
|
366
|
+
// Different jobs are not blocked by each other.
|
|
367
|
+
let mutex = this.jobStateMutexes[jobId];
|
|
368
|
+
if (!mutex) {
|
|
369
|
+
mutex = new Mutex();
|
|
370
|
+
this.jobStateMutexes[jobId] = mutex;
|
|
371
|
+
}
|
|
372
|
+
return mutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () {
|
|
373
|
+
yield updateFunction();
|
|
374
|
+
}));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
347
377
|
processAllUnfinishedJobs() {
|
|
348
378
|
return __awaiter(this, void 0, void 0, function* () {
|
|
349
379
|
const resourceId = this.getResourceId();
|
package/index.ts
CHANGED
|
@@ -5,9 +5,7 @@ import { afLogger } from "adminforth";
|
|
|
5
5
|
import pLimit from 'p-limit';
|
|
6
6
|
import { Level } from 'level';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
|
-
import {Mutex
|
|
9
|
-
|
|
10
|
-
const mutex = new Mutex();
|
|
8
|
+
import { Mutex } from 'async-mutex';
|
|
11
9
|
|
|
12
10
|
type TaskStatus = 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED';
|
|
13
11
|
type setStateFieldParams = (state: Record<string, any>) => void;
|
|
@@ -24,6 +22,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
24
22
|
private jobCustomComponents: Record<string, AdminForthComponentDeclarationFull> = {};
|
|
25
23
|
private jobParallelLimits: Record<string, number> = {};
|
|
26
24
|
private levelDbInstances: Record<string, Level> = {};
|
|
25
|
+
private jobStateMutexes: Record<string, Mutex> = {};
|
|
27
26
|
|
|
28
27
|
constructor(options: PluginOptions) {
|
|
29
28
|
super(options, import.meta.url);
|
|
@@ -73,6 +72,9 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
73
72
|
delete this.levelDbInstances[recordId];
|
|
74
73
|
}
|
|
75
74
|
|
|
75
|
+
// cleanup per-job mutex as well
|
|
76
|
+
delete this.jobStateMutexes[recordId];
|
|
77
|
+
|
|
76
78
|
//delete level db folder for the job
|
|
77
79
|
await fs.rm(levelDbPath, {
|
|
78
80
|
recursive: true,
|
|
@@ -83,6 +85,13 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
83
85
|
})
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
private cleanupJobMutexIfTerminalStatus(jobId: string, status: string) {
|
|
89
|
+
// Keep mutex while job is active to preserve atomicity between concurrent tasks.
|
|
90
|
+
if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
|
|
91
|
+
delete this.jobStateMutexes[jobId];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
private checkIfFieldInResource(resourceConfig: AdminForthResource, fieldName: string, fieldString?: string) {
|
|
87
96
|
if (!fieldName) {
|
|
88
97
|
throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
|
|
@@ -279,12 +288,14 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
279
288
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
280
289
|
})
|
|
281
290
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
|
|
291
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
|
|
282
292
|
} else if (failedTasks > 0) {
|
|
283
293
|
await this.adminforth.resource(this.getResourceId()).update(jobId, {
|
|
284
294
|
[this.options.statusField]: 'DONE_WITH_ERRORS',
|
|
285
295
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
286
296
|
})
|
|
287
297
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
|
|
298
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
301
|
|
|
@@ -372,6 +383,27 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
372
383
|
return JSON.parse(state);
|
|
373
384
|
}
|
|
374
385
|
|
|
386
|
+
public async updateJobFieldsAtomicly(jobId: string, updateFunction: () => Promise<void>) {
|
|
387
|
+
if (!jobId) {
|
|
388
|
+
throw new Error('updateJobFieldsAtomicly: jobId is required');
|
|
389
|
+
}
|
|
390
|
+
if (typeof updateFunction !== 'function') {
|
|
391
|
+
throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Ensure updates are atomic per jobId.
|
|
395
|
+
// Different jobs are not blocked by each other.
|
|
396
|
+
let mutex = this.jobStateMutexes[jobId];
|
|
397
|
+
if (!mutex) {
|
|
398
|
+
mutex = new Mutex();
|
|
399
|
+
this.jobStateMutexes[jobId] = mutex;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return mutex.runExclusive(async () => {
|
|
403
|
+
await updateFunction();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
375
407
|
private async processAllUnfinishedJobs() {
|
|
376
408
|
const resourceId = this.getResourceId();
|
|
377
409
|
const unprocessedJobs = await this.adminforth.resource(resourceId).list(Filters.EQ(this.options.statusField, 'IN_PROGRESS'));
|