@webda/async 4.0.0-beta.1 → 4.0.0-beta.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 (41) hide show
  1. package/README.md +89 -4
  2. package/lib/{models.d.ts → asyncaction.model.d.ts} +76 -42
  3. package/lib/asyncaction.model.d.ts.map +1 -0
  4. package/lib/asyncaction.model.js +269 -0
  5. package/lib/asyncaction.model.js.map +1 -0
  6. package/lib/index.d.ts +7 -4
  7. package/lib/index.d.ts.map +1 -0
  8. package/lib/index.js +6 -4
  9. package/lib/index.js.map +1 -1
  10. package/lib/services/asyncevents.service.d.ts +154 -0
  11. package/lib/services/asyncevents.service.d.ts.map +1 -0
  12. package/lib/services/asyncevents.service.js +219 -0
  13. package/lib/services/asyncevents.service.js.map +1 -0
  14. package/lib/services/{asyncjobservice.d.ts → asyncjobservice.service.d.ts} +79 -74
  15. package/lib/services/asyncjobservice.service.d.ts.map +1 -0
  16. package/lib/services/asyncjobservice.service.js +584 -0
  17. package/lib/services/asyncjobservice.service.js.map +1 -0
  18. package/lib/services/{localrunner.d.ts → localrunner.service.d.ts} +24 -9
  19. package/lib/services/localrunner.service.d.ts.map +1 -0
  20. package/lib/services/localrunner.service.js +87 -0
  21. package/lib/services/localrunner.service.js.map +1 -0
  22. package/lib/services/{runner.d.ts → runner.service.d.ts} +17 -11
  23. package/lib/services/runner.service.d.ts.map +1 -0
  24. package/lib/services/{runner.js → runner.service.js} +16 -13
  25. package/lib/services/runner.service.js.map +1 -0
  26. package/lib/services/servicerunner.service.d.ts +105 -0
  27. package/lib/services/servicerunner.service.d.ts.map +1 -0
  28. package/lib/services/servicerunner.service.js +131 -0
  29. package/lib/services/servicerunner.service.js.map +1 -0
  30. package/package.json +33 -37
  31. package/webda.module.json +1895 -857
  32. package/lib/models.js +0 -177
  33. package/lib/models.js.map +0 -1
  34. package/lib/services/asyncjobservice.js +0 -505
  35. package/lib/services/asyncjobservice.js.map +0 -1
  36. package/lib/services/localrunner.js +0 -70
  37. package/lib/services/localrunner.js.map +0 -1
  38. package/lib/services/runner.js.map +0 -1
  39. package/lib/services/servicerunner.d.ts +0 -67
  40. package/lib/services/servicerunner.js +0 -97
  41. package/lib/services/servicerunner.js.map +0 -1
package/lib/models.js DELETED
@@ -1,177 +0,0 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { Action, Core, CoreModel, WebContext, WebdaError } from "@webda/core";
8
- import * as crypto from "crypto";
9
- /**
10
- * Define here a model that can be used along with Store service
11
- * @WebdaModel
12
- */
13
- export default class AsyncAction extends CoreModel {
14
- /**
15
- * Set type
16
- */
17
- constructor() {
18
- super();
19
- this.type = this.constructor.name;
20
- }
21
- /**
22
- * By default AsyncAction are not considered internal
23
- *
24
- * Internal: That execute a method or operation of Webda application framework
25
- * @returns
26
- */
27
- isInternal() {
28
- return false;
29
- }
30
- getLogsLimit() {
31
- return 1000;
32
- }
33
- /**
34
- * Allow to report status for a job
35
- * @param context
36
- */
37
- async statusAction(context) {
38
- await this.update(await context.getRequestBody(), context.getHttpContext().getUniqueHeader("X-Job-Time"));
39
- context.write({ ...this, logs: undefined, statusDetails: undefined });
40
- }
41
- /**
42
- * Return the hook url if empty fallback to the service url
43
- * @returns
44
- */
45
- getHookUrl() {
46
- return Core.get().getApiUrl(`${Core.get().getRouter().getModelUrl(this)}/${this.uuid}`);
47
- }
48
- /**
49
- *
50
- * @param body
51
- */
52
- async update(body, time = undefined) {
53
- if (body.logs && Array.isArray(body.logs)) {
54
- this.logs ?? (this.logs = []);
55
- this.logs.push(...body.logs);
56
- // Prevent having too much logs
57
- if (this.logs.length > this.getLogsLimit()) {
58
- this.logs = this.logs.slice(-1 * this.getLogsLimit());
59
- }
60
- }
61
- this._lastJobUpdate = Number.parseInt(time || "0") || 0;
62
- if (Date.now() - this._lastJobUpdate > 60) {
63
- this._lastJobUpdate = Date.now();
64
- }
65
- await this.patch({
66
- _lastJobUpdate: this._lastJobUpdate,
67
- logs: this.logs,
68
- status: body.status || this.status,
69
- errorMessage: body.errorMessage || this.errorMessage,
70
- statusDetails: body.statusDetails || this.statusDetails,
71
- results: body.results || this.results
72
- });
73
- return this;
74
- }
75
- /**
76
- *
77
- * @param context
78
- * @param action
79
- */
80
- async verifyJobRequest(context) {
81
- const jobTime = context.getHttpContext().getUniqueHeader("X-Job-Time");
82
- const jobHash = context.getHttpContext().getUniqueHeader("X-Job-Hash");
83
- // Ensure hash mac is correct
84
- if (jobHash !== this.getHmac(jobTime)) {
85
- context.log("TRACE", "Invalid Job HMAC");
86
- throw new WebdaError.Forbidden("Invalid Job HMAC");
87
- }
88
- // Set the context extension
89
- context.setExtension("asyncJob", this);
90
- }
91
- /**
92
- * Get the hmac for the job
93
- * @param jobTime
94
- * @returns
95
- */
96
- getHmac(jobTime) {
97
- return crypto.createHmac("sha256", this.__secretKey).update(jobTime).digest("hex");
98
- }
99
- /**
100
- * Ensure action hook is ok
101
- * @param context
102
- * @param _action
103
- * @returns
104
- */
105
- async checkAct(context, action) {
106
- // Only action runner can call status action
107
- if ("status" === action) {
108
- if (context instanceof WebContext) {
109
- if (context.getHttpContext().getUniqueHeader("X-Job-Hash") &&
110
- context.getHttpContext().getUniqueHeader("X-Job-Time")) {
111
- return await this.verifyJobRequest(context);
112
- }
113
- }
114
- throw new WebdaError.Forbidden("Only Job runner can call this action");
115
- }
116
- // Allow to retrieve/update binaries by action runner
117
- if (["get_binary", "attach_binary", "update_binary_metadata"].includes(action) &&
118
- context.getHttpContext()?.getUniqueHeader("X-Job-Hash") &&
119
- context.getHttpContext()?.getUniqueHeader("X-Job-Time")) {
120
- console.log(action, context.getHttpContext()?.getUniqueHeader("X-Job-Hash"), context.getHttpContext()?.getUniqueHeader("X-Job-Time"));
121
- return await this.verifyJobRequest(context);
122
- }
123
- return super.checkAct(context, action);
124
- }
125
- }
126
- __decorate([
127
- Action({ name: "status", openapi: { hidden: true } })
128
- ], AsyncAction.prototype, "statusAction", null);
129
- /**
130
- * Define a Webda Async Action
131
- *
132
- * @WebdaModel
133
- */
134
- export class AsyncWebdaAction extends AsyncAction {
135
- /**
136
- *
137
- * @param serviceName service to call
138
- * @param method method to call
139
- * @param arguments to call with the method
140
- */
141
- constructor(serviceName, method, ...args) {
142
- super();
143
- this.serviceName = serviceName;
144
- this.method = method;
145
- this.logLevel = "INFO";
146
- this.arguments = args;
147
- }
148
- /**
149
- * Execute a serviceName.method(...args) so this is internal
150
- * @returns
151
- */
152
- isInternal() {
153
- return true;
154
- }
155
- }
156
- /**
157
- * Operation called asynchronously
158
- *
159
- * @WebdaModel
160
- */
161
- export class AsyncOperationAction extends AsyncAction {
162
- constructor(operationId, context, logLevel = "INFO") {
163
- super();
164
- this.operationId = operationId;
165
- this.context = context;
166
- this.logLevel = logLevel;
167
- }
168
- /**
169
- * Execute a webda.callOperation(context ,id) so this is internal
170
- * @returns
171
- */
172
- isInternal() {
173
- return true;
174
- }
175
- }
176
- export { AsyncAction };
177
- //# sourceMappingURL=models.js.map
package/lib/models.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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"}
@@ -1,505 +0,0 @@
1
- import { CancelableLoopPromise, CancelablePromise, Core, CronService, FileUtils, JSONUtils, OperationError, Service, ServiceParameters, SimpleOperationContext, WebdaError } from "@webda/core";
2
- import axios from "axios";
3
- import * as crypto from "crypto";
4
- import { schedule as crontabSchedule } from "node-cron";
5
- import { Runner } from "./runner.js";
6
- import ServiceRunner from "./servicerunner.js";
7
- /**
8
- * @inheritdoc
9
- */
10
- export class AsyncJobServiceParameters extends ServiceParameters {
11
- constructor(params) {
12
- super(params);
13
- this.url ?? (this.url = "/async/jobs");
14
- this.queue ?? (this.queue = "AsyncActionsQueue");
15
- this.runners ?? (this.runners = []);
16
- this.schedulerResolution ?? (this.schedulerResolution = 60000);
17
- this.onlyHttpHook ?? (this.onlyHttpHook = false);
18
- this.includeCron ?? (this.includeCron = true);
19
- this.logsLimit ?? (this.logsLimit = 500);
20
- this.asyncActionModel ?? (this.asyncActionModel = "Webda/AsyncWebdaAction");
21
- this.asyncOperationModel ?? (this.asyncOperationModel = "Webda/AsyncOperationAction");
22
- }
23
- }
24
- /**
25
- * AsyncService allows you to launch jobs and report status
26
- *
27
- * @WebdaModda
28
- */
29
- class AsyncJobService extends Service {
30
- constructor() {
31
- super(...arguments);
32
- /**
33
- * Validator of sessions
34
- */
35
- this.operationsQueries = {};
36
- }
37
- /**
38
- * @inheritdoc
39
- */
40
- loadParameters(params) {
41
- return new AsyncJobServiceParameters(params);
42
- }
43
- /**
44
- * @inheritdoc
45
- */
46
- resolve() {
47
- super.resolve();
48
- this.model = this.getWebda().getModel(this.parameters.asyncActionModel);
49
- this.queue = this.getService(this.parameters.queue);
50
- if (!this.queue && !this.parameters.localLaunch) {
51
- throw new Error(`AsyncService requires a valid queue. '${this.parameters.queue}' is invalid`);
52
- }
53
- // Get all runners
54
- this.runners = this.parameters.runners
55
- .map(n => {
56
- const res = this.getService(n);
57
- if (res === undefined) {
58
- this.log("WARN", `Runner ${n} does not exist`);
59
- }
60
- return res;
61
- })
62
- .filter(r => r !== undefined);
63
- // Add route for operations launch
64
- if (this.parameters.asyncOperationDefinition) {
65
- this.log("INFO", "Loading operations", this.parameters.asyncOperationDefinition);
66
- this.addRoute(`${this.parameters.url}{?full}`, ["GET"], this.listOperations);
67
- this.addRoute(`${this.parameters.url}/{operationId}{?schedule}`, ["PUT"], this.launchOperation);
68
- this.operationModel = this.getWebda().getModel(this.parameters.asyncOperationModel);
69
- this.registerOperations(FileUtils.load(this.parameters.asyncOperationDefinition));
70
- }
71
- // This is internal job reporting so no need to document the api
72
- this.addRoute(`${this.parameters.url}/status`, ["POST"], this.statusHook, {
73
- hidden: true
74
- });
75
- // Add upload/download route if needed
76
- if (this.parameters.binaryStore) {
77
- this.binaryStore = this.getService(this.parameters.binaryStore);
78
- // Call directly Webda addRoute as we are setting a route for another service in fact
79
- this.getWebda().addRoute(`${this.parameters.url}/download/{store}/{uuid}/{property}/{index}`, {
80
- _method: ctx => this.binaryStore.httpGet(ctx),
81
- executor: this.parameters.binaryStore,
82
- openapi: { hidden: true },
83
- methods: ["GET"]
84
- });
85
- this.getWebda().addRoute(`${this.parameters.url}/upload/{store}/{uuid}/{property}`, {
86
- _method: ctx => this.binaryStore.httpChallenge(ctx),
87
- executor: this.parameters.binaryStore,
88
- openapi: { hidden: true },
89
- methods: ["PUT"]
90
- });
91
- }
92
- this.getWebda().registerRequestFilter(this);
93
- return this;
94
- }
95
- /**
96
- * Register new operations
97
- * @param operations
98
- */
99
- registerOperations(operations) {
100
- this.operations ?? (this.operations = {
101
- application: operations.application || { name: "Unknown", version: "0.0.0" },
102
- operations: {},
103
- schemas: {}
104
- });
105
- // Register all schemas
106
- Object.keys(operations?.schemas || {})
107
- .filter(key => !this.getWebda().getApplication().hasSchema(key))
108
- .forEach(key => {
109
- this.getWebda().getApplication().registerSchema(key, operations.schemas[key]);
110
- this.operations.schemas[key] = operations.schemas[key];
111
- });
112
- // Register all operations now
113
- Object.keys(operations?.operations || {}).forEach(key => {
114
- this.getWebda().registerOperation(key, {
115
- ...operations.operations[key],
116
- method: "callOperation",
117
- service: this.getName()
118
- });
119
- this.operations.operations[key] = operations.operations[key];
120
- });
121
- }
122
- /**
123
- * Allow job status report url
124
- */
125
- async checkRequest(context) {
126
- const url = context.getHttpContext().getRelativeUri();
127
- // Only for status endpoint - operations endpoint should still be authorized by something else
128
- if (url === `${this.parameters.url}/status`) {
129
- // Allow status url to be called without other mechanism
130
- return true;
131
- }
132
- return false;
133
- }
134
- /**
135
- * Status hook for job report
136
- *
137
- * Only updates specific fields: status, errorMessage, statusDetails, results, logs (appending)
138
- */
139
- async statusHook(context) {
140
- if (!context.getHttpContext().getUniqueHeader("X-Job-Id")) {
141
- throw new WebdaError.NotFound("X-Job-Id header required");
142
- }
143
- const action = await this.model.ref(context.getHttpContext().getUniqueHeader("X-Job-Id")).get();
144
- if (!action) {
145
- throw new WebdaError.NotFound(`Unknown Job Id '${context.getHttpContext().getUniqueHeader("X-Job-Id")}'`);
146
- }
147
- await action.checkAct(context, "status");
148
- await action.statusAction(context);
149
- }
150
- /**
151
- * Worker
152
- *
153
- * This will launch actions based on defined runners
154
- */
155
- worker() {
156
- if (this.runners.length === 0) {
157
- throw new Error(`AsyncJobService.worker requires runners`);
158
- }
159
- let p = [this.queue.consume(this.handleEvent.bind(this))];
160
- if (this.parameters.includeSchedulerInWorker) {
161
- p.push(this.scheduler());
162
- }
163
- // Return a cancelable promise for both worker and scheduler
164
- return new CancelablePromise((resolve, reject) => {
165
- Promise.all(p).then(() => resolve(), reject);
166
- }, async () => {
167
- this.log("INFO", "Worker stopped");
168
- await Promise.all(p.map(pi => (pi.cancel ? pi.cancel() : "")));
169
- });
170
- }
171
- /**
172
- * Handle one event from the queue and launch the job
173
- * @param event
174
- */
175
- async handleEvent(event) {
176
- let selectedRunner;
177
- // Take first to acknowledge the job
178
- for (let runner of this.runners) {
179
- if (runner.handleType(event.type)) {
180
- selectedRunner = runner;
181
- break;
182
- }
183
- }
184
- // Fallback if needed
185
- if (selectedRunner === undefined && this.parameters.fallbackOnFirst) {
186
- selectedRunner = this.runners[0];
187
- }
188
- if (selectedRunner === undefined) {
189
- this.log("ERROR", `Cannot find a runner for action ${event.uuid}`);
190
- await this.model.ref(event.uuid).patch({
191
- status: "ERROR",
192
- errorMessage: `No runner found for the job`
193
- });
194
- return;
195
- }
196
- this.log("INFO", `Starting action ${event.uuid}`);
197
- await this.model.ref(event.uuid).patch({
198
- uuid: event.uuid,
199
- status: "STARTING"
200
- });
201
- const action = await this.model.ref(event.uuid).get();
202
- let job = await selectedRunner.launchAction(action, this.getJobInfo(action));
203
- await action.patch({ job }, null);
204
- return job.promise || Promise.resolve();
205
- }
206
- /**
207
- * Get the job info
208
- * @param action
209
- * @returns
210
- */
211
- getJobInfo(action) {
212
- return {
213
- JOB_SECRET_KEY: action.__secretKey,
214
- JOB_ID: action.getUuid(),
215
- JOB_HOOK: this.parameters.onlyHttpHook || !action.isInternal()
216
- ? action.getHookUrl() || this.getWebda().getApiUrl(this.parameters.url)
217
- : "store", // How to find the absolute url
218
- JOB_ORCHESTRATOR: this.getName()
219
- };
220
- }
221
- /**
222
- * List available operations through this service
223
- * @param context
224
- */
225
- async listOperations(context) {
226
- let filtered = JSONUtils.duplicate(this.operations);
227
- // Filter operations based on permissions
228
- Object.keys(filtered.operations)
229
- .filter(key => filtered.operations[key].permission && !this.getWebda().checkOperationPermission(context, key))
230
- .forEach(key => delete filtered.operations[key]);
231
- // Remove permission definition
232
- Object.values(filtered.operations)
233
- .filter(def => def.permission)
234
- .forEach(def => delete def.permission);
235
- if (!context.getParameters().full) {
236
- context.write(Object.keys(filtered.operations));
237
- return;
238
- }
239
- const schemas = [];
240
- Object.values(filtered.operations).forEach((operation) => {
241
- if (operation.input) {
242
- schemas.push(operation.input);
243
- }
244
- if (operation.output) {
245
- schemas.push(operation.output);
246
- }
247
- });
248
- // Filter schemas
249
- Object.keys(filtered.schemas)
250
- .filter(key => !schemas.includes(key))
251
- .forEach(key => {
252
- delete filtered.schemas[key];
253
- });
254
- context.write(filtered);
255
- }
256
- /**
257
- * Call an operation for an external system
258
- * @param context
259
- * @returns
260
- */
261
- async callOperation(context) {
262
- return await this.launchAction(new this.operationModel(context.getExtension("operation"), context));
263
- }
264
- /**
265
- * Launch an operation through this service
266
- */
267
- async launchOperation(context) {
268
- const { operationId, schedule } = context.getParameters();
269
- try {
270
- await this.getWebda().checkOperation(context, operationId);
271
- }
272
- catch (err) {
273
- if (err instanceof OperationError) {
274
- if (err.type === "InvalidInput") {
275
- throw new WebdaError.BadRequest("Invalid Input");
276
- }
277
- else if (err.type === "PermissionDenied") {
278
- throw new WebdaError.Forbidden("Permission Denied");
279
- }
280
- else if (err.type === "Unknown") {
281
- throw new WebdaError.NotFound("Operation not found");
282
- }
283
- }
284
- else {
285
- throw err;
286
- }
287
- }
288
- let action;
289
- if (schedule) {
290
- action = await this.scheduleAction(new this.operationModel(operationId, await SimpleOperationContext.fromContext(context)), schedule);
291
- }
292
- else {
293
- action = await this.callOperation((await SimpleOperationContext.fromContext(context)).setExtension("operation", operationId));
294
- }
295
- context.write(action);
296
- }
297
- /**
298
- * Launch the action asynchronously
299
- * @param action
300
- * @returns
301
- */
302
- async launchAction(action) {
303
- action.status = "QUEUED";
304
- action.type = action.constructor.name;
305
- action.__secretKey = this.getWebda().getUuid();
306
- await action.save();
307
- if (this.parameters.localLaunch) {
308
- // Directly call the handler but not wait for its result
309
- this.handleEvent({ uuid: action.getUuid(), __secretKey: action.__secretKey, type: action.type });
310
- }
311
- else {
312
- await this.queue.sendMessage({ uuid: action.getUuid(), __secretKey: action.__secretKey, type: action.type });
313
- }
314
- return action;
315
- }
316
- /**
317
- * Schedule action for later execution
318
- *
319
- * @param action
320
- * @param timestamp
321
- */
322
- async scheduleAction(action, timestamp) {
323
- action.status = "SCHEDULED";
324
- action.type = action.constructor.name;
325
- // Schedule based on the scheduler resolution
326
- action.scheduled = timestamp - (timestamp % this.parameters.schedulerResolution);
327
- await action.save();
328
- return action;
329
- }
330
- /**
331
- * Return headers to request status hook
332
- *
333
- * @param jobInfo
334
- * @returns
335
- */
336
- getHeaders(jobInfo) {
337
- let res = {
338
- "X-Job-Id": jobInfo.JOB_ID,
339
- "X-Job-Time": Date.now().toString()
340
- };
341
- res["X-Job-Hash"] = crypto
342
- .createHmac(AsyncJobService.HMAC_ALGO, jobInfo.JOB_SECRET_KEY)
343
- .update(res["X-Job-Time"])
344
- .digest("hex");
345
- return res;
346
- }
347
- /**
348
- * Post hook to a remote url or use local store to update status
349
- * @param jobInfo
350
- * @param message
351
- * @returns
352
- */
353
- async postHook(jobInfo, message) {
354
- // Http allow to break paradigm between the executor and the orchestrator
355
- if (jobInfo.JOB_HOOK.startsWith("http")) {
356
- return (await axios.post(jobInfo.JOB_HOOK, message, {
357
- headers: this.getHeaders(jobInfo)
358
- })).data;
359
- }
360
- else if (jobInfo.JOB_HOOK === "store") {
361
- // If executor and orchestrator runs within same privilege it simplify the infrastructure
362
- return (await this.model.ref(jobInfo.JOB_ID).get()).update(message);
363
- }
364
- }
365
- /**
366
- * Launch a service.method as an AsyncAction
367
- * @param serviceName
368
- * @param method
369
- * @param args
370
- */
371
- async launchAsAsyncAction(serviceName, method, ...args) {
372
- return this.launchAction(new this.model(serviceName, method, ...args));
373
- }
374
- /**
375
- * Execute a service.method as an AsyncAction
376
- *
377
- * Useful for crontab execution
378
- * @param serviceName
379
- * @param method
380
- * @param args
381
- */
382
- async executeAsAsyncAction(serviceName, method, ...args) {
383
- let runner = Object.values(this.getWebda().getServicesOfType(ServiceRunner)).shift();
384
- // Create a temporary one if needed
385
- runner ?? (runner = await new ServiceRunner(this.getWebda(), this.getName() + "_temprunner").resolve().init());
386
- // Save action
387
- const action = await new this.model(serviceName, method, ...args).save();
388
- // Run it
389
- return (await runner.launchAction(action, this.getJobInfo(action))).promise;
390
- }
391
- /**
392
- * Wrap async job and call the job status hook
393
- */
394
- async runAsyncOperationAction(jobInfo) {
395
- // Get it from environment
396
- if (jobInfo === undefined) {
397
- jobInfo = {};
398
- Object.keys(process.env)
399
- .filter(k => k.startsWith("JOB_"))
400
- // @ts-ignore
401
- .forEach((k) => (jobInfo[k] = process.env[k]));
402
- }
403
- // Ensure correct context
404
- if (!jobInfo.JOB_ORCHESTRATOR || !jobInfo.JOB_ID || !jobInfo.JOB_SECRET_KEY || !jobInfo.JOB_HOOK) {
405
- this.log("ERROR", "Cannot run AsyncAction without context");
406
- throw new Error("Cannot run AsyncAction without context");
407
- }
408
- // If we are not the target redirect to the right one
409
- if (jobInfo.JOB_ORCHESTRATOR !== this.getName()) {
410
- this.log("DEBUG", `Passing jobInfo ${jobInfo} to targeted service`);
411
- return this.getService(jobInfo.JOB_ORCHESTRATOR).runAsyncOperationAction(jobInfo);
412
- }
413
- this.log("DEBUG", "Getting action to execute from hook", jobInfo);
414
- // Get action info by calling the hook
415
- let action = await this.postHook(jobInfo, {
416
- agent: {
417
- ...Runner.getAgentInfo(),
418
- nodeVersion: process.version
419
- },
420
- status: "RUNNING"
421
- });
422
- this.log("DEBUG", "Action received", action.serviceName, action.method, action.arguments);
423
- let results;
424
- try {
425
- // Check it contains the right info
426
- if (!action.method || !action.serviceName) {
427
- throw new Error("WebdaAsyncAction must have method and serviceName defined at least");
428
- }
429
- // Call the service[method](...args)
430
- let service = this.getService(action.serviceName);
431
- if (!service) {
432
- throw new Error(`WebdaAsyncAction Service '${action.serviceName}' not found: mismatch app version`);
433
- }
434
- // @ts-ignore
435
- if (!service[action.method]) {
436
- throw new Error(`WebdaAsyncAction Method '${action.method}' not found in service ${action.serviceName}: mismatch app version`);
437
- }
438
- // @ts-ignore
439
- results = await service[action.method](...(action.arguments || []));
440
- }
441
- catch (err) {
442
- // Job is in error
443
- await this.postHook(jobInfo, {
444
- errorMessage: err?.message,
445
- status: "ERROR"
446
- });
447
- return;
448
- }
449
- // Update status
450
- await this.postHook(jobInfo, {
451
- results,
452
- status: "SUCCESS"
453
- });
454
- }
455
- /**
456
- * Get the cron callback function
457
- * @param cron
458
- * @returns
459
- */
460
- getCronExecutor(cron) {
461
- return async () => {
462
- try {
463
- this.log("INFO", `Execute cron ${cron.cron}: ${cron.serviceName}.${cron.method}(...) # ${cron.description} as an AsyncOperationAction`);
464
- await this.launchAction(new this.model(cron.serviceName, cron.method, cron.args));
465
- }
466
- catch (err) {
467
- this.log("ERROR", `Execution error cron ${cron.cron}: ${cron.serviceName}.${cron.method}(...) # ${cron.description} - ${err}`);
468
- }
469
- };
470
- }
471
- /**
472
- * Manage scheduled actions and crontab actions
473
- * @returns
474
- */
475
- scheduler() {
476
- // Map cron to an AsyncOperationAction
477
- // It allows you to keep a trace of the cron execution in the AsyncAction
478
- if (this.parameters.includeCron) {
479
- CronService.loadAnnotations(this._webda.getServices()).forEach(cron => {
480
- this.log("INFO", `Schedule cron ${cron.cron}: ${cron.serviceName}.${cron.method}(...) # ${cron.description}`);
481
- crontabSchedule(cron.cron, this.getCronExecutor(cron));
482
- });
483
- }
484
- // Every schedulerResolution will check for scheduled task
485
- return new CancelableLoopPromise(async () => {
486
- let time = Date.now();
487
- time -= time % this.parameters.schedulerResolution;
488
- // Queue all actions
489
- await Promise.all((await this.model.query(`status = 'SCHEDULED' AND scheduled < ${time + 1}`)).results.map(a => this.launchAction(a)));
490
- time += this.parameters.schedulerResolution;
491
- // Wait for next scheduler resolution
492
- if (time > Date.now()) {
493
- await Core.sleep(time - Date.now());
494
- /* c8 ignore next */
495
- }
496
- });
497
- }
498
- }
499
- /**
500
- * HMAC Algorithm used for simple auth
501
- */
502
- AsyncJobService.HMAC_ALGO = "sha256";
503
- export default AsyncJobService;
504
- export { AsyncJobService };
505
- //# sourceMappingURL=asyncjobservice.js.map