@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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { IWorker, Work } from '@unchainedshop/types/worker';
|
|
2
|
+
import later from 'later';
|
|
3
|
+
import { log } from '@unchainedshop/logger';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { WorkerDirector } from '../director/WorkerDirector';
|
|
6
|
+
|
|
7
|
+
const { UNCHAINED_WORKER_ID } = process.env;
|
|
8
|
+
|
|
9
|
+
const resolveWorkerId = (customWorkerId: string, type: string) =>
|
|
10
|
+
customWorkerId || UNCHAINED_WORKER_ID || `${os.hostname()}:${type}`;
|
|
11
|
+
|
|
12
|
+
interface WorkerParams {
|
|
13
|
+
workerId: string;
|
|
14
|
+
worker: IWorker<any>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const BaseWorker: IWorker<WorkerParams> = {
|
|
18
|
+
key: 'shop.unchained.worker.base',
|
|
19
|
+
label: 'Base worker. Do not use this directly.',
|
|
20
|
+
version: '1.0',
|
|
21
|
+
type: 'BASE',
|
|
22
|
+
|
|
23
|
+
getFloorDate: (date = new Date()) => {
|
|
24
|
+
const floored = new Date(Math.floor(date.getTime() / 1000) * 1000);
|
|
25
|
+
return floored;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
getInternalTypes() {
|
|
29
|
+
return WorkerDirector.getActivePluginTypes().filter((type) => type !== 'EXTERNAL');
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
actions: ({ workerId, worker }: WorkerParams, requestContext) => {
|
|
33
|
+
const resolvedWorkerId = resolveWorkerId(workerId, worker.type);
|
|
34
|
+
log(`${worker.key} -> Initialized: ${resolvedWorkerId}`);
|
|
35
|
+
|
|
36
|
+
const workerActions = {
|
|
37
|
+
start() {
|
|
38
|
+
throw new Error(`Not implemented on ${this.constructor.key}`);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
stop() {
|
|
42
|
+
throw new Error(`Not implemented on ${this.constructor.key}`);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
reset: async (referenceDate = new Date()) => {
|
|
46
|
+
await requestContext.modules.worker.markOldWorkAsFailed({
|
|
47
|
+
types: BaseWorker.getInternalTypes(),
|
|
48
|
+
worker: workerId,
|
|
49
|
+
referenceDate,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
autorescheduleTypes: async ({ referenceDate }) => {
|
|
54
|
+
return Promise.all(
|
|
55
|
+
WorkerDirector.getAutoSchedules().map(async ([type, work]) => {
|
|
56
|
+
const { schedule, input, ...rest } = work;
|
|
57
|
+
const fixedSchedule = { ...schedule };
|
|
58
|
+
fixedSchedule.schedules[0].s = [0]; // ignore seconds, always run on second 0
|
|
59
|
+
const nextDate = later.schedule(fixedSchedule).next(1, referenceDate);
|
|
60
|
+
nextDate.setMilliseconds(0);
|
|
61
|
+
return requestContext.modules.worker.ensureOneWork({
|
|
62
|
+
type,
|
|
63
|
+
input: input(),
|
|
64
|
+
scheduled: nextDate,
|
|
65
|
+
worker: workerId,
|
|
66
|
+
...rest,
|
|
67
|
+
retries: 0,
|
|
68
|
+
});
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
process: async ({ maxWorkItemCount, referenceDate }) => {
|
|
74
|
+
await workerActions.autorescheduleTypes({
|
|
75
|
+
referenceDate,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const processRecursively = async (recursionCounter = 0) => {
|
|
79
|
+
if (maxWorkItemCount && maxWorkItemCount < recursionCounter) return null;
|
|
80
|
+
|
|
81
|
+
const work = await requestContext.modules.worker.allocateWork({
|
|
82
|
+
types: BaseWorker.getInternalTypes(),
|
|
83
|
+
worker: workerId,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let doneWork: Work | null = null;
|
|
87
|
+
|
|
88
|
+
if (work) {
|
|
89
|
+
const output = await requestContext.modules.worker.doWork(work, requestContext);
|
|
90
|
+
|
|
91
|
+
doneWork = await requestContext.modules.worker.finishWork(
|
|
92
|
+
work._id,
|
|
93
|
+
{
|
|
94
|
+
...output,
|
|
95
|
+
finished: work.finished || new Date(),
|
|
96
|
+
started: work.started,
|
|
97
|
+
worker: workerId,
|
|
98
|
+
},
|
|
99
|
+
'0',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (doneWork) return processRecursively(recursionCounter + 1);
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await processRecursively();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
workerActions.reset();
|
|
113
|
+
|
|
114
|
+
return workerActions;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { IWorker } from '@unchainedshop/types/worker';
|
|
2
|
+
import { WorkerEventTypes } from '../director/WorkerEventTypes';
|
|
3
|
+
import { WorkerDirector } from '../director/WorkerDirector';
|
|
4
|
+
import { BaseWorker } from './BaseWorker';
|
|
5
|
+
|
|
6
|
+
export const EventListenerWorker: IWorker<{ workerId: string }> = {
|
|
7
|
+
...BaseWorker,
|
|
8
|
+
|
|
9
|
+
key: 'shop.unchained.worker.event-listener',
|
|
10
|
+
label: 'Allocates work on events. This worker does not make sense on multiple containers.',
|
|
11
|
+
version: '1.0',
|
|
12
|
+
type: 'EVENT_LISTENER',
|
|
13
|
+
|
|
14
|
+
actions: ({ workerId }, requestContext) => {
|
|
15
|
+
let onAdded: () => Promise<void>;
|
|
16
|
+
let onFinished: () => Promise<void>;
|
|
17
|
+
|
|
18
|
+
const baseWorkerActions = BaseWorker.actions(
|
|
19
|
+
{ workerId, worker: EventListenerWorker },
|
|
20
|
+
requestContext,
|
|
21
|
+
);
|
|
22
|
+
return {
|
|
23
|
+
...baseWorkerActions,
|
|
24
|
+
|
|
25
|
+
start() {
|
|
26
|
+
onAdded = async () => {
|
|
27
|
+
await baseWorkerActions.process({
|
|
28
|
+
maxWorkItemCount: 0,
|
|
29
|
+
referenceDate: EventListenerWorker.getFloorDate(),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
onFinished = async () => {
|
|
33
|
+
await baseWorkerActions.process({
|
|
34
|
+
maxWorkItemCount: 0,
|
|
35
|
+
referenceDate: EventListenerWorker.getFloorDate(),
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
WorkerDirector.events.on(WorkerEventTypes.ADDED, onAdded);
|
|
40
|
+
WorkerDirector.events.on(WorkerEventTypes.FINISHED, onFinished);
|
|
41
|
+
|
|
42
|
+
setTimeout(async () => {
|
|
43
|
+
await baseWorkerActions.autorescheduleTypes({
|
|
44
|
+
referenceDate: EventListenerWorker.getFloorDate(),
|
|
45
|
+
});
|
|
46
|
+
}, 300);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
stop() {
|
|
50
|
+
WorkerDirector.events.off(WorkerEventTypes.ADDED, onAdded);
|
|
51
|
+
WorkerDirector.events.off(WorkerEventTypes.FINISHED, onFinished);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Context } from '@unchainedshop/types/api';
|
|
2
|
+
import { IWorker, WorkerSchedule } from '@unchainedshop/types/worker';
|
|
3
|
+
import later from 'later';
|
|
4
|
+
import { BaseWorker } from './BaseWorker';
|
|
5
|
+
|
|
6
|
+
const { NODE_ENV } = process.env;
|
|
7
|
+
|
|
8
|
+
interface WorkerParams {
|
|
9
|
+
workerId: string;
|
|
10
|
+
batchCount?: number;
|
|
11
|
+
schedule: WorkerSchedule | string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultSchedule = later.parse.text(
|
|
15
|
+
NODE_ENV !== 'production' ? 'every 2 seconds' : 'every 30 seconds',
|
|
16
|
+
) as WorkerSchedule;
|
|
17
|
+
|
|
18
|
+
export const scheduleToInterval = (scheduleRaw: WorkerSchedule | string) => {
|
|
19
|
+
const referenceDate = new Date(1000);
|
|
20
|
+
const schedule = typeof scheduleRaw === 'string' ? later.parse.text(scheduleRaw) : scheduleRaw;
|
|
21
|
+
const [one, two] = later.schedule(schedule).next(2, referenceDate);
|
|
22
|
+
const diff = new Date(two).getTime() - new Date(one).getTime();
|
|
23
|
+
return Math.min(1000 * 60 * 60, diff); // at least once every hour!
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const IntervalWorker: IWorker<WorkerParams> = {
|
|
27
|
+
...BaseWorker,
|
|
28
|
+
|
|
29
|
+
key: 'shop.unchained.worker.cron',
|
|
30
|
+
label: 'Allocates work on fixed intervals with native node setInterval',
|
|
31
|
+
version: '1.0',
|
|
32
|
+
type: 'CRON',
|
|
33
|
+
|
|
34
|
+
actions: ({ workerId, batchCount = 0, schedule = defaultSchedule }, requestContext: Context) => {
|
|
35
|
+
const baseWorkerActions = BaseWorker.actions({ workerId, worker: IntervalWorker }, requestContext);
|
|
36
|
+
|
|
37
|
+
const intervalDelay = scheduleToInterval(schedule);
|
|
38
|
+
let intervalHandle: NodeJS.Timer;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...baseWorkerActions,
|
|
42
|
+
|
|
43
|
+
start() {
|
|
44
|
+
intervalHandle = setInterval(() => {
|
|
45
|
+
baseWorkerActions.process({
|
|
46
|
+
maxWorkItemCount: batchCount,
|
|
47
|
+
referenceDate: IntervalWorker.getFloorDate(),
|
|
48
|
+
});
|
|
49
|
+
}, intervalDelay);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
stop() {
|
|
53
|
+
clearInterval(intervalHandle);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Db } from '@unchainedshop/types/common';
|
|
2
|
+
import { WorkerModule } from '@unchainedshop/types/worker';
|
|
3
|
+
import { assert } from 'chai';
|
|
4
|
+
import {
|
|
5
|
+
configureWorkerModule,
|
|
6
|
+
WorkerDirector,
|
|
7
|
+
} from '@unchainedshop/core-worker';
|
|
8
|
+
import { initDb } from 'meteor/unchained:mongodb';
|
|
9
|
+
|
|
10
|
+
import { ExternalWorkerPlugin } from '../plugins/external';
|
|
11
|
+
|
|
12
|
+
describe('Test exports', () => {
|
|
13
|
+
let module: WorkerModule;
|
|
14
|
+
let db: Db;
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
db = await initDb();
|
|
18
|
+
module = await configureWorkerModule({ db }).catch((error) => {
|
|
19
|
+
console.warn('ERROR', error);
|
|
20
|
+
return null;
|
|
21
|
+
});
|
|
22
|
+
assert.ok(module);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('Check queries', () => {
|
|
26
|
+
assert.isFunction(module.addWork);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('Check director', () => {
|
|
30
|
+
WorkerDirector.doWork({ type: ExternalWorkerPlugin.type });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"lib": ["esnext"],
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noUnusedLocals": false,
|
|
14
|
+
"outDir": "lib",
|
|
15
|
+
"preserveWatchOutput": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"target": "esnext",
|
|
19
|
+
"types": ["node"],
|
|
20
|
+
"baseUrl": ".", // This must be specified if "paths" is.
|
|
21
|
+
"paths": {
|
|
22
|
+
"meteor/unchained:*": ["node_modules/@unchainedshop/types/index.d.ts"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|