@unchainedshop/core-worker 1.1.3
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/.npm/package/README +7 -0
- package/.npm/package/npm-shrinkwrap.json +257 -0
- package/.versions +53 -0
- package/README.md +9 -0
- package/lib/db/WorkQueueCollection.d.ts +3 -0
- package/lib/db/WorkQueueCollection.js +32 -0
- package/lib/db/WorkQueueCollection.js.map +1 -0
- package/lib/db/WorkQueueSchema.d.ts +1 -0
- package/lib/db/WorkQueueSchema.js +20 -0
- package/lib/db/WorkQueueSchema.js.map +1 -0
- package/lib/director/WorkStatus.d.ts +7 -0
- package/lib/director/WorkStatus.js +9 -0
- package/lib/director/WorkStatus.js.map +1 -0
- package/lib/director/WorkerAdapter.d.ts +2 -0
- package/lib/director/WorkerAdapter.js +10 -0
- package/lib/director/WorkerAdapter.js.map +1 -0
- package/lib/director/WorkerDirector.d.ts +3 -0
- package/lib/director/WorkerDirector.js +46 -0
- package/lib/director/WorkerDirector.js.map +1 -0
- package/lib/director/WorkerEventTypes.d.ts +8 -0
- package/lib/director/WorkerEventTypes.js +13 -0
- package/lib/director/WorkerEventTypes.js.map +1 -0
- package/lib/module/configureWorkerModule.d.ts +3 -0
- package/lib/module/configureWorkerModule.js +272 -0
- package/lib/module/configureWorkerModule.js.map +1 -0
- package/lib/schedulers/FailedRescheduler.d.ts +2 -0
- package/lib/schedulers/FailedRescheduler.js +38 -0
- package/lib/schedulers/FailedRescheduler.js.map +1 -0
- package/lib/worker-index.d.ts +9 -0
- package/lib/worker-index.js +10 -0
- package/lib/worker-index.js.map +1 -0
- package/lib/workers/BaseWorker.d.ts +7 -0
- package/lib/workers/BaseWorker.js +85 -0
- package/lib/workers/BaseWorker.js.map +1 -0
- package/lib/workers/EventListenerWorker.d.ts +4 -0
- package/lib/workers/EventListenerWorker.js +44 -0
- package/lib/workers/EventListenerWorker.js.map +1 -0
- package/lib/workers/IntervalWorker.d.ts +9 -0
- package/lib/workers/IntervalWorker.js +38 -0
- package/lib/workers/IntervalWorker.js.map +1 -0
- package/package.js +33 -0
- package/package.json +43 -0
- package/src/db/WorkQueueCollection.ts +37 -0
- package/src/db/WorkQueueSchema.js +23 -0
- package/src/director/WorkStatus.ts +7 -0
- package/src/director/WorkerAdapter.ts +12 -0
- package/src/director/WorkerDirector.ts +73 -0
- package/src/director/WorkerEventTypes.ts +11 -0
- package/src/module/configureWorkerModule.ts +397 -0
- package/src/schedulers/FailedRescheduler.ts +53 -0
- package/src/worker-index.ts +12 -0
- package/src/workers/BaseWorker.ts +116 -0
- package/src/workers/EventListenerWorker.ts +55 -0
- package/src/workers/IntervalWorker.ts +57 -0
- package/tests/worker-index.test.ts +32 -0
- package/tsconfig.build.json +26 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unchainedshop/core-worker",
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"main": "lib/worker-index.js",
|
|
5
|
+
"types": "lib/worker-index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rm -rf lib",
|
|
9
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
10
|
+
"watch": "tsc -p tsconfig.build.json --watch",
|
|
11
|
+
"link:core": "npm link @unchainedshop/types",
|
|
12
|
+
"test": "cross-env METEOR_PACKAGE_DIRS=../ TEST_CLIENT=0 TEST_WATCH=1 meteor test-packages ./ --driver-package meteortesting:mocha --port 4200"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/unchainedshop/unchained.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"unchained",
|
|
20
|
+
"ecommerce",
|
|
21
|
+
"core"
|
|
22
|
+
],
|
|
23
|
+
"author": "Joël Meiller",
|
|
24
|
+
"license": "EUPL-1.2",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/unchainedshop/unchained/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"later": "1.2.0",
|
|
31
|
+
"simpl-schema": "1.12.0",
|
|
32
|
+
"@unchainedshop/logger": "latest",
|
|
33
|
+
"@unchainedshop/utils": "latest"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/chai": "^4.3.1",
|
|
37
|
+
"@types/mocha": "^9.1.1",
|
|
38
|
+
"@unchainedshop/types": "latest",
|
|
39
|
+
"chai": "^4.3.6",
|
|
40
|
+
"cross-env": "^7.0.3",
|
|
41
|
+
"typescript": "^4.7.4"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Db } from '@unchainedshop/types/common';
|
|
2
|
+
import { Work } from '@unchainedshop/types/worker';
|
|
3
|
+
import { buildDbIndexes } from '@unchainedshop/utils';
|
|
4
|
+
|
|
5
|
+
const ONE_DAY_IN_SECONDS = 86400;
|
|
6
|
+
|
|
7
|
+
export const WorkQueueCollection = async (db: Db) => {
|
|
8
|
+
const WorkQueue = db.collection<Work>('work_queue');
|
|
9
|
+
|
|
10
|
+
await buildDbIndexes<Work>(WorkQueue, [
|
|
11
|
+
{
|
|
12
|
+
index: { created: -1 },
|
|
13
|
+
options: { expireAfterSeconds: 30 * ONE_DAY_IN_SECONDS },
|
|
14
|
+
},
|
|
15
|
+
{ index: { started: -1 } },
|
|
16
|
+
{ index: { finished: 1 } },
|
|
17
|
+
{ index: { scheduled: 1 } },
|
|
18
|
+
{ index: { priority: -1 } },
|
|
19
|
+
{ index: { type: 1 } },
|
|
20
|
+
{ index: { originalWorkId: 1 } },
|
|
21
|
+
{
|
|
22
|
+
index: { originalWorkId: 'text', _id: 'text', worker: 'text', input: 'text', type: 'text' },
|
|
23
|
+
options: {
|
|
24
|
+
weights: {
|
|
25
|
+
_id: 8,
|
|
26
|
+
originalWorkId: 6,
|
|
27
|
+
type: 5,
|
|
28
|
+
worker: 4,
|
|
29
|
+
input: 2,
|
|
30
|
+
},
|
|
31
|
+
name: 'workqueue_fulltext_search',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return WorkQueue;
|
|
37
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schemas } from '@unchainedshop/utils';
|
|
2
|
+
import SimpleSchema from 'simpl-schema';
|
|
3
|
+
|
|
4
|
+
export const WorkQueueSchema = new SimpleSchema(
|
|
5
|
+
{
|
|
6
|
+
finished: Date,
|
|
7
|
+
originalWorkId: { type: String },
|
|
8
|
+
priority: { type: Number, required: true },
|
|
9
|
+
retries: { type: Number, required: true },
|
|
10
|
+
scheduled: { type: Date, required: true },
|
|
11
|
+
started: Date,
|
|
12
|
+
success: Boolean,
|
|
13
|
+
timeout: Number,
|
|
14
|
+
type: { type: String, required: true },
|
|
15
|
+
worker: String,
|
|
16
|
+
// Worker data
|
|
17
|
+
input: { type: Object, blackbox: true },
|
|
18
|
+
result: { type: Object, blackbox: true },
|
|
19
|
+
error: { type: Object, blackbox: true },
|
|
20
|
+
...Schemas.timestampFields,
|
|
21
|
+
},
|
|
22
|
+
{ requiredByDefault: false },
|
|
23
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IWorkerAdapter } from '@unchainedshop/types/worker';
|
|
2
|
+
import { log, LogLevel } from '@unchainedshop/logger';
|
|
3
|
+
|
|
4
|
+
export const WorkerAdapter: Omit<IWorkerAdapter<any, void>, 'key' | 'label' | 'type' | 'version'> = {
|
|
5
|
+
async doWork() {
|
|
6
|
+
return { success: false, result: null };
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
log(message: string, { level = LogLevel.Debug, ...options } = {}) {
|
|
10
|
+
return log(message, { level, ...options });
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Context } from '@unchainedshop/types/api';
|
|
2
|
+
import {
|
|
3
|
+
IWorkerAdapter,
|
|
4
|
+
IWorkerDirector,
|
|
5
|
+
WorkScheduleConfigureation,
|
|
6
|
+
} from '@unchainedshop/types/worker';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { log, LogLevel } from '@unchainedshop/logger';
|
|
9
|
+
import { BaseDirector } from '@unchainedshop/utils';
|
|
10
|
+
import { WorkerEventTypes } from './WorkerEventTypes';
|
|
11
|
+
|
|
12
|
+
export const DIRECTOR_MARKED_FAILED_ERROR = 'DIRECTOR_MARKED_FAILED';
|
|
13
|
+
|
|
14
|
+
const AutoScheduleMap = new Map<string, WorkScheduleConfigureation>();
|
|
15
|
+
|
|
16
|
+
const baseDirector = BaseDirector<IWorkerAdapter<any, any>>('WorkerDirector', {
|
|
17
|
+
adapterKeyField: 'type',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const WorkerDirector: IWorkerDirector = {
|
|
21
|
+
...baseDirector,
|
|
22
|
+
|
|
23
|
+
events: new EventEmitter(),
|
|
24
|
+
|
|
25
|
+
getActivePluginTypes: () => {
|
|
26
|
+
return baseDirector.getAdapters().map((adapter) => adapter.type);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
registerAdapter: (Adapter) => {
|
|
30
|
+
if (baseDirector.getAdapter(Adapter.type))
|
|
31
|
+
throw new Error(
|
|
32
|
+
`WorkderDirector: There is already a adapter registered with type: ${Adapter.type}`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
baseDirector.registerAdapter(Adapter);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
configureAutoscheduling: (adapter, workQueue) => {
|
|
39
|
+
const { schedule } = workQueue;
|
|
40
|
+
AutoScheduleMap.set(adapter.type, workQueue);
|
|
41
|
+
log(
|
|
42
|
+
`WorkderDirector -> Configured ${adapter.type} ${adapter.key}@${adapter.version} (${adapter.label}) for Autorun at ${schedule}`,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
getAutoSchedules: () => Array.from(AutoScheduleMap),
|
|
46
|
+
|
|
47
|
+
doWork: async ({ type, input, _id: workId }, requestContext: Context) => {
|
|
48
|
+
const adapter = baseDirector.getAdapter(type);
|
|
49
|
+
|
|
50
|
+
if (!adapter) {
|
|
51
|
+
log(`WorkderDirector: No registered adapter for type: ${type}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const output = await adapter.doWork(input, requestContext, workId).catch((error) => {
|
|
55
|
+
// DO not use this as flow control. The adapter should catch expected errors and return status: FAILED
|
|
56
|
+
log('DO not use this as flow control.', { level: LogLevel.Verbose });
|
|
57
|
+
|
|
58
|
+
log(`WorkderDirector -> Error doing work ${type}: ${error.message}`);
|
|
59
|
+
|
|
60
|
+
const errorOutput = { error, success: false };
|
|
61
|
+
|
|
62
|
+
WorkerDirector.events.emit(WorkerEventTypes.DONE, {
|
|
63
|
+
output: errorOutput,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return errorOutput;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
WorkerDirector.events.emit(WorkerEventTypes.DONE, { output });
|
|
70
|
+
|
|
71
|
+
return output;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export enum WorkerEventTypes {
|
|
2
|
+
ADDED = 'added',
|
|
3
|
+
ALLOCATED = 'allocated',
|
|
4
|
+
DONE = 'done',
|
|
5
|
+
FINISHED = 'finished',
|
|
6
|
+
DELETED = 'deleted',
|
|
7
|
+
RESCHEDULED = 'rescheduled',
|
|
8
|
+
}
|
|
9
|
+
// The difference between `done` and `finished` is, that work is `done` after
|
|
10
|
+
// it was computed (no DB write, could be external) and `finished` after
|
|
11
|
+
// the changes are written to the DB
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModifyResult,
|
|
3
|
+
ModuleInput,
|
|
4
|
+
ModuleMutations,
|
|
5
|
+
Projection,
|
|
6
|
+
Query,
|
|
7
|
+
} from '@unchainedshop/types/common';
|
|
8
|
+
import { Work, WorkerModule } from '@unchainedshop/types/worker';
|
|
9
|
+
import { log, LogLevel } from '@unchainedshop/logger';
|
|
10
|
+
import { generateDbFilterById, generateDbMutations, buildSortOptions } from '@unchainedshop/utils';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import { WorkQueueCollection } from '../db/WorkQueueCollection';
|
|
13
|
+
import { WorkQueueSchema } from '../db/WorkQueueSchema';
|
|
14
|
+
import { DIRECTOR_MARKED_FAILED_ERROR, WorkerDirector } from '../director/WorkerDirector';
|
|
15
|
+
import { WorkerEventTypes } from '../director/WorkerEventTypes';
|
|
16
|
+
import { WorkStatus } from '../director/WorkStatus';
|
|
17
|
+
|
|
18
|
+
const { UNCHAINED_WORKER_ID = os.hostname() } = process.env;
|
|
19
|
+
|
|
20
|
+
const buildQuerySelector = ({
|
|
21
|
+
created,
|
|
22
|
+
scheduled,
|
|
23
|
+
types,
|
|
24
|
+
status,
|
|
25
|
+
workId,
|
|
26
|
+
queryString,
|
|
27
|
+
...rest
|
|
28
|
+
}: Query & {
|
|
29
|
+
created?: { end?: Date; start?: Date };
|
|
30
|
+
scheduled?: { end?: Date; start?: Date };
|
|
31
|
+
status?: Array<WorkStatus>;
|
|
32
|
+
workId?: string;
|
|
33
|
+
queryString?: string;
|
|
34
|
+
types?: Array<string>;
|
|
35
|
+
}) => {
|
|
36
|
+
const filterMap = {
|
|
37
|
+
[WorkStatus.DELETED]: { deleted: { $exists: true } },
|
|
38
|
+
[WorkStatus.NEW]: {
|
|
39
|
+
started: { $exists: false },
|
|
40
|
+
deleted: { $exists: false },
|
|
41
|
+
},
|
|
42
|
+
[WorkStatus.ALLOCATED]: {
|
|
43
|
+
started: { $exists: true },
|
|
44
|
+
finished: { $exists: false },
|
|
45
|
+
deleted: { $exists: false },
|
|
46
|
+
},
|
|
47
|
+
[WorkStatus.SUCCESS]: {
|
|
48
|
+
finished: { $exists: true },
|
|
49
|
+
success: true,
|
|
50
|
+
deleted: { $exists: false },
|
|
51
|
+
},
|
|
52
|
+
[WorkStatus.FAILED]: {
|
|
53
|
+
finished: { $exists: true },
|
|
54
|
+
success: false,
|
|
55
|
+
deleted: { $exists: false },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const statusQuery = {
|
|
59
|
+
$or: Object.entries(filterMap).reduce(
|
|
60
|
+
(acc, [key, filter]) => (status?.includes(key as WorkStatus) ? [...acc, filter] : acc),
|
|
61
|
+
[],
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let query: Query = statusQuery.$or.length > 0 ? statusQuery : { deleted: { $exists: false } };
|
|
66
|
+
|
|
67
|
+
if (created) {
|
|
68
|
+
query.created = created?.end
|
|
69
|
+
? { $gte: created.start || new Date(0), $lte: created.end }
|
|
70
|
+
: { $gte: created.start || new Date(0) };
|
|
71
|
+
}
|
|
72
|
+
if (scheduled) {
|
|
73
|
+
query.scheduled = scheduled?.end
|
|
74
|
+
? { $gte: scheduled.start || new Date(0), $lte: scheduled.end }
|
|
75
|
+
: { $gte: scheduled.start || new Date(0) };
|
|
76
|
+
}
|
|
77
|
+
if (types) {
|
|
78
|
+
query.type = { $in: types };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (workId) {
|
|
82
|
+
query = generateDbFilterById(workId, query);
|
|
83
|
+
}
|
|
84
|
+
if (queryString) query.$text = { $search: queryString };
|
|
85
|
+
|
|
86
|
+
return { ...query, ...rest };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const defaultSort: Array<{ key: string; value: 'DESC' | 'ASC' }> = [
|
|
90
|
+
{ key: 'started', value: 'DESC' },
|
|
91
|
+
{ key: 'priority', value: 'DESC' },
|
|
92
|
+
{ key: 'originalWorkId', value: 'ASC' },
|
|
93
|
+
{ key: 'created', value: 'ASC' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export const configureWorkerModule = async ({
|
|
97
|
+
db,
|
|
98
|
+
}: ModuleInput<Record<string, never>>): Promise<WorkerModule> => {
|
|
99
|
+
const WorkQueue = await WorkQueueCollection(db);
|
|
100
|
+
|
|
101
|
+
const mutations = generateDbMutations<Work>(WorkQueue, WorkQueueSchema) as ModuleMutations<Work>;
|
|
102
|
+
|
|
103
|
+
const finishWork: WorkerModule['finishWork'] = async (
|
|
104
|
+
workId,
|
|
105
|
+
{
|
|
106
|
+
error,
|
|
107
|
+
finished = new Date(),
|
|
108
|
+
result,
|
|
109
|
+
started = new Date(),
|
|
110
|
+
success,
|
|
111
|
+
worker = UNCHAINED_WORKER_ID,
|
|
112
|
+
},
|
|
113
|
+
userId,
|
|
114
|
+
) => {
|
|
115
|
+
const workBeforeUpdate = await WorkQueue.findOne(
|
|
116
|
+
buildQuerySelector({ workId, status: [WorkStatus.ALLOCATED] }),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!workBeforeUpdate) return null;
|
|
120
|
+
|
|
121
|
+
await mutations.update(
|
|
122
|
+
workId,
|
|
123
|
+
{
|
|
124
|
+
$set: {
|
|
125
|
+
finished,
|
|
126
|
+
success,
|
|
127
|
+
error,
|
|
128
|
+
result,
|
|
129
|
+
...(!workBeforeUpdate.started ? { started } : {}),
|
|
130
|
+
worker,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
userId,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const work = await WorkQueue.findOne(generateDbFilterById(workId), {});
|
|
137
|
+
|
|
138
|
+
log(`Finished work ${workId}`, {
|
|
139
|
+
level: LogLevel.Verbose,
|
|
140
|
+
work,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
WorkerDirector.events.emit(WorkerEventTypes.FINISHED, { work, userId });
|
|
144
|
+
|
|
145
|
+
return work;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
// Queries
|
|
150
|
+
activeWorkTypes: async () => {
|
|
151
|
+
const typeList = await WorkQueue.aggregate([{ $group: { _id: '$type' } }]).toArray();
|
|
152
|
+
return typeList
|
|
153
|
+
.map((t) => t._id as string)
|
|
154
|
+
.filter((type) => {
|
|
155
|
+
return WorkerDirector.getActivePluginTypes().includes(type);
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
findWork: async ({ workId, originalWorkId }) =>
|
|
160
|
+
WorkQueue.findOne(workId ? generateDbFilterById(workId) : { originalWorkId }, {}),
|
|
161
|
+
|
|
162
|
+
findWorkQueue: async ({ limit, skip, sort, ...selectorOptions }) => {
|
|
163
|
+
const selector = buildQuerySelector(selectorOptions);
|
|
164
|
+
const workQueues = WorkQueue.find(selector, {
|
|
165
|
+
skip,
|
|
166
|
+
limit,
|
|
167
|
+
sort: buildSortOptions(sort),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return workQueues.toArray();
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
count: async (query) => {
|
|
174
|
+
return WorkQueue.countDocuments(buildQuerySelector(query));
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
workExists: async ({ workId, originalWorkId }) => {
|
|
178
|
+
const queueCount = await WorkQueue.countDocuments(
|
|
179
|
+
workId ? generateDbFilterById(workId) : { originalWorkId },
|
|
180
|
+
{ limit: 1 },
|
|
181
|
+
);
|
|
182
|
+
return !!queueCount;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Transformations
|
|
186
|
+
|
|
187
|
+
type: (work) => {
|
|
188
|
+
if (WorkerDirector.getActivePluginTypes().includes(work.type)) {
|
|
189
|
+
return work.type;
|
|
190
|
+
}
|
|
191
|
+
return 'UNKNOWN';
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
status: (work) => {
|
|
195
|
+
if (work.deleted) {
|
|
196
|
+
return WorkStatus.DELETED;
|
|
197
|
+
}
|
|
198
|
+
if (!work.started && !work.finished) {
|
|
199
|
+
return WorkStatus.NEW;
|
|
200
|
+
}
|
|
201
|
+
if (work.started && !work.finished) {
|
|
202
|
+
return WorkStatus.ALLOCATED;
|
|
203
|
+
}
|
|
204
|
+
if (work.started && work.finished && work.success) {
|
|
205
|
+
return WorkStatus.SUCCESS;
|
|
206
|
+
}
|
|
207
|
+
if (work.started && work.finished && !work.success) {
|
|
208
|
+
return WorkStatus.FAILED;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
log('Unexpected work status', { level: LogLevel.Warning });
|
|
212
|
+
|
|
213
|
+
throw new Error('Unexpected work status');
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Mutations
|
|
217
|
+
addWork: async ({ type, input, priority = 0, scheduled, originalWorkId, retries = 20 }, userId) => {
|
|
218
|
+
if (!WorkerDirector.getAdapter(type)) {
|
|
219
|
+
throw new Error(`No plugin registered for type ${type}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const created = new Date();
|
|
223
|
+
const workId = await mutations.create(
|
|
224
|
+
{
|
|
225
|
+
type,
|
|
226
|
+
input,
|
|
227
|
+
priority,
|
|
228
|
+
scheduled: scheduled || created,
|
|
229
|
+
originalWorkId,
|
|
230
|
+
retries,
|
|
231
|
+
created,
|
|
232
|
+
},
|
|
233
|
+
userId,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
log(`WorkerDirector -> Work added ${workId} (${type} / ${scheduled || created} / ${retries})`, {
|
|
237
|
+
userId,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const work = await WorkQueue.findOne(generateDbFilterById(workId), {});
|
|
241
|
+
|
|
242
|
+
WorkerDirector.events.emit(WorkerEventTypes.ADDED, { work, userId });
|
|
243
|
+
|
|
244
|
+
return work;
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
rescheduleWork: async (currentWork, scheduled, { userId }) => {
|
|
248
|
+
await mutations.update(
|
|
249
|
+
currentWork._id,
|
|
250
|
+
{
|
|
251
|
+
$set: {
|
|
252
|
+
scheduled,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
userId,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const work = await WorkQueue.findOne(generateDbFilterById(currentWork._id), {});
|
|
259
|
+
|
|
260
|
+
WorkerDirector.events.emit(WorkerEventTypes.RESCHEDULED, {
|
|
261
|
+
work,
|
|
262
|
+
oldScheduled: currentWork.scheduled,
|
|
263
|
+
userId,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return work;
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
allocateWork: async ({ types, worker = UNCHAINED_WORKER_ID }) => {
|
|
270
|
+
// Find a work item that is scheduled for now and is not started.
|
|
271
|
+
// Also:
|
|
272
|
+
// - Restrict by types and worker if provided
|
|
273
|
+
// - Sort by default queue order
|
|
274
|
+
const query = buildQuerySelector({
|
|
275
|
+
status: [WorkStatus.NEW],
|
|
276
|
+
scheduled: { end: new Date() },
|
|
277
|
+
worker: { $in: [null, worker] },
|
|
278
|
+
...(types ? { type: { $in: types } } : {}),
|
|
279
|
+
});
|
|
280
|
+
const result = await WorkQueue.findOneAndUpdate(
|
|
281
|
+
query,
|
|
282
|
+
{
|
|
283
|
+
$set: { started: new Date(), worker },
|
|
284
|
+
},
|
|
285
|
+
{ sort: buildSortOptions(defaultSort), returnDocument: 'after' },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
WorkerDirector.events.emit(WorkerEventTypes.ALLOCATED, {
|
|
289
|
+
work: result.value,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return result.value;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
ensureOneWork: async ({ type, input, priority = 0, scheduled, originalWorkId, retries = 20 }) => {
|
|
296
|
+
const created = new Date();
|
|
297
|
+
const query = buildQuerySelector({
|
|
298
|
+
type,
|
|
299
|
+
status: [WorkStatus.NEW],
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
const workId = `${type}:${scheduled.getTime()}`;
|
|
303
|
+
const result = await (WorkQueue.findOneAndUpdate(
|
|
304
|
+
query,
|
|
305
|
+
{
|
|
306
|
+
$set: {
|
|
307
|
+
input,
|
|
308
|
+
priority,
|
|
309
|
+
worker: null,
|
|
310
|
+
updated: created,
|
|
311
|
+
},
|
|
312
|
+
$setOnInsert: {
|
|
313
|
+
_id: workId,
|
|
314
|
+
type,
|
|
315
|
+
originalWorkId,
|
|
316
|
+
scheduled,
|
|
317
|
+
retries,
|
|
318
|
+
created,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
sort: buildSortOptions(defaultSort),
|
|
323
|
+
returnDocument: 'after',
|
|
324
|
+
upsert: true,
|
|
325
|
+
},
|
|
326
|
+
) as Promise<ModifyResult<Work>>);
|
|
327
|
+
|
|
328
|
+
if (!result.lastErrorObject.updatedExisting) {
|
|
329
|
+
log(`WorkerDirector -> Work added again (ensure) ${type} ${scheduled} ${retries}`);
|
|
330
|
+
|
|
331
|
+
WorkerDirector.events.emit(WorkerEventTypes.ADDED, {
|
|
332
|
+
work: result.value,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return result.value;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
doWork: (work, requestContext) => {
|
|
342
|
+
return WorkerDirector.doWork(work, requestContext);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
finishWork,
|
|
346
|
+
|
|
347
|
+
deleteWork: async (workId, userId) => {
|
|
348
|
+
const workBeforeRemoval = await WorkQueue.findOne(
|
|
349
|
+
buildQuerySelector({
|
|
350
|
+
workId,
|
|
351
|
+
status: [WorkStatus.NEW, WorkStatus.ALLOCATED],
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
if (!workBeforeRemoval) return null;
|
|
355
|
+
|
|
356
|
+
await mutations.delete(workId, userId);
|
|
357
|
+
|
|
358
|
+
const work = await WorkQueue.findOne(generateDbFilterById(workId), {});
|
|
359
|
+
|
|
360
|
+
WorkerDirector.events.emit(WorkerEventTypes.DELETED, { work, userId });
|
|
361
|
+
|
|
362
|
+
return work;
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
markOldWorkAsFailed: async ({ types, worker, referenceDate }, userId) => {
|
|
366
|
+
const workQueue = await WorkQueue.find(
|
|
367
|
+
buildQuerySelector({
|
|
368
|
+
status: [WorkStatus.ALLOCATED],
|
|
369
|
+
started: { $lte: referenceDate },
|
|
370
|
+
worker,
|
|
371
|
+
type: { $in: types },
|
|
372
|
+
}),
|
|
373
|
+
{ type: 1, sort: { test: 1 } } as Projection<Work>,
|
|
374
|
+
).toArray();
|
|
375
|
+
|
|
376
|
+
return Promise.all(
|
|
377
|
+
workQueue.map(({ _id }) =>
|
|
378
|
+
finishWork(
|
|
379
|
+
_id as string,
|
|
380
|
+
{
|
|
381
|
+
finished: new Date(),
|
|
382
|
+
result: null,
|
|
383
|
+
success: false,
|
|
384
|
+
error: {
|
|
385
|
+
name: DIRECTOR_MARKED_FAILED_ERROR,
|
|
386
|
+
message:
|
|
387
|
+
'Director marked old work as failed after restart. This work was eventually running at the moment when node.js exited.',
|
|
388
|
+
},
|
|
389
|
+
worker,
|
|
390
|
+
},
|
|
391
|
+
userId,
|
|
392
|
+
),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IScheduler, Work } from '@unchainedshop/types/worker';
|
|
2
|
+
import { log } from '@unchainedshop/logger';
|
|
3
|
+
import { WorkerDirector } from '../director/WorkerDirector';
|
|
4
|
+
import { WorkerEventTypes } from '../director/WorkerEventTypes';
|
|
5
|
+
|
|
6
|
+
export const FailedRescheduler: IScheduler = {
|
|
7
|
+
key: 'shop.unchained.scheduler.failed',
|
|
8
|
+
label: 'Reschedule failed works',
|
|
9
|
+
version: '1.0',
|
|
10
|
+
|
|
11
|
+
actions: (requestContext) => {
|
|
12
|
+
const handleFinishedWork = ({ work, userId }: { work: Work; userId: string }) => {
|
|
13
|
+
if (!work.success && work.retries > 0) {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const workDelayMs = work.scheduled.getTime() - work.created.getTime();
|
|
16
|
+
|
|
17
|
+
// In short: Double the delay of the old work or delay for 5 seconds
|
|
18
|
+
const scheduled =
|
|
19
|
+
workDelayMs > 1000
|
|
20
|
+
? new Date(now.getTime() + workDelayMs * 2)
|
|
21
|
+
: new Date(now.setSeconds(now.getSeconds() + 5));
|
|
22
|
+
|
|
23
|
+
log(
|
|
24
|
+
`${FailedRescheduler.key} -> Reschedule failed work ${work._id} ${
|
|
25
|
+
work.type
|
|
26
|
+
} for ${scheduled} (in ${Math.round(workDelayMs / 1000)}). Remaining retries: ${work.retries}`,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
requestContext.modules.worker.addWork(
|
|
30
|
+
{
|
|
31
|
+
type: work.type,
|
|
32
|
+
input: work.input,
|
|
33
|
+
priority: work.priority,
|
|
34
|
+
originalWorkId: work.originalWorkId || work._id,
|
|
35
|
+
retries: work.retries - 1,
|
|
36
|
+
scheduled,
|
|
37
|
+
},
|
|
38
|
+
userId,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
start() {
|
|
45
|
+
WorkerDirector.events.on(WorkerEventTypes.FINISHED, handleFinishedWork);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
stop() {
|
|
49
|
+
WorkerDirector.events.off(WorkerEventTypes.FINISHED, handleFinishedWork);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { configureWorkerModule } from './module/configureWorkerModule';
|
|
2
|
+
|
|
3
|
+
export { WorkerDirector } from './director/WorkerDirector';
|
|
4
|
+
export { WorkerAdapter } from './director/WorkerAdapter';
|
|
5
|
+
export { WorkStatus } from './director/WorkStatus';
|
|
6
|
+
export { WorkerEventTypes } from './director/WorkerEventTypes';
|
|
7
|
+
|
|
8
|
+
export { FailedRescheduler } from './schedulers/FailedRescheduler';
|
|
9
|
+
|
|
10
|
+
export { BaseWorker } from './workers/BaseWorker';
|
|
11
|
+
export { EventListenerWorker } from './workers/EventListenerWorker';
|
|
12
|
+
export { IntervalWorker } from './workers/IntervalWorker';
|