@adminforth/background-jobs 1.4.1 → 1.6.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 +1 -1
- package/custom/StateToIcon.vue +19 -6
- package/dist/custom/JobInfoPopup.vue +1 -1
- package/dist/custom/StateToIcon.vue +19 -6
- package/dist/index.js +46 -3
- package/index.ts +53 -6
- package/package.json +2 -2
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
|
|
15
|
-
total size is 12,
|
|
14
|
+
sent 13,135 bytes received 134 bytes 26,538.00 bytes/sec
|
|
15
|
+
total size is 12,650 speedup is 0.95
|
package/custom/JobInfoPopup.vue
CHANGED
package/custom/StateToIcon.vue
CHANGED
|
@@ -1,40 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Tooltip v-if="job
|
|
2
|
+
<Tooltip v-if="job?.status === 'IN_PROGRESS' || status === 'IN_PROGRESS'">
|
|
3
3
|
<Spinner class="w-5 h-5 ml-2" />
|
|
4
4
|
<template #tooltip>
|
|
5
5
|
{{ t('In progress') }}
|
|
6
6
|
</template>
|
|
7
7
|
</Tooltip>
|
|
8
|
-
<Tooltip v-else-if="job
|
|
8
|
+
<Tooltip v-else-if="job?.status === 'DONE' || status === 'DONE'">
|
|
9
9
|
<IconCheckCircleOutline class="w-6 h-6 ml-2 text-green-500" />
|
|
10
10
|
<template #tooltip>
|
|
11
11
|
{{ t('Done') }}
|
|
12
12
|
</template>
|
|
13
13
|
</Tooltip>
|
|
14
|
-
<Tooltip v-else-if="job
|
|
14
|
+
<Tooltip v-else-if="job?.status === 'CANCELLED' || status === 'CANCELLED'">
|
|
15
15
|
<IconCloseCircleOutline class="w-6 h-6 ml-2 text-red-500" />
|
|
16
16
|
<template #tooltip>
|
|
17
17
|
{{ t('Cancelled') }}
|
|
18
18
|
</template>
|
|
19
19
|
</Tooltip>
|
|
20
|
-
<Tooltip v-else-if="job
|
|
20
|
+
<Tooltip v-else-if="job?.status === 'DONE_WITH_ERRORS' || status === 'DONE_WITH_ERRORS'">
|
|
21
21
|
<IconExclamationCircleOutline class="w-6 h-6 ml-2 text-yellow-500" />
|
|
22
22
|
<template #tooltip>
|
|
23
23
|
{{ t('Done with errors') }}
|
|
24
24
|
</template>
|
|
25
25
|
</Tooltip>
|
|
26
|
+
<Tooltip v-else-if="status === 'FAILED'">
|
|
27
|
+
<IconCloseCircleOutline class="w-6 h-6 ml-2 text-red-500" />
|
|
28
|
+
<template #tooltip>
|
|
29
|
+
{{ t('Failed') }}
|
|
30
|
+
</template>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
<Tooltip v-else-if="status === 'SCHEDULED'">
|
|
33
|
+
<IconClockOutline class="w-6 h-6 ml-2 text-blue-500" />
|
|
34
|
+
<template #tooltip>
|
|
35
|
+
{{ t('Scheduled') }}
|
|
36
|
+
</template>
|
|
37
|
+
</Tooltip>
|
|
26
38
|
</template>
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
<script setup lang="ts">
|
|
30
42
|
import type { IJob } from './utils';
|
|
31
|
-
import { IconCheckCircleOutline, IconCloseCircleOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
|
|
43
|
+
import { IconCheckCircleOutline, IconCloseCircleOutline, IconExclamationCircleOutline, IconClockOutline } from '@iconify-prerendered/vue-flowbite';
|
|
32
44
|
import { Spinner, Tooltip } from '@/afcl';
|
|
33
45
|
import { useI18n } from 'vue-i18n';
|
|
34
46
|
|
|
35
47
|
const { t } = useI18n();
|
|
36
48
|
|
|
37
49
|
const props = defineProps<{
|
|
38
|
-
job
|
|
50
|
+
job?: IJob;
|
|
51
|
+
status?: 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED' | 'CANCELLED' | 'DONE_WITH_ERRORS';
|
|
39
52
|
}>();
|
|
40
53
|
</script>
|
|
@@ -1,40 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Tooltip v-if="job
|
|
2
|
+
<Tooltip v-if="job?.status === 'IN_PROGRESS' || status === 'IN_PROGRESS'">
|
|
3
3
|
<Spinner class="w-5 h-5 ml-2" />
|
|
4
4
|
<template #tooltip>
|
|
5
5
|
{{ t('In progress') }}
|
|
6
6
|
</template>
|
|
7
7
|
</Tooltip>
|
|
8
|
-
<Tooltip v-else-if="job
|
|
8
|
+
<Tooltip v-else-if="job?.status === 'DONE' || status === 'DONE'">
|
|
9
9
|
<IconCheckCircleOutline class="w-6 h-6 ml-2 text-green-500" />
|
|
10
10
|
<template #tooltip>
|
|
11
11
|
{{ t('Done') }}
|
|
12
12
|
</template>
|
|
13
13
|
</Tooltip>
|
|
14
|
-
<Tooltip v-else-if="job
|
|
14
|
+
<Tooltip v-else-if="job?.status === 'CANCELLED' || status === 'CANCELLED'">
|
|
15
15
|
<IconCloseCircleOutline class="w-6 h-6 ml-2 text-red-500" />
|
|
16
16
|
<template #tooltip>
|
|
17
17
|
{{ t('Cancelled') }}
|
|
18
18
|
</template>
|
|
19
19
|
</Tooltip>
|
|
20
|
-
<Tooltip v-else-if="job
|
|
20
|
+
<Tooltip v-else-if="job?.status === 'DONE_WITH_ERRORS' || status === 'DONE_WITH_ERRORS'">
|
|
21
21
|
<IconExclamationCircleOutline class="w-6 h-6 ml-2 text-yellow-500" />
|
|
22
22
|
<template #tooltip>
|
|
23
23
|
{{ t('Done with errors') }}
|
|
24
24
|
</template>
|
|
25
25
|
</Tooltip>
|
|
26
|
+
<Tooltip v-else-if="status === 'FAILED'">
|
|
27
|
+
<IconCloseCircleOutline class="w-6 h-6 ml-2 text-red-500" />
|
|
28
|
+
<template #tooltip>
|
|
29
|
+
{{ t('Failed') }}
|
|
30
|
+
</template>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
<Tooltip v-else-if="status === 'SCHEDULED'">
|
|
33
|
+
<IconClockOutline class="w-6 h-6 ml-2 text-blue-500" />
|
|
34
|
+
<template #tooltip>
|
|
35
|
+
{{ t('Scheduled') }}
|
|
36
|
+
</template>
|
|
37
|
+
</Tooltip>
|
|
26
38
|
</template>
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
<script setup lang="ts">
|
|
30
42
|
import type { IJob } from './utils';
|
|
31
|
-
import { IconCheckCircleOutline, IconCloseCircleOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
|
|
43
|
+
import { IconCheckCircleOutline, IconCloseCircleOutline, IconExclamationCircleOutline, IconClockOutline } from '@iconify-prerendered/vue-flowbite';
|
|
32
44
|
import { Spinner, Tooltip } from '@/afcl';
|
|
33
45
|
import { useI18n } from 'vue-i18n';
|
|
34
46
|
|
|
35
47
|
const { t } = useI18n();
|
|
36
48
|
|
|
37
49
|
const props = defineProps<{
|
|
38
|
-
job
|
|
50
|
+
job?: IJob;
|
|
51
|
+
status?: 'SCHEDULED' | 'IN_PROGRESS' | 'DONE' | 'FAILED' | 'CANCELLED' | 'DONE_WITH_ERRORS';
|
|
39
52
|
}>();
|
|
40
53
|
</script>
|
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
|
}
|
|
@@ -39,7 +39,6 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
39
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
40
40
|
var _a, _b;
|
|
41
41
|
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
42
|
-
console.log('Modifying resource config for Background Jobs Plugin');
|
|
43
42
|
if (!((_b = (_a = adminforth.config.customization) === null || _a === void 0 ? void 0 : _a.globalInjections) === null || _b === void 0 ? void 0 : _b.header)) {
|
|
44
43
|
adminforth.config.customization.globalInjections.header = [];
|
|
45
44
|
}
|
|
@@ -49,6 +48,12 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
49
48
|
pluginInstanceId: this.pluginInstanceId,
|
|
50
49
|
}
|
|
51
50
|
});
|
|
51
|
+
if (!this.adminforth.config.componentsToExplicitRegister) {
|
|
52
|
+
this.adminforth.config.componentsToExplicitRegister = [];
|
|
53
|
+
}
|
|
54
|
+
this.adminforth.config.componentsToExplicitRegister.push({
|
|
55
|
+
file: this.componentPath('StateToIcon.vue')
|
|
56
|
+
});
|
|
52
57
|
if (!this.resourceConfig.hooks) {
|
|
53
58
|
this.resourceConfig.hooks = {};
|
|
54
59
|
}
|
|
@@ -66,6 +71,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
66
71
|
yield jobLevelDb.close();
|
|
67
72
|
delete this.levelDbInstances[recordId];
|
|
68
73
|
}
|
|
74
|
+
// cleanup per-job mutex as well
|
|
75
|
+
delete this.jobStateMutexes[recordId];
|
|
69
76
|
//delete level db folder for the job
|
|
70
77
|
yield fs.rm(levelDbPath, {
|
|
71
78
|
recursive: true,
|
|
@@ -75,6 +82,12 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
75
82
|
}));
|
|
76
83
|
});
|
|
77
84
|
}
|
|
85
|
+
cleanupJobMutexIfTerminalStatus(jobId, status) {
|
|
86
|
+
// Keep mutex while job is active to preserve atomicity between concurrent tasks.
|
|
87
|
+
if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
|
|
88
|
+
delete this.jobStateMutexes[jobId];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
78
91
|
checkIfFieldInResource(resourceConfig, fieldName, fieldString) {
|
|
79
92
|
if (!fieldName) {
|
|
80
93
|
throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
|
|
@@ -126,6 +139,12 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
126
139
|
return Promise.resolve(null);
|
|
127
140
|
});
|
|
128
141
|
}
|
|
142
|
+
getTotalTasksInLevelDb(levelDb) {
|
|
143
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
144
|
+
const count = yield levelDb.get('_meta:count');
|
|
145
|
+
return count ? parseInt(count, 10) : 0;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
129
148
|
registerTaskHandler({ jobHandlerName, handler, parallelLimit = 3, }) {
|
|
130
149
|
//register the handler in a map with jobHandlerName as key and handler as value
|
|
131
150
|
this.taskHandlers[jobHandlerName] = handler;
|
|
@@ -171,6 +190,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
171
190
|
//create a level db instance for the job with name as jobId
|
|
172
191
|
const jobLevelDb = new Level(`${this.options.levelDbPath || './background-jobs-dbs/'}job_${jobId}`, { valueEncoding: 'json' });
|
|
173
192
|
this.levelDbInstances[jobId] = jobLevelDb;
|
|
193
|
+
yield jobLevelDb.put('_meta:count', `${tasks.length}`);
|
|
174
194
|
const limit2 = pLimit(parrallelLimit);
|
|
175
195
|
const createTaskRecordsPromises = tasks.map((task, index) => {
|
|
176
196
|
return limit2(() => this.createLevelDbTaskRecord(jobLevelDb, index.toString(), task.state));
|
|
@@ -247,6 +267,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
247
267
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
248
268
|
});
|
|
249
269
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
|
|
270
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
|
|
250
271
|
}
|
|
251
272
|
else if (failedTasks > 0) {
|
|
252
273
|
yield this.adminforth.resource(this.getResourceId()).update(jobId, {
|
|
@@ -254,6 +275,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
254
275
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
255
276
|
});
|
|
256
277
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
|
|
278
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
|
|
257
279
|
}
|
|
258
280
|
});
|
|
259
281
|
}
|
|
@@ -344,6 +366,26 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
344
366
|
return JSON.parse(state);
|
|
345
367
|
});
|
|
346
368
|
}
|
|
369
|
+
updateJobFieldsAtomicly(jobId, updateFunction) {
|
|
370
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
371
|
+
if (!jobId) {
|
|
372
|
+
throw new Error('updateJobFieldsAtomicly: jobId is required');
|
|
373
|
+
}
|
|
374
|
+
if (typeof updateFunction !== 'function') {
|
|
375
|
+
throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
|
|
376
|
+
}
|
|
377
|
+
// Ensure updates are atomic per jobId.
|
|
378
|
+
// Different jobs are not blocked by each other.
|
|
379
|
+
let mutex = this.jobStateMutexes[jobId];
|
|
380
|
+
if (!mutex) {
|
|
381
|
+
mutex = new Mutex();
|
|
382
|
+
this.jobStateMutexes[jobId] = mutex;
|
|
383
|
+
}
|
|
384
|
+
return mutex.runExclusive(() => __awaiter(this, void 0, void 0, function* () {
|
|
385
|
+
yield updateFunction();
|
|
386
|
+
}));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
347
389
|
processAllUnfinishedJobs() {
|
|
348
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
349
391
|
const resourceId = this.getResourceId();
|
|
@@ -465,7 +507,8 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
465
507
|
tasks.push(parsedTaskData);
|
|
466
508
|
taskIndex++;
|
|
467
509
|
}
|
|
468
|
-
|
|
510
|
+
const total = yield this.getTotalTasksInLevelDb(jobLevelDb);
|
|
511
|
+
return { ok: true, data: { tasks, total } };
|
|
469
512
|
})
|
|
470
513
|
});
|
|
471
514
|
}
|
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);
|
|
@@ -42,7 +41,6 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
42
41
|
|
|
43
42
|
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
44
43
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
45
|
-
console.log('Modifying resource config for Background Jobs Plugin');
|
|
46
44
|
if (!adminforth.config.customization?.globalInjections?.header) {
|
|
47
45
|
adminforth.config.customization.globalInjections.header = [];
|
|
48
46
|
}
|
|
@@ -53,6 +51,15 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
53
51
|
}
|
|
54
52
|
});
|
|
55
53
|
|
|
54
|
+
if (!this.adminforth.config.componentsToExplicitRegister) {
|
|
55
|
+
this.adminforth.config.componentsToExplicitRegister = [];
|
|
56
|
+
}
|
|
57
|
+
this.adminforth.config.componentsToExplicitRegister.push(
|
|
58
|
+
{
|
|
59
|
+
file: this.componentPath('StateToIcon.vue')
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
56
63
|
if (!this.resourceConfig.hooks) {
|
|
57
64
|
this.resourceConfig.hooks = {};
|
|
58
65
|
}
|
|
@@ -73,6 +80,9 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
73
80
|
delete this.levelDbInstances[recordId];
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
// cleanup per-job mutex as well
|
|
84
|
+
delete this.jobStateMutexes[recordId];
|
|
85
|
+
|
|
76
86
|
//delete level db folder for the job
|
|
77
87
|
await fs.rm(levelDbPath, {
|
|
78
88
|
recursive: true,
|
|
@@ -83,6 +93,13 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
83
93
|
})
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
private cleanupJobMutexIfTerminalStatus(jobId: string, status: string) {
|
|
97
|
+
// Keep mutex while job is active to preserve atomicity between concurrent tasks.
|
|
98
|
+
if (status === 'DONE' || status === 'DONE_WITH_ERRORS' || status === 'CANCELLED') {
|
|
99
|
+
delete this.jobStateMutexes[jobId];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
private checkIfFieldInResource(resourceConfig: AdminForthResource, fieldName: string, fieldString?: string) {
|
|
87
104
|
if (!fieldName) {
|
|
88
105
|
throw new Error(`Field name for ${fieldString} is not provided. Please check your plugin options.`);
|
|
@@ -129,6 +146,11 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
129
146
|
}
|
|
130
147
|
return Promise.resolve(null);
|
|
131
148
|
}
|
|
149
|
+
|
|
150
|
+
private async getTotalTasksInLevelDb(levelDb: Level): Promise<number> {
|
|
151
|
+
const count = await levelDb.get('_meta:count');
|
|
152
|
+
return count ? parseInt(count, 10) : 0;
|
|
153
|
+
}
|
|
132
154
|
|
|
133
155
|
public registerTaskHandler({ jobHandlerName, handler, parallelLimit = 3,
|
|
134
156
|
}:{jobHandlerName: string, handler: taskHandlerType, parallelLimit?: number}) {
|
|
@@ -188,7 +210,7 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
188
210
|
//create a level db instance for the job with name as jobId
|
|
189
211
|
const jobLevelDb = new Level(`${this.options.levelDbPath || './background-jobs-dbs/'}job_${jobId}`, { valueEncoding: 'json' });
|
|
190
212
|
this.levelDbInstances[jobId] = jobLevelDb;
|
|
191
|
-
|
|
213
|
+
await jobLevelDb.put('_meta:count', `${tasks.length}`);
|
|
192
214
|
const limit2 = pLimit(parrallelLimit);
|
|
193
215
|
const createTaskRecordsPromises = tasks.map((task, index) => {
|
|
194
216
|
return limit2(() => this.createLevelDbTaskRecord(jobLevelDb, index.toString(), task.state));
|
|
@@ -279,12 +301,14 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
279
301
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
280
302
|
})
|
|
281
303
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE', finishedAt: (new Date()).toISOString() });
|
|
304
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE');
|
|
282
305
|
} else if (failedTasks > 0) {
|
|
283
306
|
await this.adminforth.resource(this.getResourceId()).update(jobId, {
|
|
284
307
|
[this.options.statusField]: 'DONE_WITH_ERRORS',
|
|
285
308
|
[this.options.finishedAtField]: (new Date()).toISOString(),
|
|
286
309
|
})
|
|
287
310
|
this.adminforth.websocket.publish('/background-jobs', { jobId, status: 'DONE_WITH_ERRORS' });
|
|
311
|
+
this.cleanupJobMutexIfTerminalStatus(jobId, 'DONE_WITH_ERRORS');
|
|
288
312
|
}
|
|
289
313
|
}
|
|
290
314
|
|
|
@@ -372,6 +396,27 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
372
396
|
return JSON.parse(state);
|
|
373
397
|
}
|
|
374
398
|
|
|
399
|
+
public async updateJobFieldsAtomicly(jobId: string, updateFunction: () => Promise<void>) {
|
|
400
|
+
if (!jobId) {
|
|
401
|
+
throw new Error('updateJobFieldsAtomicly: jobId is required');
|
|
402
|
+
}
|
|
403
|
+
if (typeof updateFunction !== 'function') {
|
|
404
|
+
throw new Error('updateJobFieldsAtomicly: updateFunction must be a function');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Ensure updates are atomic per jobId.
|
|
408
|
+
// Different jobs are not blocked by each other.
|
|
409
|
+
let mutex = this.jobStateMutexes[jobId];
|
|
410
|
+
if (!mutex) {
|
|
411
|
+
mutex = new Mutex();
|
|
412
|
+
this.jobStateMutexes[jobId] = mutex;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return mutex.runExclusive(async () => {
|
|
416
|
+
await updateFunction();
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
375
420
|
private async processAllUnfinishedJobs() {
|
|
376
421
|
const resourceId = this.getResourceId();
|
|
377
422
|
const unprocessedJobs = await this.adminforth.resource(resourceId).list(Filters.EQ(this.options.statusField, 'IN_PROGRESS'));
|
|
@@ -494,7 +539,9 @@ export default class BackgroundJobsPlugin extends AdminForthPlugin {
|
|
|
494
539
|
tasks.push(parsedTaskData);
|
|
495
540
|
taskIndex++;
|
|
496
541
|
}
|
|
497
|
-
|
|
542
|
+
|
|
543
|
+
const total = await this.getTotalTasksInLevelDb(jobLevelDb);
|
|
544
|
+
return { ok: true, data: { tasks, total } };
|
|
498
545
|
}
|
|
499
546
|
});
|
|
500
547
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/background-jobs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@vueuse/core": "^14.2.1",
|
|
23
|
-
"adminforth": "
|
|
23
|
+
"adminforth": "^2.24.0",
|
|
24
24
|
"async-mutex": "^0.5.0",
|
|
25
25
|
"level": "^10.0.0",
|
|
26
26
|
"p-limit": "^7.3.0"
|