@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.
Files changed (57) hide show
  1. package/.npm/package/README +7 -0
  2. package/.npm/package/npm-shrinkwrap.json +257 -0
  3. package/.versions +53 -0
  4. package/README.md +9 -0
  5. package/lib/db/WorkQueueCollection.d.ts +3 -0
  6. package/lib/db/WorkQueueCollection.js +32 -0
  7. package/lib/db/WorkQueueCollection.js.map +1 -0
  8. package/lib/db/WorkQueueSchema.d.ts +1 -0
  9. package/lib/db/WorkQueueSchema.js +20 -0
  10. package/lib/db/WorkQueueSchema.js.map +1 -0
  11. package/lib/director/WorkStatus.d.ts +7 -0
  12. package/lib/director/WorkStatus.js +9 -0
  13. package/lib/director/WorkStatus.js.map +1 -0
  14. package/lib/director/WorkerAdapter.d.ts +2 -0
  15. package/lib/director/WorkerAdapter.js +10 -0
  16. package/lib/director/WorkerAdapter.js.map +1 -0
  17. package/lib/director/WorkerDirector.d.ts +3 -0
  18. package/lib/director/WorkerDirector.js +46 -0
  19. package/lib/director/WorkerDirector.js.map +1 -0
  20. package/lib/director/WorkerEventTypes.d.ts +8 -0
  21. package/lib/director/WorkerEventTypes.js +13 -0
  22. package/lib/director/WorkerEventTypes.js.map +1 -0
  23. package/lib/module/configureWorkerModule.d.ts +3 -0
  24. package/lib/module/configureWorkerModule.js +272 -0
  25. package/lib/module/configureWorkerModule.js.map +1 -0
  26. package/lib/schedulers/FailedRescheduler.d.ts +2 -0
  27. package/lib/schedulers/FailedRescheduler.js +38 -0
  28. package/lib/schedulers/FailedRescheduler.js.map +1 -0
  29. package/lib/worker-index.d.ts +9 -0
  30. package/lib/worker-index.js +10 -0
  31. package/lib/worker-index.js.map +1 -0
  32. package/lib/workers/BaseWorker.d.ts +7 -0
  33. package/lib/workers/BaseWorker.js +85 -0
  34. package/lib/workers/BaseWorker.js.map +1 -0
  35. package/lib/workers/EventListenerWorker.d.ts +4 -0
  36. package/lib/workers/EventListenerWorker.js +44 -0
  37. package/lib/workers/EventListenerWorker.js.map +1 -0
  38. package/lib/workers/IntervalWorker.d.ts +9 -0
  39. package/lib/workers/IntervalWorker.js +38 -0
  40. package/lib/workers/IntervalWorker.js.map +1 -0
  41. package/package.js +33 -0
  42. package/package.json +43 -0
  43. package/src/db/WorkQueueCollection.ts +37 -0
  44. package/src/db/WorkQueueSchema.js +23 -0
  45. package/src/director/WorkStatus.ts +7 -0
  46. package/src/director/WorkerAdapter.ts +12 -0
  47. package/src/director/WorkerDirector.ts +73 -0
  48. package/src/director/WorkerEventTypes.ts +11 -0
  49. package/src/module/configureWorkerModule.ts +397 -0
  50. package/src/schedulers/FailedRescheduler.ts +53 -0
  51. package/src/worker-index.ts +12 -0
  52. package/src/workers/BaseWorker.ts +116 -0
  53. package/src/workers/EventListenerWorker.ts +55 -0
  54. package/src/workers/IntervalWorker.ts +57 -0
  55. package/tests/worker-index.test.ts +32 -0
  56. package/tsconfig.build.json +26 -0
  57. 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,7 @@
1
+ export enum WorkStatus {
2
+ NEW = 'NEW',
3
+ ALLOCATED = 'ALLOCATED',
4
+ SUCCESS = 'SUCCESS',
5
+ FAILED = 'FAILED',
6
+ DELETED = 'DELETED',
7
+ }
@@ -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';