teraslice 0.87.1 → 0.88.0

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 (69) hide show
  1. package/cluster-service.js +24 -18
  2. package/dist/src/index.js +42 -0
  3. package/package.json +11 -15
  4. package/service.js +4 -6
  5. package/worker-service.js +6 -6
  6. package/index.js +0 -21
  7. package/lib/cluster/cluster_master.js +0 -164
  8. package/lib/cluster/node_master.js +0 -393
  9. package/lib/cluster/services/api.js +0 -581
  10. package/lib/cluster/services/assets.js +0 -211
  11. package/lib/cluster/services/cluster/backends/kubernetes/deployments/worker.hbs +0 -86
  12. package/lib/cluster/services/cluster/backends/kubernetes/index.js +0 -225
  13. package/lib/cluster/services/cluster/backends/kubernetes/jobs/execution_controller.hbs +0 -69
  14. package/lib/cluster/services/cluster/backends/kubernetes/k8s.js +0 -450
  15. package/lib/cluster/services/cluster/backends/kubernetes/k8sResource.js +0 -443
  16. package/lib/cluster/services/cluster/backends/kubernetes/k8sState.js +0 -67
  17. package/lib/cluster/services/cluster/backends/kubernetes/utils.js +0 -58
  18. package/lib/cluster/services/cluster/backends/native/index.js +0 -611
  19. package/lib/cluster/services/cluster/backends/native/messaging.js +0 -563
  20. package/lib/cluster/services/cluster/backends/state-utils.js +0 -49
  21. package/lib/cluster/services/cluster/index.js +0 -15
  22. package/lib/cluster/services/execution.js +0 -459
  23. package/lib/cluster/services/jobs.js +0 -303
  24. package/lib/config/default-sysconfig.js +0 -47
  25. package/lib/config/index.js +0 -32
  26. package/lib/config/schemas/system.js +0 -333
  27. package/lib/processors/save_file/index.js +0 -9
  28. package/lib/processors/save_file/processor.js +0 -17
  29. package/lib/processors/save_file/schema.js +0 -17
  30. package/lib/processors/script.js +0 -130
  31. package/lib/processors/stdout/index.js +0 -9
  32. package/lib/processors/stdout/processor.js +0 -19
  33. package/lib/processors/stdout/schema.js +0 -18
  34. package/lib/storage/analytics.js +0 -106
  35. package/lib/storage/assets.js +0 -275
  36. package/lib/storage/backends/elasticsearch_store.js +0 -567
  37. package/lib/storage/backends/mappings/analytics.json +0 -49
  38. package/lib/storage/backends/mappings/asset.json +0 -40
  39. package/lib/storage/backends/mappings/ex.json +0 -55
  40. package/lib/storage/backends/mappings/job.json +0 -31
  41. package/lib/storage/backends/mappings/state.json +0 -37
  42. package/lib/storage/execution.js +0 -331
  43. package/lib/storage/index.js +0 -16
  44. package/lib/storage/jobs.js +0 -97
  45. package/lib/storage/state.js +0 -302
  46. package/lib/utils/api_utils.js +0 -173
  47. package/lib/utils/asset_utils.js +0 -117
  48. package/lib/utils/date_utils.js +0 -58
  49. package/lib/utils/encoding_utils.js +0 -29
  50. package/lib/utils/events.js +0 -7
  51. package/lib/utils/file_utils.js +0 -118
  52. package/lib/utils/id_utils.js +0 -19
  53. package/lib/utils/port_utils.js +0 -83
  54. package/lib/workers/assets/loader.js +0 -109
  55. package/lib/workers/assets/spawn.js +0 -78
  56. package/lib/workers/context/execution-context.js +0 -16
  57. package/lib/workers/context/terafoundation-context.js +0 -10
  58. package/lib/workers/execution-controller/execution-analytics.js +0 -211
  59. package/lib/workers/execution-controller/index.js +0 -1033
  60. package/lib/workers/execution-controller/recovery.js +0 -188
  61. package/lib/workers/execution-controller/scheduler.js +0 -461
  62. package/lib/workers/execution-controller/slice-analytics.js +0 -115
  63. package/lib/workers/helpers/job.js +0 -93
  64. package/lib/workers/helpers/op-analytics.js +0 -22
  65. package/lib/workers/helpers/terafoundation.js +0 -43
  66. package/lib/workers/helpers/worker-shutdown.js +0 -187
  67. package/lib/workers/metrics/index.js +0 -139
  68. package/lib/workers/worker/index.js +0 -344
  69. package/lib/workers/worker/slice.js +0 -143
@@ -1,581 +0,0 @@
1
- 'use strict';
2
-
3
- const { Router } = require('express');
4
- const bodyParser = require('body-parser');
5
- const { pipeline: streamPipeline } = require('node:stream/promises');
6
- const { RecoveryCleanupType } = require('@terascope/job-components');
7
- const {
8
- parseErrorInfo, parseList, logError, TSError, startsWith
9
- } = require('@terascope/utils');
10
- const { makeLogger } = require('../../workers/helpers/terafoundation');
11
- const {
12
- makePrometheus,
13
- isPrometheusRequest,
14
- makeTable,
15
- sendError,
16
- handleRequest,
17
- getSearchOptions,
18
- } = require('../../utils/api_utils');
19
- const terasliceVersion = require('../../../package.json').version;
20
-
21
- let gotESMModule;
22
-
23
- async function getGotESM() {
24
- if (gotESMModule) return gotESMModule;
25
- const module = await import('gotESM'); // eslint-disable-line
26
- gotESMModule = module.default;
27
- return module.default;
28
- }
29
-
30
- module.exports = function apiService(context, { assetsUrl, app }) {
31
- const clusterConfig = context.sysconfig.teraslice;
32
- const clusterType = clusterConfig.cluster_manager_type;
33
-
34
- const logger = makeLogger(context, 'api_service');
35
-
36
- let available = false;
37
- let executionService;
38
- let jobsService;
39
- let stateStore;
40
- let clusterService;
41
- let exStore;
42
- let jobStore;
43
-
44
- const v1routes = new Router();
45
-
46
- app.use(bodyParser.json({
47
- type(req) {
48
- return (req.headers['content-type'] === 'application/json' || req.headers['content-type'] === 'application/x-www-form-urlencoded');
49
- }
50
- }));
51
-
52
- app.use((err, req, res, next) => {
53
- if (err instanceof SyntaxError) {
54
- sendError(res, 400, 'the json submitted is malformed');
55
- } else {
56
- next();
57
- }
58
- });
59
-
60
- app.use((req, res, next) => {
61
- if (!available) {
62
- res.json({ error: 'api is not available' });
63
- return;
64
- }
65
- req.logger = logger;
66
- next();
67
- });
68
-
69
- app.set('json spaces', 4);
70
-
71
- v1routes.get('/', (req, res) => {
72
- const requestHandler = handleRequest(req, res);
73
- requestHandler(() => ({
74
- arch: context.arch,
75
- clustering_type: context.sysconfig.teraslice.cluster_manager_type,
76
- name: context.sysconfig.teraslice.name,
77
- node_version: process.version,
78
- platform: context.platform,
79
- teraslice_version: `v${terasliceVersion}`
80
- }));
81
- });
82
-
83
- v1routes.get('/cluster/state', (req, res) => {
84
- const requestHandler = handleRequest(req, res);
85
- requestHandler(() => clusterService.getClusterState());
86
- });
87
-
88
- v1routes.route('/assets*')
89
- .delete((req, res) => {
90
- _redirect(req, res);
91
- })
92
- .post((req, res) => {
93
- if (req.headers['content-type'] === 'application/json' || req.headers['content-type'] === 'application/x-www-form-urlencoded') {
94
- sendError(res, 400, '/asset endpoints do not accept json');
95
- return;
96
- }
97
- _redirect(req, res);
98
- })
99
- .get(_redirect);
100
-
101
- v1routes.post('/jobs', (req, res) => {
102
- // if no job was posted an empty object is returned, so we check if it has values
103
- if (!req.body.operations) {
104
- sendError(res, 400, 'No job was posted');
105
- return;
106
- }
107
-
108
- const { start } = req.query;
109
- const jobSpec = req.body;
110
- const shouldRun = `${start}` !== 'false';
111
-
112
- const requestHandler = handleRequest(req, res, 'Job submission failed');
113
- requestHandler(() => jobsService.submitJob(jobSpec, shouldRun));
114
- });
115
-
116
- v1routes.get('/jobs', (req, res) => {
117
- let query;
118
- const { size, from, sort } = getSearchOptions(req);
119
-
120
- if (req.query.active === 'true') {
121
- query = 'job_id:* AND !active:false';
122
- } else if (req.query.active === 'false') {
123
- query = 'job_id:* AND active:false';
124
- } else {
125
- query = 'job_id:*';
126
- }
127
-
128
- const requestHandler = handleRequest(req, res, 'Could not retrieve list of jobs');
129
- requestHandler(() => jobStore.search(query, from, size, sort));
130
- });
131
-
132
- v1routes.get('/jobs/:jobId', (req, res) => {
133
- const { jobId } = req.params;
134
-
135
- const requestHandler = handleRequest(req, res, 'Could not retrieve job');
136
- requestHandler(async () => jobStore.get(jobId));
137
- });
138
-
139
- v1routes.put('/jobs/:jobId', (req, res) => {
140
- const { jobId } = req.params;
141
- const jobSpec = req.body;
142
-
143
- if (Object.keys(jobSpec).length === 0) {
144
- sendError(res, 400, `no data was provided to update job ${jobId}`);
145
- return;
146
- }
147
-
148
- const requestHandler = handleRequest(req, res, 'Could not update job');
149
- requestHandler(async () => jobsService.updateJob(jobId, jobSpec));
150
- });
151
-
152
- v1routes.get('/jobs/:jobId/ex', (req, res) => {
153
- const { jobId } = req.params;
154
-
155
- const requestHandler = handleRequest(req, res, 'Could not retrieve list of execution contexts');
156
- requestHandler(async () => jobsService.getLatestExecution(jobId));
157
- });
158
-
159
- v1routes.post('/jobs/:jobId/_active', (req, res) => {
160
- const { jobId } = req.params;
161
-
162
- const requestHandler = handleRequest(req, res, `Could not change active to 'true' for job: ${jobId}`);
163
- requestHandler(async () => jobsService.setActiveState(jobId, true));
164
- });
165
-
166
- v1routes.post('/jobs/:jobId/_inactive', (req, res) => {
167
- const { jobId } = req.params;
168
-
169
- const requestHandler = handleRequest(req, res, `Could not change active to 'false' for job: ${jobId}`);
170
- requestHandler(async () => jobsService.setActiveState(jobId, false));
171
- });
172
-
173
- v1routes.post('/jobs/:jobId/_start', (req, res) => {
174
- const { jobId } = req.params;
175
-
176
- const requestHandler = handleRequest(req, res, `Could not start job: ${jobId}`);
177
- requestHandler(async () => jobsService.startJob(jobId));
178
- });
179
-
180
- v1routes.post(['/jobs/:jobId/_stop', '/ex/:exId/_stop'], (req, res) => {
181
- const { timeout, blocking = true } = req.query;
182
-
183
- const requestHandler = handleRequest(req, res, 'Could not stop execution');
184
- requestHandler(async () => {
185
- const exId = await _getExIdFromRequest(req);
186
- await executionService.stopExecution(exId, timeout);
187
- return _waitForStop(exId, blocking);
188
- });
189
- });
190
-
191
- v1routes.post(['/jobs/:jobId/_pause', '/ex/:exId/_pause'], (req, res) => {
192
- const requestHandler = handleRequest(req, res, 'Could not pause execution');
193
- requestHandler(async () => {
194
- const exId = await _getExIdFromRequest(req);
195
- return executionService.pauseExecution(exId);
196
- });
197
- });
198
-
199
- v1routes.post(['/jobs/:jobId/_resume', '/ex/:exId/_resume'], (req, res) => {
200
- const requestHandler = handleRequest(req, res, 'Could not resume execution');
201
- requestHandler(async () => {
202
- const exId = await _getExIdFromRequest(req);
203
- return executionService.resumeExecution(exId);
204
- });
205
- });
206
-
207
- function validateCleanupType(cleanupType) {
208
- if (cleanupType && !RecoveryCleanupType[cleanupType]) {
209
- const types = Object.values(RecoveryCleanupType);
210
- throw new TSError(`cleanup_type must be empty or set to ${types.join(', ')}`, {
211
- statusCode: 400
212
- });
213
- }
214
- }
215
-
216
- v1routes.post('/jobs/:jobId/_recover', (req, res) => {
217
- const cleanupType = req.query.cleanup_type || req.query.cleanup;
218
- const { jobId } = req.params;
219
-
220
- const requestHandler = handleRequest(req, res, 'Could not recover job');
221
- requestHandler(async () => {
222
- validateCleanupType(cleanupType);
223
- return jobsService.recoverJob(jobId, cleanupType);
224
- });
225
- });
226
-
227
- v1routes.post('/ex/:exId/_recover', (req, res) => {
228
- const cleanupType = req.query.cleanup_type || req.query.cleanup;
229
- const { exId } = req.params;
230
-
231
- const requestHandler = handleRequest(req, res, 'Could not recover execution');
232
- requestHandler(async () => {
233
- validateCleanupType(cleanupType);
234
- return executionService.recoverExecution(exId, cleanupType);
235
- });
236
- });
237
-
238
- v1routes.post(['/jobs/:jobId/_workers', '/ex/:exId/_workers'], (req, res) => {
239
- const { query } = req;
240
-
241
- const requestHandler = handleRequest(req, res, 'Could not change workers count');
242
- requestHandler(async () => {
243
- const exId = await _getExIdFromRequest(req);
244
- const result = await _changeWorkers(exId, query);
245
- return { message: `${result.workerNum} workers have been ${result.action} for execution: ${result.ex_id}` };
246
- });
247
- });
248
-
249
- v1routes.get([
250
- '/jobs/:jobId/slicer',
251
- '/jobs/:jobId/controller',
252
- '/ex/:exId/slicer',
253
- '/ex/:exId/controller'
254
- ], (req, res) => {
255
- const requestHandler = handleRequest(req, res, 'Could not get slicer statistics');
256
- requestHandler(async () => {
257
- const exId = await _getExIdFromRequest(req);
258
- return _controllerStats(exId);
259
- });
260
- });
261
-
262
- v1routes.get([
263
- '/jobs/:jobId/errors',
264
- '/jobs/:jobId/errors/:exId',
265
- '/ex/:exId/errors',
266
- '/ex/errors',
267
- ], (req, res) => {
268
- const { size, from, sort } = getSearchOptions(req);
269
-
270
- const requestHandler = handleRequest(req, res, 'Could not get errors for job');
271
- requestHandler(async () => {
272
- const exId = await _getExIdFromRequest(req, true);
273
-
274
- const query = `state:error AND ex_id:"${exId}"`;
275
- return stateStore.search(query, from, size, sort);
276
- });
277
- });
278
-
279
- v1routes.get('/ex', (req, res) => {
280
- const { status = '' } = req.query;
281
- const { size, from, sort } = getSearchOptions(req);
282
-
283
- const requestHandler = handleRequest(req, res, 'Could not retrieve list of execution contexts');
284
- requestHandler(async () => {
285
- const statuses = parseList(status);
286
-
287
- let query = 'ex_id:*';
288
-
289
- if (statuses.length) {
290
- const statusTerms = statuses.map((s) => `_status:"${s}"`).join(' OR ');
291
- query += ` AND (${statusTerms})`;
292
- }
293
-
294
- return exStore.search(query, from, size, sort);
295
- });
296
- });
297
-
298
- v1routes.get('/ex/:exId', (req, res) => {
299
- const { exId } = req.params;
300
-
301
- const requestHandler = handleRequest(req, res, `Could not retrieve execution context ${exId}`);
302
- requestHandler(async () => executionService.getExecutionContext(exId));
303
- });
304
-
305
- v1routes.get('/cluster/stats', (req, res) => {
306
- const { name: cluster } = context.sysconfig.teraslice;
307
-
308
- const requestHandler = handleRequest(req, res, 'Could not get cluster statistics');
309
- requestHandler(async () => {
310
- const stats = await executionService.getClusterAnalytics();
311
-
312
- if (isPrometheusRequest(req)) return makePrometheus(stats, { cluster });
313
- // for backwards compatability (unsupported for prometheus)
314
- stats.slicer = stats.controllers;
315
- return stats;
316
- });
317
- });
318
-
319
- v1routes.get(['/cluster/slicers', '/cluster/controllers'], (req, res) => {
320
- const requestHandler = handleRequest(req, res, 'Could not get execution statistics');
321
- requestHandler(() => _controllerStats());
322
- });
323
-
324
- // backwards compatibility for /v1 routes
325
- app.use(v1routes);
326
- app.use('/v1', v1routes);
327
-
328
- app.route('/txt/assets*')
329
- .get(_redirect);
330
-
331
- app.get('/txt/workers', (req, res) => {
332
- const { size, from } = getSearchOptions(req);
333
- let defaults;
334
- if (clusterType === 'native') {
335
- defaults = ['assignment', 'job_id', 'ex_id', 'node_id', 'pid'];
336
- }
337
-
338
- if (clusterType === 'kubernetes') {
339
- defaults = ['assignment', 'job_id', 'ex_id', 'node_id', 'pod_name', 'image'];
340
- }
341
-
342
- const requestHandler = handleRequest(req, res, 'Could not get all workers');
343
- requestHandler(async () => {
344
- const workers = await executionService.findAllWorkers();
345
- return makeTable(req, defaults, workers.slice(from, size));
346
- });
347
- });
348
-
349
- app.get('/txt/nodes', (req, res) => {
350
- const { size, from } = getSearchOptions(req);
351
- const defaults = ['node_id', 'state', 'hostname', 'total', 'active', 'pid', 'teraslice_version', 'node_version'];
352
-
353
- const requestHandler = handleRequest(req, res, 'Could not get all nodes');
354
- requestHandler(async () => {
355
- const nodes = await clusterService.getClusterState();
356
-
357
- const transform = Object.values(nodes)
358
- .slice(from, size)
359
- .map((node) => Object.assign(
360
- {},
361
- node,
362
- { active: node.active.length }
363
- ));
364
-
365
- return makeTable(req, defaults, transform);
366
- });
367
- });
368
-
369
- app.get('/txt/jobs', (req, res) => {
370
- let query;
371
- const { size, from, sort } = getSearchOptions(req);
372
-
373
- const defaults = ['job_id', 'name', 'active', 'lifecycle', 'slicers', 'workers', '_created', '_updated'];
374
-
375
- if (req.query.active === 'true') {
376
- query = 'job_id:* AND !active:false';
377
- } else if (req.query.active === 'false') {
378
- query = 'job_id:* AND active:false';
379
- } else {
380
- query = 'job_id:*';
381
- }
382
-
383
- const requestHandler = handleRequest(req, res, 'Could not get all jobs');
384
- requestHandler(async () => {
385
- const jobs = await jobStore.search(query, from, size, sort);
386
- return makeTable(req, defaults, jobs);
387
- });
388
- });
389
-
390
- app.get('/txt/ex', (req, res) => {
391
- const { size, from, sort } = getSearchOptions(req);
392
-
393
- const defaults = ['name', 'lifecycle', 'slicers', 'workers', '_status', 'ex_id', 'job_id', '_created', '_updated'];
394
- const query = 'ex_id:*';
395
-
396
- const requestHandler = handleRequest(req, res, 'Could not get all executions');
397
- requestHandler(async () => {
398
- const exs = await exStore.search(query, from, size, sort);
399
- return makeTable(req, defaults, exs);
400
- });
401
- });
402
-
403
- app.get(['/txt/slicers', '/txt/controllers'], (req, res) => {
404
- const { size, from } = getSearchOptions(req);
405
-
406
- const defaults = [
407
- 'name',
408
- 'job_id',
409
- 'workers_available',
410
- 'workers_active',
411
- 'failed',
412
- 'queued',
413
- 'processed'
414
- ];
415
-
416
- const requestHandler = handleRequest(req, res, 'Could not get all execution statistics');
417
- requestHandler(async () => {
418
- const stats = await _controllerStats();
419
- return makeTable(req, defaults, stats.slice(from, size));
420
- });
421
- });
422
-
423
- // This is a catch all, any none supported api endpoints will return an error
424
- app.route('*')
425
- .all((req, res) => {
426
- sendError(res, 405, `cannot ${req.method} endpoint ${req.originalUrl}`);
427
- });
428
-
429
- async function _changeWorkers(exId, query) {
430
- let msg;
431
- let workerNum;
432
- const keyOptions = { add: true, remove: true, total: true };
433
- const queryKeys = Object.keys(query);
434
-
435
- if (!query) {
436
- throw new TSError('Must provide a query parameter in request', {
437
- statusCode: 400
438
- });
439
- }
440
-
441
- queryKeys.forEach((key) => {
442
- if (keyOptions[key]) {
443
- msg = key;
444
- workerNum = Number(query[key]);
445
- }
446
- });
447
-
448
- if (!msg || Number.isNaN(workerNum) || workerNum <= 0) {
449
- throw new TSError('Must provide a valid worker parameter(add/remove/total) that is a number and greater than zero', {
450
- statusCode: 400
451
- });
452
- }
453
-
454
- if (msg === 'add') {
455
- return executionService.addWorkers(exId, workerNum);
456
- }
457
-
458
- if (msg === 'remove') {
459
- return executionService.removeWorkers(exId, workerNum);
460
- }
461
-
462
- return executionService.setWorkers(exId, workerNum);
463
- }
464
-
465
- async function _getExIdFromRequest(req, allowWildcard = false) {
466
- const { path } = req;
467
- if (startsWith(path, '/ex')) {
468
- const { exId } = req.params;
469
- if (exId) return exId;
470
-
471
- if (allowWildcard) {
472
- return '*';
473
- }
474
- const error = new Error('Execution Context ID is required');
475
- error.code = 406;
476
- throw error;
477
- }
478
-
479
- if (startsWith(path, '/jobs')) {
480
- const { jobId } = req.params;
481
- const exId = await jobsService.getLatestExecutionId(jobId);
482
- if (!exId) {
483
- const error = new Error(`No executions were found for job: ${jobId}`);
484
- error.code = 404;
485
- throw error;
486
- }
487
- return exId;
488
- }
489
-
490
- const error = new Error('Only /ex and /jobs are allowed');
491
- error.code = 405;
492
- throw error;
493
- }
494
-
495
- async function _redirect(req, res) {
496
- const module = await getGotESM();
497
- const options = {
498
- prefixUrl: assetsUrl,
499
- headers: req.headers,
500
- searchParams: req.query,
501
- throwHttpErrors: false,
502
- timeout: { request: clusterConfig.api_response_timeout },
503
- decompress: false,
504
- retry: { limit: 0 }
505
- };
506
-
507
- const uri = req.url.replace(/^\//, '');
508
- const method = req.method.toLowerCase();
509
-
510
- try {
511
- await streamPipeline(
512
- req,
513
- module.stream[method](uri, options),
514
- res,
515
- );
516
- } catch (err) {
517
- const { statusCode, message } = parseErrorInfo(err, {
518
- defaultErrorMsg: 'Asset Service error while processing request'
519
- });
520
- sendError(res, statusCode, message, req.logger);
521
- }
522
- }
523
-
524
- async function _controllerStats(exId) {
525
- return executionService.getControllerStats(exId);
526
- }
527
-
528
- async function shutdown() {
529
- logger.info('shutting down api service');
530
- }
531
-
532
- async function initialize() {
533
- logger.info('api service is initializing...');
534
-
535
- stateStore = context.stores.state;
536
- exStore = context.stores.execution;
537
- jobStore = context.stores.jobs;
538
- if (stateStore == null || exStore == null || jobStore == null) {
539
- throw new Error('Missing required stores');
540
- }
541
-
542
- executionService = context.services.execution;
543
- jobsService = context.services.jobs;
544
- clusterService = context.services.cluster;
545
-
546
- if (jobsService == null || executionService == null || clusterService == null) {
547
- throw new Error('Missing required services');
548
- }
549
-
550
- available = true;
551
- }
552
-
553
- function _waitForStop(exId, blocking) {
554
- return new Promise((resolve) => {
555
- function checkExecution() {
556
- executionService.getExecutionContext(exId)
557
- .then((execution) => {
558
- const status = execution._status;
559
- const terminalList = exStore.getTerminalStatuses();
560
- const isTerminal = terminalList.find((tStat) => tStat === status);
561
- if (isTerminal || `${blocking}` !== 'true') {
562
- resolve({ status });
563
- } else {
564
- setTimeout(checkExecution, 3000);
565
- }
566
- })
567
- .catch((err) => {
568
- logError(logger, err, 'failure waiting for stop');
569
- setTimeout(checkExecution, 3000);
570
- });
571
- }
572
-
573
- checkExecution();
574
- });
575
- }
576
-
577
- return {
578
- initialize,
579
- shutdown,
580
- };
581
- };