@hotmeshio/hotmesh 0.0.35 → 0.0.37
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/README.md +12 -12
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +3 -1
- package/build/modules/errors.d.ts +9 -1
- package/build/modules/errors.js +12 -1
- package/build/modules/key.d.ts +20 -19
- package/build/modules/key.js +20 -20
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +10 -0
- package/build/services/activities/activity.js +28 -3
- package/build/services/activities/await.js +10 -9
- package/build/services/activities/cycle.js +10 -9
- package/build/services/activities/hook.d.ts +7 -1
- package/build/services/activities/hook.js +61 -44
- package/build/services/activities/interrupt.js +10 -9
- package/build/services/activities/signal.js +7 -7
- package/build/services/activities/trigger.js +4 -2
- package/build/services/activities/worker.js +9 -8
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +2 -2
- package/build/services/durable/workflow.js +17 -17
- package/build/services/engine/index.d.ts +5 -7
- package/build/services/engine/index.js +53 -47
- package/build/services/hotmesh/index.js +3 -3
- package/build/services/{signaler/stream.d.ts → router/index.d.ts} +3 -3
- package/build/services/{signaler/stream.js → router/index.js} +6 -6
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +9 -4
- package/build/services/store/index.js +21 -10
- package/build/services/task/index.d.ts +13 -4
- package/build/services/task/index.js +115 -17
- package/build/services/telemetry/index.js +6 -6
- package/build/services/worker/index.d.ts +3 -3
- package/build/services/worker/index.js +8 -8
- package/build/types/job.d.ts +2 -0
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +4 -1
- package/modules/errors.ts +18 -0
- package/modules/key.ts +21 -20
- package/package.json +1 -1
- package/services/activities/activity.ts +44 -4
- package/services/activities/await.ts +14 -10
- package/services/activities/cycle.ts +14 -10
- package/services/activities/hook.ts +70 -47
- package/services/activities/interrupt.ts +13 -10
- package/services/activities/signal.ts +11 -8
- package/services/activities/trigger.ts +5 -1
- package/services/activities/worker.ts +13 -9
- package/services/durable/meshos.ts +1 -1
- package/services/durable/worker.ts +1 -1
- package/services/durable/workflow.ts +1 -1
- package/services/engine/index.ts +82 -44
- package/services/hotmesh/index.ts +3 -3
- package/services/{signaler/stream.ts → router/index.ts} +5 -5
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/ioredis.ts +1 -0
- package/services/store/index.ts +23 -12
- package/services/task/index.ts +120 -21
- package/services/telemetry/index.ts +6 -6
- package/services/worker/index.ts +7 -7
- package/types/job.ts +2 -0
- package/types/stream.ts +6 -5
- package/build/services/signaler/store.d.ts +0 -15
- package/build/services/signaler/store.js +0 -68
- package/services/signaler/store.ts +0 -76
- /package/build/{services/durable/asyncLocalStorage.d.ts → modules/storage.d.ts} +0 -0
- /package/build/{services/durable/asyncLocalStorage.js → modules/storage.js} +0 -0
- /package/{services/durable/asyncLocalStorage.ts → modules/storage.ts} +0 -0
|
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TaskService = void 0;
|
|
4
4
|
const enums_1 = require("../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
+
const pipe_1 = require("../pipe");
|
|
6
7
|
class TaskService {
|
|
7
8
|
constructor(store, logger) {
|
|
8
9
|
this.cleanupTimeout = null;
|
|
10
|
+
this.isScout = false;
|
|
9
11
|
this.logger = logger;
|
|
10
12
|
this.store = store;
|
|
11
13
|
}
|
|
@@ -36,29 +38,61 @@ class TaskService {
|
|
|
36
38
|
await this.store.registerExpireJob(jobId, expireTimeSlot, options);
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
async registerTimeHook(jobId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
|
|
41
|
+
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
|
|
40
42
|
const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
41
|
-
await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
|
|
43
|
+
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Should this engine instance play the role of 'scout' for the quorum.
|
|
47
|
+
*/
|
|
48
|
+
async shouldScout() {
|
|
49
|
+
const wasScout = this.isScout;
|
|
50
|
+
const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
|
|
51
|
+
if (isScout) {
|
|
52
|
+
if (!wasScout) {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
this.isScout = false;
|
|
55
|
+
}, enums_1.SCOUT_INTERVAL_SECONDS * 1000);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
42
60
|
}
|
|
43
61
|
async processTimeHooks(timeEventCallback, listKey) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
if (await this.shouldScout()) {
|
|
63
|
+
try {
|
|
64
|
+
const timeJob = await this.store.getNextTimeJob(listKey);
|
|
65
|
+
if (Array.isArray(timeJob)) {
|
|
66
|
+
//a queue had a job; try again immediately
|
|
67
|
+
const [listKey, jobId, gId, activityId, type] = timeJob;
|
|
68
|
+
await timeEventCallback(jobId, gId, activityId, type);
|
|
69
|
+
await (0, utils_1.sleepFor)(0);
|
|
70
|
+
this.processTimeHooks(timeEventCallback, listKey);
|
|
71
|
+
}
|
|
72
|
+
else if (timeJob) {
|
|
73
|
+
//a queue was just emptied; try again immediately
|
|
74
|
+
await (0, utils_1.sleepFor)(0);
|
|
75
|
+
this.processTimeHooks(timeEventCallback);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
//all queues are empty; sleep before checking
|
|
79
|
+
let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
|
|
80
|
+
this.cleanupTimeout = sleep.timerId;
|
|
81
|
+
await sleep.promise;
|
|
82
|
+
this.processTimeHooks(timeEventCallback);
|
|
83
|
+
}
|
|
51
84
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
await sleep.promise;
|
|
56
|
-
this.processTimeHooks(timeEventCallback);
|
|
85
|
+
catch (err) {
|
|
86
|
+
//todo: retry connect to redis
|
|
87
|
+
this.logger.error('task-process-timehooks-error', err);
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
90
|
+
else {
|
|
91
|
+
//didn't get the scout role; try again in 'one-ish' minutes
|
|
92
|
+
let sleep = (0, utils_1.XSleepFor)(enums_1.SCOUT_INTERVAL_SECONDS * 1000 * 2 * Math.random());
|
|
93
|
+
this.cleanupTimeout = sleep.timerId;
|
|
94
|
+
await sleep.promise;
|
|
95
|
+
this.processTimeHooks(timeEventCallback);
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
98
|
cancelCleanup() {
|
|
@@ -67,5 +101,69 @@ class TaskService {
|
|
|
67
101
|
this.cleanupTimeout = undefined;
|
|
68
102
|
}
|
|
69
103
|
}
|
|
104
|
+
async getHookRule(topic) {
|
|
105
|
+
const rules = await this.store.getHookRules();
|
|
106
|
+
return rules?.[topic]?.[0];
|
|
107
|
+
}
|
|
108
|
+
async registerWebHook(topic, context, dad, multi) {
|
|
109
|
+
const hookRule = await this.getHookRule(topic);
|
|
110
|
+
if (hookRule) {
|
|
111
|
+
const mapExpression = hookRule.conditions.match[0].expected;
|
|
112
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
113
|
+
const jobId = context.metadata.jid;
|
|
114
|
+
const gId = context.metadata.gid;
|
|
115
|
+
const activityId = hookRule.to;
|
|
116
|
+
const hook = {
|
|
117
|
+
topic,
|
|
118
|
+
resolved,
|
|
119
|
+
jobId: `${activityId}::${dad}::${gId}::${jobId}`,
|
|
120
|
+
};
|
|
121
|
+
await this.store.setHookSignal(hook, multi);
|
|
122
|
+
return jobId;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw new Error('signaler.registerWebHook:error: hook rule not found');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async processWebHookSignal(topic, data) {
|
|
129
|
+
const hookRule = await this.getHookRule(topic);
|
|
130
|
+
if (hookRule) {
|
|
131
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
132
|
+
// `$self.hook.data` OR `$hook.data`
|
|
133
|
+
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
134
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
135
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
136
|
+
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
137
|
+
if (!hookSignalId) {
|
|
138
|
+
//messages can be double-processed; not an issue; return undefined
|
|
139
|
+
//users can also provide a bogus topic; not an issue; return undefined
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
//`aid` is part of composit key, but the hook `topic` is its public interface;
|
|
143
|
+
// this means that a new version of the graph can be deployed and the
|
|
144
|
+
// topic can be re-mapped to a different activity id. Outside callers
|
|
145
|
+
// can adhere to the unchanged contract (calling the same topic),
|
|
146
|
+
// while the internal system can be updated in real time as necessary.
|
|
147
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
|
|
148
|
+
return [jid.join('::'), hookRule.to, dad, gid];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new Error('signal-not-found');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async deleteWebHookSignal(topic, data) {
|
|
155
|
+
const hookRule = await this.getHookRule(topic);
|
|
156
|
+
if (hookRule) {
|
|
157
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
158
|
+
// `$self.hook.data` OR `$hook.data`
|
|
159
|
+
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
160
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
161
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
162
|
+
return await this.store.deleteHookSignal(topic, resolved);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error('signaler.process:error: hook rule not found');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
70
168
|
}
|
|
71
169
|
exports.TaskService = TaskService;
|
|
@@ -93,17 +93,17 @@ class TelemetryService {
|
|
|
93
93
|
return result;
|
|
94
94
|
}, {})
|
|
95
95
|
};
|
|
96
|
-
this.span
|
|
96
|
+
this.span?.setAttributes(namespacedAtts);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
setActivityAttributes(attributes) {
|
|
100
|
-
this.span
|
|
100
|
+
this.span?.setAttributes(attributes);
|
|
101
101
|
}
|
|
102
102
|
setStreamAttributes(attributes) {
|
|
103
|
-
this.span
|
|
103
|
+
this.span?.setAttributes(attributes);
|
|
104
104
|
}
|
|
105
105
|
setJobAttributes(attributes) {
|
|
106
|
-
this.jobSpan
|
|
106
|
+
this.jobSpan?.setAttributes(attributes);
|
|
107
107
|
}
|
|
108
108
|
endJobSpan() {
|
|
109
109
|
this.endSpan(this.jobSpan);
|
|
@@ -174,10 +174,10 @@ class TelemetryService {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
setActivityError(message) {
|
|
177
|
-
this.span
|
|
177
|
+
this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
|
|
178
178
|
}
|
|
179
179
|
setStreamError(message) {
|
|
180
|
-
this.span
|
|
180
|
+
this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* Adds the paths (HGET) necessary to restore telemetry state for an activity
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ILogger } from "../logger";
|
|
2
|
-
import {
|
|
2
|
+
import { Router } from "../router";
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
4
|
import { StreamService } from '../stream';
|
|
5
5
|
import { SubService } from '../sub';
|
|
@@ -15,7 +15,7 @@ declare class WorkerService {
|
|
|
15
15
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
16
16
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
17
17
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
18
|
-
|
|
18
|
+
router: Router | null;
|
|
19
19
|
logger: ILogger;
|
|
20
20
|
reporting: boolean;
|
|
21
21
|
static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
|
|
@@ -23,7 +23,7 @@ declare class WorkerService {
|
|
|
23
23
|
initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
|
|
24
24
|
initSubChannel(service: WorkerService, sub: RedisClient): Promise<void>;
|
|
25
25
|
initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
|
|
26
|
-
|
|
26
|
+
initRouter(worker: HotMeshWorker, logger: ILogger): Router;
|
|
27
27
|
subscriptionHandler(): SubscriptionCallback;
|
|
28
28
|
throttle(delayInMillis: number): Promise<void>;
|
|
29
29
|
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkerService = void 0;
|
|
4
4
|
const key_1 = require("../../modules/key");
|
|
5
|
-
const
|
|
5
|
+
const router_1 = require("../router");
|
|
6
6
|
const redis_1 = require("../store/clients/redis");
|
|
7
7
|
const ioredis_1 = require("../store/clients/ioredis");
|
|
8
8
|
const redis_2 = require("../stream/clients/redis");
|
|
9
9
|
const ioredis_2 = require("../stream/clients/ioredis");
|
|
10
10
|
const ioredis_3 = require("../sub/clients/ioredis");
|
|
11
11
|
const redis_3 = require("../sub/clients/redis");
|
|
12
|
-
const
|
|
12
|
+
const stream_1 = require("../../types/stream");
|
|
13
13
|
const utils_1 = require("../../modules/utils");
|
|
14
14
|
const connector_1 = require("../connector");
|
|
15
15
|
class WorkerService {
|
|
@@ -35,9 +35,9 @@ class WorkerService {
|
|
|
35
35
|
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
|
|
36
36
|
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
|
|
37
37
|
await service.initStreamChannel(service, worker.stream);
|
|
38
|
-
service.
|
|
38
|
+
service.router = service.initRouter(worker, logger);
|
|
39
39
|
const key = service.stream.mintKey(key_1.KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
|
|
40
|
-
await service.
|
|
40
|
+
await service.router.consumeMessages(key, 'WORKER', service.guid, worker.callback);
|
|
41
41
|
services.push(service);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -78,12 +78,12 @@ class WorkerService {
|
|
|
78
78
|
}
|
|
79
79
|
await service.stream.init(service.namespace, service.appId, service.logger);
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
return new
|
|
81
|
+
initRouter(worker, logger) {
|
|
82
|
+
return new router_1.Router({
|
|
83
83
|
namespace: this.namespace,
|
|
84
84
|
appId: this.appId,
|
|
85
85
|
guid: this.guid,
|
|
86
|
-
role:
|
|
86
|
+
role: stream_1.StreamRole.WORKER,
|
|
87
87
|
topic: worker.topic,
|
|
88
88
|
reclaimDelay: worker.reclaimDelay,
|
|
89
89
|
reclaimCount: worker.reclaimCount,
|
|
@@ -99,7 +99,7 @@ class WorkerService {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
async throttle(delayInMillis) {
|
|
102
|
-
this.
|
|
102
|
+
this.router.setThrottle(delayInMillis);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
exports.WorkerService = WorkerService;
|
package/build/types/job.d.ts
CHANGED
package/build/types/stream.d.ts
CHANGED
package/modules/enums.ts
CHANGED
|
@@ -29,4 +29,7 @@ export const TEST_FIDELITY_SECONDS = 5;
|
|
|
29
29
|
export const FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? TEST_FIDELITY_SECONDS : BASE_FIDELITY_SECONDS
|
|
30
30
|
|
|
31
31
|
// DURABLE CONSTANTS
|
|
32
|
-
export const DURABLE_EXPIRE_SECONDS = 1;
|
|
32
|
+
export const DURABLE_EXPIRE_SECONDS = 1;
|
|
33
|
+
|
|
34
|
+
// TASK CONSTANTS
|
|
35
|
+
export const SCOUT_INTERVAL_SECONDS = 60;
|
package/modules/errors.ts
CHANGED
|
@@ -122,6 +122,23 @@ class InactiveJobError extends Error {
|
|
|
122
122
|
this.status = status;
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
class GenerationalError extends Error {
|
|
126
|
+
expected: string;
|
|
127
|
+
actual: string;
|
|
128
|
+
jobId: string;
|
|
129
|
+
activityId: string;
|
|
130
|
+
dimensionalAddress: string;
|
|
131
|
+
|
|
132
|
+
constructor(expected: string, actual: string, jobId: string, activityId: string, dimensionalAddress: string) {
|
|
133
|
+
super("Generational Error");
|
|
134
|
+
this.expected = expected;
|
|
135
|
+
this.actual = actual;
|
|
136
|
+
this.jobId = jobId;
|
|
137
|
+
this.activityId = activityId;
|
|
138
|
+
this.dimensionalAddress = dimensionalAddress;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
class ExecActivityError extends Error {
|
|
126
143
|
constructor() {
|
|
127
144
|
super("Error occurred while executing activity");
|
|
@@ -155,6 +172,7 @@ export {
|
|
|
155
172
|
DurableWaitForSignalError,
|
|
156
173
|
DuplicateJobError,
|
|
157
174
|
ExecActivityError,
|
|
175
|
+
GenerationalError,
|
|
158
176
|
GetStateError,
|
|
159
177
|
InactiveJobError,
|
|
160
178
|
MapDataError,
|
package/modules/key.ts
CHANGED
|
@@ -31,25 +31,25 @@ const HMNS = "hmsh";
|
|
|
31
31
|
|
|
32
32
|
//these are the entity types that are stored in the key/value store
|
|
33
33
|
enum KeyType {
|
|
34
|
-
APP,
|
|
35
|
-
ENGINE_ID,
|
|
36
|
-
HOOKS,
|
|
37
|
-
JOB_DEPENDENTS,
|
|
38
|
-
JOB_STATE,
|
|
39
|
-
JOB_STATS_GENERAL,
|
|
40
|
-
JOB_STATS_MEDIAN,
|
|
41
|
-
JOB_STATS_INDEX,
|
|
42
|
-
HOTMESH,
|
|
43
|
-
QUORUM,
|
|
44
|
-
SCHEMAS,
|
|
45
|
-
SIGNALS,
|
|
46
|
-
STREAMS,
|
|
47
|
-
SUBSCRIPTIONS,
|
|
48
|
-
SUBSCRIPTION_PATTERNS,
|
|
49
|
-
SYMKEYS,
|
|
50
|
-
SYMVALS,
|
|
51
|
-
TIME_RANGE,
|
|
52
|
-
WORK_ITEMS,
|
|
34
|
+
APP = 'APP',
|
|
35
|
+
ENGINE_ID = 'ENGINE',
|
|
36
|
+
HOOKS = 'HOOKS',
|
|
37
|
+
JOB_DEPENDENTS = 'JOB_DEPENDENTS',
|
|
38
|
+
JOB_STATE = 'JOB_STATE',
|
|
39
|
+
JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
|
|
40
|
+
JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
|
|
41
|
+
JOB_STATS_INDEX = 'JOB_STATS_INDEX',
|
|
42
|
+
HOTMESH = 'HOTMESH',
|
|
43
|
+
QUORUM = 'QUORUM',
|
|
44
|
+
SCHEMAS = 'SCHEMAS',
|
|
45
|
+
SIGNALS = 'SIGNALS',
|
|
46
|
+
STREAMS = 'STREAMS',
|
|
47
|
+
SUBSCRIPTIONS = 'SUBSCRIPTIONS',
|
|
48
|
+
SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
|
|
49
|
+
SYMKEYS = 'SYMKEYS',
|
|
50
|
+
SYMVALS = 'SYMVALS',
|
|
51
|
+
TIME_RANGE = 'TIME_RANGE',
|
|
52
|
+
WORK_ITEMS = 'WORK_ITEMS',
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
//when minting a key, the following parameters are used to create a unique key per entity
|
|
@@ -64,6 +64,7 @@ type KeyStoreParams = {
|
|
|
64
64
|
facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
|
|
65
65
|
topic?: string; //topic name (e.g., "foo" or "" for top-level)
|
|
66
66
|
timeValue?: number; //time value (rounded to minute) (for delete range)
|
|
67
|
+
scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
class KeyService {
|
|
@@ -86,7 +87,7 @@ class KeyService {
|
|
|
86
87
|
case KeyType.ENGINE_ID:
|
|
87
88
|
return `${namespace}:${params.appId}:e:${params.engineId}`;
|
|
88
89
|
case KeyType.WORK_ITEMS:
|
|
89
|
-
return `${namespace}:${params.appId}:w
|
|
90
|
+
return `${namespace}:${params.appId}:w:${params.scoutType || ''}`;
|
|
90
91
|
case KeyType.TIME_RANGE:
|
|
91
92
|
return `${namespace}:${params.appId}:t:${params.timeValue || ''}`;
|
|
92
93
|
case KeyType.APP:
|
package/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EXPIRE_DURATION } from '../../modules/enums';
|
|
2
|
+
import {
|
|
3
|
+
CollationError,
|
|
4
|
+
GenerationalError,
|
|
5
|
+
GetStateError,
|
|
6
|
+
InactiveJobError } from '../../modules/errors';
|
|
2
7
|
import {
|
|
3
8
|
formatISODate,
|
|
4
9
|
getValueByPath,
|
|
@@ -30,7 +35,6 @@ import {
|
|
|
30
35
|
StreamDataType,
|
|
31
36
|
StreamStatus } from '../../types/stream';
|
|
32
37
|
import { TransitionRule } from '../../types/transition';
|
|
33
|
-
import { EXPIRE_DURATION } from '../../modules/enums';
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
40
|
* The base class for all activities
|
|
@@ -71,6 +75,21 @@ class Activity {
|
|
|
71
75
|
this.leg = leg;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Upon entering leg 1 of a duplexed activty, verify
|
|
80
|
+
* all aspects of the entry including job and activty state
|
|
81
|
+
*/
|
|
82
|
+
async verifyEntry() {
|
|
83
|
+
this.setLeg(1);
|
|
84
|
+
await this.getState();
|
|
85
|
+
CollatorService.assertJobActive(
|
|
86
|
+
this.context.metadata.js,
|
|
87
|
+
this.context.metadata.jid,
|
|
88
|
+
this.metadata.aid
|
|
89
|
+
);
|
|
90
|
+
await CollatorService.notarizeEntry(this);
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
//******** DUPLEX RE-ENTRY POINT ********//
|
|
75
94
|
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
|
|
76
95
|
this.setLeg(2);
|
|
@@ -116,6 +135,9 @@ class Activity {
|
|
|
116
135
|
} else if (error instanceof InactiveJobError) {
|
|
117
136
|
this.logger.info('process-event-inactive-job-error', { error });
|
|
118
137
|
return;
|
|
138
|
+
} else if (error instanceof GenerationalError) {
|
|
139
|
+
this.logger.info('process-event-generational-job-error', { error });
|
|
140
|
+
return;
|
|
119
141
|
} else if (error instanceof GetStateError) {
|
|
120
142
|
this.logger.info('process-event-get-job-error', { error });
|
|
121
143
|
return;
|
|
@@ -337,7 +359,7 @@ class Activity {
|
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
async getState() {
|
|
340
|
-
|
|
362
|
+
const gid = this.context.metadata.gid;
|
|
341
363
|
const jobSymbolHashName = `$${this.config.subscribes}`;
|
|
342
364
|
const consumes: Consumes = {
|
|
343
365
|
[jobSymbolHashName]: MDATA_SYMBOLS.JOB.KEYS.map((key) => `metadata/${key}`)
|
|
@@ -365,11 +387,28 @@ class Activity {
|
|
|
365
387
|
//`state` is a flat hash; context is a tree
|
|
366
388
|
const [state, status] = await this.store.getState(jid, consumes, dIds);
|
|
367
389
|
this.context = restoreHierarchy(state) as JobState;
|
|
390
|
+
this.assertGenerationalId(this.context.metadata.gid, gid);
|
|
368
391
|
this.initDimensionalAddress(dad);
|
|
369
392
|
this.initSelf(this.context);
|
|
370
393
|
this.initPolicies(this.context);
|
|
371
394
|
}
|
|
372
395
|
|
|
396
|
+
/**
|
|
397
|
+
* if the job is created/deleted/created with the same key,
|
|
398
|
+
* the 'gid' ensures no stale messages enter the stream
|
|
399
|
+
*/
|
|
400
|
+
assertGenerationalId(jobGID: string, msgGID?: string) {
|
|
401
|
+
if (msgGID !== jobGID) {
|
|
402
|
+
throw new GenerationalError(
|
|
403
|
+
jobGID,
|
|
404
|
+
msgGID,
|
|
405
|
+
this.context.metadata.jid,
|
|
406
|
+
this.context.metadata.aid,
|
|
407
|
+
this.context.metadata.dad
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
373
412
|
initDimensionalAddress(dad: string): void {
|
|
374
413
|
this.metadata.dad = dad;
|
|
375
414
|
}
|
|
@@ -434,6 +473,7 @@ class Activity {
|
|
|
434
473
|
metadata: {
|
|
435
474
|
guid: guid(),
|
|
436
475
|
jid: this.context.metadata.jid,
|
|
476
|
+
gid: this.context.metadata.gid,
|
|
437
477
|
dad: adjacentDad,
|
|
438
478
|
aid: toActivityId,
|
|
439
479
|
spn: this.context['$self'].output.metadata?.l2s,
|
|
@@ -466,7 +506,7 @@ class Activity {
|
|
|
466
506
|
if (adjacencyList.length && jobStatus > 0) {
|
|
467
507
|
const multi = this.store.getMulti();
|
|
468
508
|
for (const execSignal of adjacencyList) {
|
|
469
|
-
await this.engine.
|
|
509
|
+
await this.engine.router?.publishMessage(null, execSignal, multi);
|
|
470
510
|
}
|
|
471
511
|
mIds = (await multi.exec()) as string[];
|
|
472
512
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
GenerationalError,
|
|
3
|
+
GetStateError,
|
|
4
|
+
InactiveJobError } from '../../modules/errors';
|
|
2
5
|
import { Activity } from './activity';
|
|
3
6
|
import { CollatorService } from '../collator';
|
|
4
7
|
import { EngineService } from '../engine';
|
|
8
|
+
import { TelemetryService } from '../telemetry';
|
|
5
9
|
import {
|
|
6
10
|
ActivityData,
|
|
7
11
|
ActivityMetadata,
|
|
@@ -10,7 +14,6 @@ import {
|
|
|
10
14
|
import { JobState } from '../../types/job';
|
|
11
15
|
import { MultiResponseFlags, RedisMulti } from '../../types/redis';
|
|
12
16
|
import { StreamData, StreamDataType } from '../../types/stream';
|
|
13
|
-
import { TelemetryService } from '../telemetry';
|
|
14
17
|
import { Pipe } from '../pipe';
|
|
15
18
|
import { guid } from '../../modules/utils';
|
|
16
19
|
|
|
@@ -29,14 +32,11 @@ class Await extends Activity {
|
|
|
29
32
|
|
|
30
33
|
//******** INITIAL ENTRY POINT (A) ********//
|
|
31
34
|
async process(): Promise<string> {
|
|
32
|
-
this.logger.debug('await-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
35
|
+
this.logger.debug('await-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
|
|
33
36
|
let telemetry: TelemetryService;
|
|
34
37
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
await CollatorService.notarizeEntry(this);
|
|
38
|
-
await this.getState();
|
|
39
|
-
CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
|
|
38
|
+
await this.verifyEntry();
|
|
39
|
+
|
|
40
40
|
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
41
41
|
telemetry.startActivitySpan(this.leg);
|
|
42
42
|
this.mapInputData();
|
|
@@ -62,6 +62,9 @@ class Await extends Activity {
|
|
|
62
62
|
if (error instanceof InactiveJobError) {
|
|
63
63
|
this.logger.error('await-inactive-job-error', { error });
|
|
64
64
|
return;
|
|
65
|
+
} else if (error instanceof GenerationalError) {
|
|
66
|
+
this.logger.info('process-event-generational-job-error', { error });
|
|
67
|
+
return;
|
|
65
68
|
} else if (error instanceof GetStateError) {
|
|
66
69
|
this.logger.error('await-get-state-error', { error });
|
|
67
70
|
return;
|
|
@@ -72,7 +75,7 @@ class Await extends Activity {
|
|
|
72
75
|
throw error;
|
|
73
76
|
} finally {
|
|
74
77
|
telemetry?.endActivitySpan();
|
|
75
|
-
this.logger.debug('await-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
78
|
+
this.logger.debug('await-process-end', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -82,6 +85,7 @@ class Await extends Activity {
|
|
|
82
85
|
metadata: {
|
|
83
86
|
guid: guid(),
|
|
84
87
|
jid: this.context.metadata.jid,
|
|
88
|
+
gid: this.context.metadata.gid,
|
|
85
89
|
dad: this.metadata.dad,
|
|
86
90
|
aid: this.metadata.aid,
|
|
87
91
|
topic,
|
|
@@ -96,7 +100,7 @@ class Await extends Activity {
|
|
|
96
100
|
retry: this.config.retry
|
|
97
101
|
};
|
|
98
102
|
}
|
|
99
|
-
return (await this.engine.
|
|
103
|
+
return (await this.engine.router?.publishMessage(null, streamData, multi)) as string;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
GenerationalError,
|
|
3
|
+
GetStateError,
|
|
4
|
+
InactiveJobError } from '../../modules/errors';
|
|
2
5
|
import { CollatorService } from '../collator';
|
|
3
6
|
import { EngineService } from '../engine';
|
|
4
7
|
import { Activity, ActivityType } from './activity';
|
|
@@ -28,14 +31,11 @@ class Cycle extends Activity {
|
|
|
28
31
|
|
|
29
32
|
//******** LEG 1 ENTRY ********//
|
|
30
33
|
async process(): Promise<string> {
|
|
31
|
-
this.logger.debug('cycle-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
34
|
+
this.logger.debug('cycle-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
|
|
32
35
|
let telemetry: TelemetryService;
|
|
33
36
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await CollatorService.notarizeEntry(this);
|
|
37
|
-
await this.getState();
|
|
38
|
-
CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
|
|
37
|
+
await this.verifyEntry();
|
|
38
|
+
|
|
39
39
|
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
40
40
|
telemetry.startActivitySpan(this.leg);
|
|
41
41
|
this.mapInputData();
|
|
@@ -65,6 +65,9 @@ class Cycle extends Activity {
|
|
|
65
65
|
if (error instanceof InactiveJobError) {
|
|
66
66
|
this.logger.error('cycle-inactive-job-error', { error });
|
|
67
67
|
return;
|
|
68
|
+
} else if (error instanceof GenerationalError) {
|
|
69
|
+
this.logger.info('process-event-generational-job-error', { error });
|
|
70
|
+
return;
|
|
68
71
|
} else if (error instanceof GetStateError) {
|
|
69
72
|
this.logger.error('cycle-get-state-error', { error });
|
|
70
73
|
return;
|
|
@@ -75,7 +78,7 @@ class Cycle extends Activity {
|
|
|
75
78
|
throw error;
|
|
76
79
|
} finally {
|
|
77
80
|
telemetry?.endActivitySpan();
|
|
78
|
-
this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
81
|
+
this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -95,15 +98,16 @@ class Cycle extends Activity {
|
|
|
95
98
|
const streamData: StreamData = {
|
|
96
99
|
metadata: {
|
|
97
100
|
guid: guid(),
|
|
98
|
-
dad: CollatorService.resolveReentryDimension(this),
|
|
99
101
|
jid: this.context.metadata.jid,
|
|
102
|
+
gid: this.context.metadata.gid,
|
|
103
|
+
dad: CollatorService.resolveReentryDimension(this),
|
|
100
104
|
aid: this.config.ancestor,
|
|
101
105
|
spn: this.context['$self'].output.metadata?.l1s,
|
|
102
106
|
trc: this.context.metadata.trc,
|
|
103
107
|
},
|
|
104
108
|
data: this.context.data
|
|
105
109
|
};
|
|
106
|
-
return (await this.engine.
|
|
110
|
+
return (await this.engine.router?.publishMessage(null, streamData, multi)) as string;
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|