@itentialopensource/adapter-utils 4.44.9

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.
@@ -0,0 +1,1289 @@
1
+ /* @copyright Itential, LLC 2018 */
2
+
3
+ // Set globals
4
+ /* global pronghornProps log */
5
+ /* eslint class-methods-use-this:warn */
6
+ /* eslint comma-dangle: ["error", "never"] */
7
+ /* eslint consistent-return:warn */
8
+ /* eslint no-loop-func:warn */
9
+ /* eslint no-param-reassign:warn */
10
+ /* eslint no-underscore-dangle: [2, { "allow": ["_id"] }] */
11
+
12
+ /* NodeJS internal API utilities */
13
+ const uuid = require('uuid');
14
+ const os = require('os');
15
+ const fs = require('fs');
16
+
17
+ /* Set up event emitter and other required references */
18
+ const AsyncLockCl = require('async-lock');
19
+
20
+ let transUtilInst = null;
21
+ let dbUtilInst = null;
22
+
23
+ // Global Variables - From Properties
24
+ let props = {};
25
+ let numberPhs = 1;
26
+ let syncAsync = 'sync';
27
+ let MaxInQueue = 1000;
28
+ let concurrentMax = 1;
29
+ let expireTimeout = 0;
30
+ let avgRuntime = 200;
31
+ const priorityTable = [];
32
+
33
+ // Other global variables
34
+ let id = null;
35
+ let phInstance = null;
36
+ let queueColl = '_queue';
37
+ const memQueue = [];
38
+ const memQlock = 0;
39
+ let avgQueue = [];
40
+ const avgQlock = 0;
41
+ let avgPtr = 1;
42
+ const avgSize = 25;
43
+ let avgTotal = 0;
44
+ let qlock = null;
45
+ // let MONGOQ;
46
+
47
+ /* THROTTLE ENGINE INTERNAL FUNCTIONS */
48
+ /*
49
+ * INTERNAL FUNCTION: getQueueItems is used to retrieve queue items from
50
+ * the database or memory based on the filter provided
51
+ */
52
+ function getQueueItems(dbUI, collectionName, filter, callback) {
53
+ const origin = `${id}-throttle-getQueueItems`;
54
+ log.spam(origin);
55
+
56
+ try {
57
+ let myFilter = filter;
58
+
59
+ if (myFilter === null || myFilter === undefined) {
60
+ myFilter = {};
61
+ }
62
+
63
+ // If the number of Pronghorns is greater than 1, the queue is in the database
64
+ if (numberPhs > 1) {
65
+ // actual call to retrieve the queue items from the database
66
+ // return MONGOQ.find(myFilter).toArray((qerror, queueItems) => {
67
+ return dbUI.find(collectionName, filter, null, false, (err, res) => {
68
+ if (err) {
69
+ const errorObj = transUtilInst.formatErrorObject(origin, 'Find Error', [myFilter, err], null, null, null);
70
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
71
+ return callback(null, errorObj);
72
+ }
73
+ return res.toArray((qerror, queueItems) => {
74
+ if (qerror) {
75
+ const errorObj = transUtilInst.formatErrorObject(origin, 'No Queue Item', [myFilter, qerror], null, null, null);
76
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
77
+ return callback(null, errorObj);
78
+ }
79
+ return callback(queueItems);
80
+ });
81
+ });
82
+ }
83
+
84
+ if (myFilter.transNum !== undefined && myFilter.transNum.$lte !== undefined) {
85
+ // If the number of Pronghorns is 1, the queue is in memory
86
+ // need to add capabilities to searches as needed
87
+ // currently - before me, active and all
88
+ // Lock the memQueue while getting the ones before me
89
+ return qlock.acquire(memQlock, (done) => {
90
+ const tempQueue = [];
91
+
92
+ // push all of the transactions before me into the return queue
93
+ for (let i = 0; i < memQueue.length; i += 1) {
94
+ if (memQueue[i].transNum <= myFilter.transNum.$lte) {
95
+ tempQueue.push(memQueue[i]);
96
+ } else {
97
+ break;
98
+ }
99
+ }
100
+
101
+ // return the temp queue
102
+ done(tempQueue);
103
+ }, (ret, error) => {
104
+ if (error) {
105
+ const errorObj = transUtilInst.formatErrorObject(origin, 'No Queue Item', [myFilter, error], null, null, null);
106
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
107
+ return callback(null, errorObj);
108
+ }
109
+
110
+ return callback(ret);
111
+ });
112
+ }
113
+
114
+ if (myFilter.active !== undefined && myFilter.active === true) {
115
+ // currently - active and all
116
+ // Lock the memQueue while finding the active items
117
+ return qlock.acquire(memQlock, (done) => {
118
+ const activeOnes = [];
119
+
120
+ for (let i = 0; i < memQueue.length; i += 1) {
121
+ if (memQueue[i].active === true) {
122
+ activeOnes.push(memQueue[i]);
123
+ }
124
+ }
125
+
126
+ done(activeOnes);
127
+ }, (ret, error) => {
128
+ if (error) {
129
+ const errorObj = transUtilInst.formatErrorObject(origin, 'No Queue Item', [myFilter, error], null, null, null);
130
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
131
+ return callback(null, errorObj);
132
+ }
133
+
134
+ return callback(ret);
135
+ });
136
+ }
137
+
138
+ // unsupported filter - just return entire queue
139
+ // Lock the memQueue while cloning it
140
+ return qlock.acquire(memQlock, (done) => {
141
+ // return a clone so it is current state and not modified as we are looking
142
+ const tempQueue = memQueue.slice();
143
+ done(tempQueue);
144
+ }, (ret, error) => {
145
+ if (error) {
146
+ const errorObj = transUtilInst.formatErrorObject(origin, 'No Queue Item', [myFilter, error], null, null, null);
147
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
148
+ return callback(null, errorObj);
149
+ }
150
+
151
+ return callback(ret);
152
+ });
153
+ } catch (e) {
154
+ // handle any exception
155
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue getting queue items');
156
+ return callback(null, errorObj);
157
+ }
158
+ }
159
+
160
+ /*
161
+ * INTERNAL FUNCTION: claimTurn is used to start the request and
162
+ * claim the turn. This means setting the start time and changing
163
+ * the running and active flags to true.
164
+ */
165
+ function claimTurn(dbUI, collectionName, queueItem, callback) {
166
+ const origin = `${id}-throttle-claimTurn`;
167
+ log.spam(origin);
168
+
169
+ try {
170
+ const cur = new Date();
171
+
172
+ // set up the update object to mark the queue item that the execution is starting
173
+ // set start, change active to true, change running to true
174
+ const data = {
175
+ _id: queueItem._id,
176
+ ph_instance: queueItem.ph_instance,
177
+ request_id: queueItem.request_id,
178
+ transNum: queueItem.transNum,
179
+ priority: queueItem.priority,
180
+ start: cur.getTime(),
181
+ end: queueItem.end,
182
+ active: true,
183
+ running: true,
184
+ event: queueItem.event
185
+ };
186
+
187
+ // If the number of Pronghorns is greater than 1, the queue is in the database
188
+ if (numberPhs > 1) {
189
+ // actual call to claim the turn from the queue in the database
190
+ // return MONGOQ.replaceOne({ _id: data._id }, data, (error, result) => {
191
+ return dbUI.replaceOne(collectionName, { _id: data._id }, data, {}, null, false, (error, result) => {
192
+ if (error) {
193
+ const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Claim Turn', [queueItem.request_id, queueItem.transNum, error], null, null, null);
194
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
195
+ return callback(null, errorObj);
196
+ }
197
+ log.debug(`${origin}: Mongo replace one returned ${result}`);
198
+ return callback(data);
199
+ });
200
+ }
201
+
202
+ // If the number of Pronghorns is 1, the queue is in memory
203
+ // Lock the memQueue while finding and updating the item
204
+ return qlock.acquire(memQlock, (done) => {
205
+ const index = memQueue.indexOf(queueItem);
206
+
207
+ if (index >= 0) {
208
+ memQueue[index] = data;
209
+ done(memQueue[index]);
210
+ } else {
211
+ done(null, 'Queue Item Not Found');
212
+ }
213
+ }, (ret, error) => {
214
+ if (error) {
215
+ const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Claim Turn', [queueItem.request_id, queueItem.transNum, error], null, null, null);
216
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
217
+ return callback(null, errorObj);
218
+ }
219
+
220
+ return callback(ret);
221
+ });
222
+ } catch (e) {
223
+ // handle any exception
224
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue claiming turn');
225
+ return callback(null, errorObj);
226
+ }
227
+ }
228
+
229
+ /*
230
+ * INTERNAL FUNCTION: freeQueueItem is used to free the turn from the given request.
231
+ * This is done by deleting the item from the database or removing it from the
232
+ * queue.
233
+ */
234
+ function freeQueueItem(dbUI, collectionName, queueItem, callback) {
235
+ const origin = `${id}-throttle-freeQueueItem`;
236
+ log.spam(origin);
237
+
238
+ try {
239
+ // If the number of Pronghorns is greater than 1, the queue is in the database
240
+ if (numberPhs > 1) {
241
+ // actual call to remove the request from the queue in the database
242
+ // return MONGOQ.deleteOne({ _id: queueItem._id }, (error, result) => {
243
+ return dbUI.delete(collectionName, { _id: queueItem._id }, {}, false, null, false, (error, result) => {
244
+ if (error) {
245
+ const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Free Turn', [queueItem.request_id, queueItem.transNum, error], null, null, null);
246
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
247
+ return callback(null, errorObj);
248
+ }
249
+ return callback(result);
250
+ });
251
+ }
252
+
253
+ // If the number of Pronghorns is 1, the queue is in memory
254
+ // Lock the memQueue while finding and removing the item
255
+ return qlock.acquire(memQlock, (done) => {
256
+ const index = memQueue.indexOf(queueItem);
257
+
258
+ if (index >= 0) {
259
+ memQueue.splice(index, 1);
260
+ done('successfully removed from queue');
261
+ } else {
262
+ done(null, 'Queue Item Not Found');
263
+ }
264
+ }, (ret, error) => {
265
+ if (error) {
266
+ const errorObj = transUtilInst.formatErrorObject(origin, 'Unable To Free Turn', [queueItem.request_id, queueItem.transNum, error], null, null, null);
267
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
268
+ return callback(null, errorObj);
269
+ }
270
+
271
+ return callback(ret);
272
+ });
273
+ } catch (e) {
274
+ // handle any exception
275
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue freeing queue item');
276
+ return callback(null, errorObj);
277
+ }
278
+ }
279
+
280
+ /*
281
+ * INTERNAL FUNCTION: freeExpiredQueueItems goes through the current active usage to
282
+ * determine if any have finished running long enough ago that the turn has expired.
283
+ * If it has, this method calls freeQueueItem to have the turn freed in Pronghorn so
284
+ * another request can get the turn. This is only used when there is some graceful
285
+ * expiration to a concurrent run. If runs expire immediately, expireTimeout will
286
+ * be 0 and this does nothing.
287
+ */
288
+ function freeExpiredQueueItems(callback) {
289
+ const origin = `${id}-throttle-freeExpiredQueueItems`;
290
+ log.spam(origin);
291
+
292
+ try {
293
+ const status = 'success';
294
+
295
+ // if timeout is 0, this is handled in finish queue item as that is more efficient
296
+ if (expireTimeout === 0) {
297
+ return callback(status);
298
+ }
299
+
300
+ const cur = new Date();
301
+
302
+ // actual call to retrieve the in use turn from the database
303
+ return getQueueItems(dbUtilInst, queueColl, { active: true }, (currentUse, gerror) => {
304
+ if (gerror) {
305
+ return callback(null, gerror);
306
+ }
307
+
308
+ // if there is nothing to free just return
309
+ if (currentUse.length === 0) {
310
+ return callback(status);
311
+ }
312
+
313
+ let handled = 0;
314
+
315
+ // go through the queue items to see if any have expired
316
+ for (let i = 0; i < currentUse.length; i += 1) {
317
+ // if the transaction finished and the end + timeout is less than the current time
318
+ if (currentUse[i].end !== null
319
+ && (Number(currentUse[i].end) + Number(expireTimeout) < cur.getTime())) {
320
+ freeQueueItem(dbUtilInst, queueColl, currentUse[i], (flic, ferror) => {
321
+ if (ferror) {
322
+ return callback(null, ferror);
323
+ }
324
+
325
+ handled += 1;
326
+ // when we have handled everything - return
327
+ if (handled === currentUse.length) {
328
+ log.spam(`${origin}: ${flic}`);
329
+ return callback(status);
330
+ }
331
+ });
332
+ } else {
333
+ handled += 1;
334
+ }
335
+ // when we have handled everything - return
336
+ if (handled === currentUse.length) {
337
+ return callback(status);
338
+ }
339
+ }
340
+
341
+ return callback(status);
342
+ });
343
+ } catch (e) {
344
+ // handle any exception
345
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue freeing queue items');
346
+ return callback(null, errorObj);
347
+ }
348
+ }
349
+
350
+ /*
351
+ * INTERNAL FUNCTION: checkTurnAvailable is used to check what is available
352
+ * and what is ahead of me to determine if there is availability for my
353
+ * use.
354
+ */
355
+ function checkTurnAvailable(myRequest, myTransNum, callback) {
356
+ const origin = `${id}-throttle-checkTurnAvailable`;
357
+ log.spam(origin);
358
+
359
+ try {
360
+ return freeExpiredQueueItems((freeRes, ferror) => {
361
+ if (ferror) {
362
+ return callback(null, ferror);
363
+ }
364
+
365
+ log.spam(`${origin}: ${freeRes}`);
366
+
367
+ // only want to set filter if using DB - if memory want the entire queue so we get all priorities
368
+ let useFilter = null;
369
+ if (numberPhs > 1) {
370
+ useFilter = { transNum: { $lte: myTransNum } };
371
+ }
372
+
373
+ // actual call to retrieve the queue items from the database
374
+ return getQueueItems(dbUtilInst, queueColl, useFilter, (queueItems, gerror) => {
375
+ if (gerror) {
376
+ return callback(null, gerror);
377
+ }
378
+
379
+ let numActive = 0;
380
+ let beforeMe = 0;
381
+
382
+ // go through the queue items - need to determine if available and my place in queue
383
+ // Can not assume that I am last in the returned items or that they are in order as data
384
+ // from Mongo is not sorted - YET!
385
+ for (let i = 0; i < queueItems.length; i += 1) {
386
+ if (JSON.stringify(queueItems[i]) !== '{}') {
387
+ // if this item is active
388
+ if (queueItems[i].active) {
389
+ numActive += 1;
390
+ } else if (numberPhs > 1) {
391
+ // if database, need to be able to reorder since db doesn't maintain order
392
+ // if the item is not active but before me in the queue
393
+ if (Number(queueItems[i].transNum) < Number(myTransNum)) {
394
+ beforeMe += 1;
395
+ } else if (Number(queueItems[i].transNum) === Number(myTransNum)
396
+ && queueItems[i].ph_instance < phInstance) {
397
+ // if the item is not active but before me in the queue (same trans - < pronghorn)
398
+ beforeMe += 1;
399
+ } else if (Number(queueItems[i].transNum) === Number(myTransNum)
400
+ && queueItems[i].ph_instance === phInstance
401
+ && Number(queueItems[i].request_id) < Number(myRequest)) {
402
+ // if the item is not active but before me in the queue (same trans but request id)
403
+ beforeMe += 1;
404
+ }
405
+ } else if (queueItems[i].transNum === Number(myTransNum)) {
406
+ // if this is me --- I know my index and can finish searching
407
+ beforeMe = i - numActive;
408
+ break;
409
+ }
410
+ }
411
+ }
412
+
413
+ // clear the memory from the queue items so it can be reclaimed
414
+ queueItems = undefined;
415
+
416
+ // number of spaces available (max - active)
417
+ const spaceAvail = Number(concurrentMax) - numActive;
418
+
419
+ // can I run? if turn available are greater then before me - yes
420
+ if (beforeMe < spaceAvail) {
421
+ return callback(0);
422
+ }
423
+
424
+ // otherwise the ones before can be reduced by the ones that are about to start running
425
+ return callback((beforeMe - spaceAvail) + 1);
426
+ });
427
+ });
428
+ } catch (e) {
429
+ // handle any exception
430
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue checking turn');
431
+ return callback(null, errorObj);
432
+ }
433
+ }
434
+
435
+ /*
436
+ * INTERNAL FUNCTION: gettingCloseInterval checks if it is my turn under faster since
437
+ * my turn is getting near.
438
+ */
439
+ function gettingCloseInterval(myRequest, transNum, callback) {
440
+ const origin = `${id}-throttle-gettingCloseInterval`;
441
+ log.spam(origin);
442
+
443
+ try {
444
+ let intRun = false;
445
+ const fastInt = (avgTotal / avgSize) * 0.5;
446
+
447
+ // rapid inner interval - should be done when it is almost my time to run.
448
+ const intervalObject = setInterval(() => {
449
+ // Prevents running the interval ontop of itself in case interval time is less than time
450
+ // it takes to run
451
+ if (!intRun) {
452
+ intRun = true;
453
+
454
+ // check if there is an actual turn available
455
+ checkTurnAvailable(myRequest, transNum, (toRun, cerror) => {
456
+ if (cerror) {
457
+ return callback(null, cerror);
458
+ }
459
+
460
+ // is it my turn to run
461
+ if (toRun === 0) {
462
+ clearInterval(intervalObject);
463
+ return callback(true);
464
+ }
465
+
466
+ intRun = false;
467
+ log.debug(`${origin}: Request ${myRequest} Transaction ${transNum} waiting for turn to become free`);
468
+ });
469
+ }
470
+ }, fastInt);
471
+ } catch (e) {
472
+ // handle any exception
473
+ const errorObj = transUtilInst.checkAndReturn(e, origin, 'Issue during getting close');
474
+ return callback(null, errorObj);
475
+ }
476
+ }
477
+
478
+ class SystemXThrottle {
479
+ /**
480
+ * Throttle
481
+ * @constructor
482
+ */
483
+ constructor(prongid, properties, transUtilCl, dbUtilCl) {
484
+ this.myid = prongid;
485
+ id = prongid;
486
+ this.transUtil = transUtilCl;
487
+ this.dbUtil = dbUtilCl;
488
+
489
+ // set globals (available to private functions)
490
+ transUtilInst = this.transUtil;
491
+ dbUtilInst = this.dbUtil;
492
+
493
+ // this uniquely identifies this adapter on this pronghorn system
494
+ phInstance = `${id} - ${os.hostname()}`;
495
+ queueColl = id + queueColl;
496
+ this.qlockInst = new AsyncLockCl();
497
+ this.alock = new AsyncLockCl();
498
+ qlock = this.qlockInst;
499
+
500
+ // set up the properties I care about
501
+ this.refreshProperties(properties);
502
+ }
503
+
504
+ /**
505
+ * @callback verifyCallback
506
+ * @param {Boolean} result - the result of the verify ready
507
+ * @param {String} error - any error that occured
508
+ */
509
+ /**
510
+ * @callback queueCallback
511
+ * @param {Object} queueItem - the updated queue item
512
+ * @param {String} error - any error that occured
513
+ */
514
+
515
+ /* THROTTLE ENGINE EXTERNAL FUNCTIONS */
516
+ /* refreshProperties - take in new properties without having to restart */
517
+ /* verifyReady - verify that we are ready */
518
+ /* requestQueueItem - put self in queue */
519
+ /* waitingMyTurn - interval waiting turn to run */
520
+ /* finishTurn - done using my turn to run */
521
+ /**
522
+ * refreshProperties is used to set up all of the properties for the throttle engine.
523
+ * It allows properties to be changed later by simply calling refreshProperties rather
524
+ * than having to restart the throttle engine.
525
+ *
526
+ * @function refreshProperties
527
+ * @param {Object} properties - an object containing all of the properties
528
+ */
529
+ refreshProperties(properties) {
530
+ const origin = `${this.myid}-throttle-refreshProperties`;
531
+ log.trace(origin);
532
+ props = properties;
533
+
534
+ if (!props) {
535
+ log.error(`${origin}: Throttle received no properties!`);
536
+ return;
537
+ }
538
+
539
+ if (props.throttle) {
540
+ // set the throttle number of pronghorns (optional - default is 1)
541
+ if (props.throttle.number_pronghorns && Number(props.throttle.number_pronghorns) >= 1) {
542
+ numberPhs = Number(props.throttle.number_pronghorns);
543
+ }
544
+
545
+ // set the throttle synchronous or asynchronous (optional - default is synchronous)
546
+ if (props.throttle.sync_async && (props.throttle.sync_async === 'async'
547
+ || props.throttle.sync_async === 'asynchronous')) {
548
+ syncAsync = 'async';
549
+ log.error(`${origin}: Throttle Engine does not currently support async ${syncAsync}`);
550
+ syncAsync = 'sync';
551
+ }
552
+
553
+ // set the throttle maximum queue size (optional - default is 1000)
554
+ if (props.throttle.max_in_queue && Number(props.throttle.max_in_queue) > 0) {
555
+ MaxInQueue = Number(props.throttle.max_in_queue);
556
+ }
557
+
558
+ // set the throttle max (optional - default is 1)
559
+ if (props.throttle.concurrent_max !== null && Number(props.throttle.concurrent_max) > -1) {
560
+ if (props.throttle.concurrent_max === 0) {
561
+ log.warn(`${origin}: concurrent_max is set to 0. All requests will be blocked until concurrent_max is not 0`);
562
+ }
563
+ concurrentMax = Number(props.throttle.concurrent_max);
564
+ }
565
+
566
+ // set the expire timeout (optional - default is 0 milliseconds)
567
+ if (props.throttle.expire_timeout && Number(props.throttle.expire_timeout) >= 0) {
568
+ expireTimeout = Number(props.throttle.expire_timeout);
569
+ }
570
+
571
+ // set the queue interval (optional - default is 200) can not be less than 50ms
572
+ if (props.throttle.avg_runtime && Number(props.throttle.avg_runtime) >= 50) {
573
+ avgRuntime = Number(props.throttle.avg_runtime);
574
+ }
575
+
576
+ // set the priority table (default is empty)
577
+ if (props.throttle.priorities && props.throttle.priorities.length > 0) {
578
+ for (let p = 0; p < props.throttle.priorities.length; p += 1) {
579
+ if (props.throttle.priorities.value !== undefined && props.throttle.priorities.value !== null
580
+ && props.throttle.priorities.percent !== undefined && props.throttle.priorities.percent !== null) {
581
+ const prior = {
582
+ value: props.throttle.priorities.value,
583
+ percent: props.throttle.priorities.percent
584
+ };
585
+ priorityTable.push(prior);
586
+ }
587
+ }
588
+ }
589
+
590
+ // reset the average runtime queue
591
+ this.alock.acquire(avgQlock, (done) => {
592
+ avgQueue = [];
593
+ avgTotal = 0;
594
+
595
+ // load up the tools for calculating the average run time
596
+ for (let i = 0; i < avgSize; i += 1) {
597
+ avgQueue.push(avgRuntime);
598
+ avgTotal += avgRuntime;
599
+ }
600
+
601
+ done(avgTotal / avgSize);
602
+ }, (ret, error) => {
603
+ if (error) {
604
+ log.error(`${origin}: Error from updating average queue: ${error}`);
605
+ }
606
+
607
+ log.debug(`${origin}: Average run time now reset to: ${ret}`);
608
+ });
609
+ }
610
+ }
611
+
612
+ /**
613
+ * verifyReady is used to verify everything needed for throttling is set up. This
614
+ * generally means that it there is more than one Pronghorn, the database collection
615
+ * exists.
616
+ *
617
+ * @function verifyReady
618
+ * @param {Function} callback - a callback function to return whether throttle engine is ready
619
+ */
620
+ verifyReady(callback) {
621
+ const origin = `${this.myid}-throttle-verifyReady`;
622
+ log.trace(origin);
623
+
624
+ try {
625
+ // if we are using Mongo - make sure it is set up properly
626
+ if (numberPhs > 1) {
627
+ const adapterProps = pronghornProps.adapterProps.adapters;
628
+ let prongo = null;
629
+
630
+ // Find the 'pronghorn' db
631
+ for (let i = 0; i < adapterProps.length; i += 1) {
632
+ if (adapterProps[i].type === 'MongoDriver') {
633
+ prongo = adapterProps[i];
634
+ break;
635
+ }
636
+ }
637
+
638
+ if (prongo === null) {
639
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['Database Properties'], null, null, null);
640
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
641
+ return callback(null, errorObj);
642
+ }
643
+
644
+ // Connection URL
645
+ let murl = prongo.properties.url;
646
+ log.spam(`${origin}: ${murl}`);
647
+ const dbName = prongo.properties.db;
648
+
649
+ // define some local variables to help in validating the properties.json file
650
+ let sslEnabled = false;
651
+ let sslValidate = false;
652
+ let sslCheckServerIdentity = false;
653
+ let sslCA = null;
654
+ let replSetEnabled = false;
655
+ let dbAuthEnabled = false;
656
+ let dbUsername = 'pronghorn';
657
+ let dbPassword = 'pronghorn';
658
+
659
+ /*
660
+ * this first section is configuration mapping
661
+ * it can be replaced with the config object when available
662
+ */
663
+ if (prongo.properties.ssl) {
664
+ // enable ssl encryption?
665
+ if (prongo.properties.ssl.enabled === true) {
666
+ log.info(`${origin}: Connecting to MongoDB with SSL.`);
667
+ sslEnabled = true;
668
+ // validate the server's certificate against a known certificate authority?
669
+ if (prongo.properties.ssl.acceptInvalidCerts === false) {
670
+ sslValidate = true;
671
+ log.info(`${origin}: Certificate based SSL MongoDB connections will be used.`);
672
+ // if validation is enabled, we need to read the CA file
673
+ if (prongo.properties.ssl.sslCA) {
674
+ try {
675
+ sslCA = [fs.readFileSync(prongo.properties.ssl.sslCA)];
676
+ } catch (err) {
677
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', ['CA FIle'], null, null, null);
678
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
679
+ return callback(null, errorObj);
680
+ }
681
+ } else {
682
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing File', ['CA FIle'], null, null, null);
683
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
684
+ return callback(null, errorObj);
685
+ }
686
+ } else {
687
+ log.info(`${origin}: SSL MongoDB connection without CA certificate validation.`);
688
+ }
689
+ // validate the server certificate against the configured url?
690
+ if (prongo.properties.ssl.checkServerIdentity === true) {
691
+ sslCheckServerIdentity = true;
692
+ } else {
693
+ log.warn(`${origin}: WARNING: Skipping server identity validation`);
694
+ }
695
+ } else {
696
+ log.warn(`${origin}: WARNING: Connecting to MongoDB without SSL.`);
697
+ }
698
+ } else {
699
+ log.warn(`${origin}: WARNING: Connecting to MongoDB without SSL.`);
700
+ }
701
+
702
+ // are we using a replication set?
703
+ if (prongo.properties.replSet && prongo.properties.replSet.enabled === true) {
704
+ replSetEnabled = true;
705
+ murl = prongo.properties.url;
706
+ }
707
+ // are we using a username and password to authenticate?
708
+ if (prongo.properties.credentials) {
709
+ if (prongo.properties.credentials.dbAuth === true) {
710
+ dbAuthEnabled = true;
711
+ } else {
712
+ log.warn(`${origin}: WARNING: Connecting to MongoDB without user authentication.`);
713
+ }
714
+ if (prongo.properties.credentials.user) {
715
+ dbUsername = prongo.properties.credentials.user;
716
+ } else {
717
+ log.info(`${origin}: Using default mongo username`);
718
+ }
719
+ if (prongo.properties.credentials.passwd) {
720
+ dbPassword = prongo.properties.credentials.passwd;
721
+ } else {
722
+ log.info(`${origin}: Using default mongo password`);
723
+ }
724
+ if (dbAuthEnabled && (dbUsername === null || dbPassword === null)) {
725
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Credentials', [], null, null, null);
726
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
727
+ return callback(null, errorObj);
728
+ }
729
+ } else {
730
+ log.warn(`${origin}: WARNING: Connecting to MongoDB without user authentication.`);
731
+ }
732
+
733
+ /*
734
+ * This second section is to construct the mongo options object
735
+ */
736
+ const opts = {
737
+ reconnectTries: 2000,
738
+ reconnectInterval: 1000,
739
+ ssl: sslEnabled,
740
+ sslValidate,
741
+ checkServerIdentity: sslCheckServerIdentity
742
+ };
743
+
744
+ const options = (replSetEnabled === true) ? { replSet: opts } : { server: opts };
745
+ log.debug(`${origin}: Connecting to MongoDB with options ${JSON.stringify(options)}`);
746
+
747
+ if (sslValidate === true) {
748
+ opts.sslCA = sslCA;
749
+ }
750
+
751
+ // Connect to the DB
752
+ log.info(`${origin}: Workflow Engine: Establishing connection to Pronghorn DB...`);
753
+
754
+ this.dbUtil.connect((alive, client) => {
755
+ if (!alive) {
756
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Nonexistent', [], null, null, null);
757
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
758
+ return callback(null, errorObj);
759
+ }
760
+ log.info(`${origin}: Workflow Engine: Connection to Pronghorn DB Established`);
761
+ // Use db specified by properties file
762
+ const db = client.db(dbName);
763
+ /*
764
+ * once we are connected to mongo, we need to authenticate the connection
765
+ */
766
+ if (dbAuthEnabled) {
767
+ db.authenticate(dbUsername, dbPassword, (autherr, result) => {
768
+ if (autherr) {
769
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Credentials', [], null, null, null);
770
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
771
+ return callback(null, errorObj);
772
+ }
773
+ if (result === false) {
774
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Credentials', [], null, null, null);
775
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
776
+ return callback(null, errorObj);
777
+ }
778
+
779
+ /*
780
+ * finally, we need to perform a health check to ensure we can read from the database
781
+ */
782
+ log.info(`${origin}: Successfully authenticated with the Mongo DB server as user : ${dbUsername}`);
783
+ });
784
+ }
785
+
786
+ // See if the queue collection exists in the database
787
+ db.listCollections({ name: queueColl }).toArray((dberr, dbres) => {
788
+ // if it does not exist, create it and index it
789
+ if (dberr || dbres === null || dbres === undefined || dbres.length <= 0) {
790
+ log.info(`${origin}: Queue collection does not exist, creating new collection`);
791
+
792
+ // add the queue to the database
793
+ this.dbUtil.createCollection(queueColl, null, false, (error, result) => {
794
+ if (error) {
795
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Unable To Create Queue', [error], null, null, null);
796
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
797
+ return callback(null, errorObj);
798
+ }
799
+ log.info(`${origin}: ${result}`);
800
+ // Get the queue collection
801
+ this.dbUtil.createIndex(queueColl, { transNum: 1 }, {}, null, (inerr, inres) => {
802
+ if (inerr) {
803
+ log.error(`${origin}: Received an error on indexing ${inerr}`);
804
+ } else {
805
+ log.debug(`${origin}: Mongo ensure index returned ${inres}`);
806
+ }
807
+ });
808
+ return callback(true);
809
+ // ensureIndex is deprecated, using Node.js MongoDB's createIndex method now.
810
+ });
811
+ // db.createCollection(queueColl, null, false, (data, error) => {
812
+ // if (error) {
813
+ // const errorObj = this.transUtil.formatErrorObject(origin, 'Unable To Create Queue', [error], null, null, null);
814
+ // log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
815
+ // return callback(null, errorObj);
816
+ // }
817
+
818
+ // // Get the queue collection
819
+ // MONGOQ = db.collection(queueColl);
820
+ // MONGOQ.ensureIndex({ transNum: 1 }, (inerr, inres) => {
821
+ // if (inerr) {
822
+ // log.error(`${origin}: Received an error on indexing ${inerr}`);
823
+ // } else {
824
+ // log.debug(`${origin}: Mongo ensure index returned ${inres}`);
825
+ // }
826
+ // });
827
+
828
+ // return callback(true);
829
+ // });
830
+ } else {
831
+ // if the collection already exists in the database
832
+ log.info(`${origin}: Queue collection check passed`);
833
+
834
+ // Get the queue collection
835
+ // MONGOQ = db.collection(queueColl);
836
+ this.dbUtil.createCollection(queueColl, null, false, (error, res) => {
837
+ if (error) {
838
+ log.error(`${origin}: ${error}`);
839
+ return callback(null, error);
840
+ }
841
+ log.info(`${origin}: ${res}`);
842
+ this.dbUtil.createIndex(queueColl, { transNum: 1 }, {}, null, (inerr, inres) => {
843
+ if (inerr) {
844
+ log.error(`${origin}: Received an error on indexing ${inerr}`);
845
+ } else {
846
+ log.debug(`${origin}: Mongo ensure index returned ${inres}`);
847
+ }
848
+ });
849
+ // ensureIndex is deprecated, using Node.js MongoDB's createIndex method now.
850
+ });
851
+ // MONGOQ.ensureIndex({ transNum: 1 }, (inerr, inres) => {
852
+ // if (inerr) {
853
+ // log.error(`Received an error on indexing ${inerr}`);
854
+ // }
855
+
856
+ // log.debug(`Mongo ensure index returned ${inres}`);
857
+ // });
858
+
859
+ // Delete any queue belonging to this adapter
860
+ // These requests are no longer waiting/processing since the adapter went down
861
+ const deleteFilter = {
862
+ ph_instance: phInstance
863
+ };
864
+
865
+ this.dbUtil.delete(queueColl, deleteFilter, {}, true, null, false, (delerr, res) => {
866
+ if (delerr) {
867
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Unable To Clear Queue', [delerr], null, null, null);
868
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
869
+ return callback(null, errorObj);
870
+ }
871
+ log.debug(`${origin}: Mongo delete many returned ${res}`);
872
+ return callback(true);
873
+ });
874
+ // MONGOQ.deleteMany(deleteFilter, (delerr, res) => {
875
+ // if (delerr) {
876
+ // const errorObj = this.transUtil.formatErrorObject(origin, 'Unable To Clear Queue', [delerr], null, null, null);
877
+ // log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
878
+ // return callback(null, errorObj);
879
+ // }
880
+
881
+ // log.debug(`Mongo delete many returned ${res}`);
882
+ // return callback(true);
883
+ // });
884
+ }
885
+ });
886
+ });
887
+ } else {
888
+ // if in memory queue nothing to verify
889
+ return callback(true);
890
+ }
891
+ } catch (e) {
892
+ // handle any exception
893
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue during verify ready');
894
+ return callback(null, errorObj);
895
+ }
896
+ }
897
+
898
+ /**
899
+ * requestQueueItem is used to request a queue item putting the request on the queue
900
+ * so that it can acquire the turn when it is available.
901
+ *
902
+ * @function requestQueueItem
903
+ * @param {String} myRequest - identifies the request. unique to this Pronghorn (required)
904
+ * @param {String} transNum - something to denote approximate order
905
+ * (e.g. time of the request) (required)
906
+ * @param {Number} priority - identifies the priorityfor the request (optional)
907
+ * @param {Function} callback - a callback function to return the resulting Queue Object
908
+ */
909
+ requestQueueItem(myRequest, transNum, priority, event, callback) {
910
+ const origin = `${this.myid}-throttle-requestQueueItem`;
911
+ log.trace(origin);
912
+
913
+ try {
914
+ if (myRequest === null || myRequest === undefined) {
915
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['request id'], null, null, null);
916
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
917
+ return callback(null, errorObj);
918
+ }
919
+ if (transNum === null || transNum === undefined) {
920
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['transaction number'], null, null, null);
921
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
922
+ return callback(null, errorObj);
923
+ }
924
+
925
+ // if priority
926
+ let myPercent = 100;
927
+ if (priority !== undefined && priority !== null && priority !== -1) {
928
+ // find the priority
929
+ for (let p = 0; p < priorityTable.length; p += 1) {
930
+ if (priority === priorityTable[p].value) {
931
+ try {
932
+ myPercent = Number(priorityTable[p].percent);
933
+ } catch (exp) {
934
+ myPercent = 100;
935
+ }
936
+ break;
937
+ }
938
+ }
939
+ }
940
+ // must make percent legitimate
941
+ if (myPercent < 0 || myPercent > 100) {
942
+ myPercent = 100;
943
+ }
944
+
945
+ // set up the queue item request - set instance, request, transNum,
946
+ // and set active and running flags to false
947
+ const data = {
948
+ _id: uuid.v4(),
949
+ ph_instance: phInstance,
950
+ request_id: myRequest,
951
+ transNum: Number(transNum),
952
+ priority: myPercent,
953
+ start: null,
954
+ end: null,
955
+ active: false,
956
+ running: false,
957
+ event
958
+ };
959
+
960
+ // If the number of Pronghorns is greater than 1, the queue is in the database
961
+ if (numberPhs > 1) {
962
+ // determine if there is space in the queue
963
+
964
+ // count is now deprecated, use countDocuments instead.
965
+ // operator replacements (count => countDocuments): $where => $expr, $near => $geoWithin with $center, $nearSphere => $geoWithin with $centerSphere
966
+ // return MONGOQ.count({}, (gerror, qsize) => {
967
+ return this.dbUtil.countDocuments(queueColl, {}, {}, null, false, (gerror, qsize) => {
968
+ if (gerror) {
969
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Error', [gerror], null, null, null);
970
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
971
+ return callback(null, errorObj);
972
+ }
973
+
974
+ // check to see if queue maxed out
975
+ if (qsize >= Number(MaxInQueue)) {
976
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Queue Full', [myRequest, transNum, qsize], null, null, null);
977
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
978
+ return callback(null, errorObj);
979
+ }
980
+
981
+ // write my queue item into the database queue
982
+ // return MONGOQ.insertOne(data, (error, result) => {
983
+ return this.dbUtil.create(queueColl, data, null, false, (error, result) => {
984
+ if (error) {
985
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Error', [error], null, null, null);
986
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
987
+ return callback(null, errorObj);
988
+ }
989
+
990
+ log.debug(`${origin}: Mongo insert one returned ${result}`);
991
+ return callback(data);
992
+ });
993
+ });
994
+ }
995
+
996
+ // If the number of Pronghorns is 1, the queue is in memory
997
+ // Lock the memQueue while adding the item
998
+ return qlock.acquire(memQlock, (done) => {
999
+ // if the queue is less than concurrentMax length -
1000
+ // just add it to queue at end as it will run right away
1001
+ if (memQueue.length < concurrentMax) {
1002
+ memQueue.push(data);
1003
+ done(memQueue[memQueue.length - 1]);
1004
+ } else if (myPercent >= 0 && myPercent < 100) {
1005
+ // if prioritized - put it at the right place in the queue
1006
+ let myIndex = Math.round(memQueue.length * (myPercent / 100));
1007
+
1008
+ // if myIndex is less than what is running, insert after running ones
1009
+ if (myIndex < concurrentMax) {
1010
+ myIndex = concurrentMax + 1;
1011
+ }
1012
+
1013
+ // if at the end push
1014
+ if (myIndex >= memQueue.length) {
1015
+ myIndex = memQueue.length;
1016
+ memQueue.push(data);
1017
+ } else {
1018
+ // need to put behind any other equal or higher priority requests already in the queue
1019
+ for (let pos = myIndex; pos < memQueue.length; pos += 1) {
1020
+ // when we find a lower priority request, we will put this one in front of it so break the loop
1021
+ if (memQueue[pos].priority === undefined || memQueue[pos].priority === null || memQueue[pos].priority > myPercent) {
1022
+ myIndex = pos;
1023
+ break;
1024
+ }
1025
+ }
1026
+
1027
+ // if not at the end, insert after current index
1028
+ memQueue.splice(myIndex, 0, data);
1029
+ }
1030
+
1031
+ // return my item
1032
+ done(memQueue[myIndex]);
1033
+ } else if (memQueue.length >= Number(MaxInQueue)) {
1034
+ // check to see if queue maxed out
1035
+ done(null, 'Queue Full');
1036
+ } else {
1037
+ // put at the end of the queue
1038
+ memQueue.push(data);
1039
+
1040
+ // return my item
1041
+ done(memQueue[memQueue.length - 1]);
1042
+ }
1043
+ }, (ret, error) => {
1044
+ if (error) {
1045
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Queue Full', [myRequest, transNum, memQueue.length], null, null, null);
1046
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1047
+ return callback(null, errorObj);
1048
+ }
1049
+ return callback(ret);
1050
+ });
1051
+ } catch (e) {
1052
+ // handle any exception
1053
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue while requesting a queue item');
1054
+ return callback(null, errorObj);
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * waitingMyTurn is used to determine when is it safe for the item to run. It attempts to see
1060
+ * if there is availability first, if it can not (because all spaces are in use) it will put the
1061
+ * attempts into an interval that re attempts. The interval is based on where in the queue and
1062
+ * approximate time before there will be availability.
1063
+ *
1064
+ * @function waitingMyTurn
1065
+ * @param {Object} queueItem - the queue request which would have been returned from
1066
+ * requestQueueItem (required)
1067
+ * @param {Function} callback - a callback function to return the resulting Queue Object
1068
+ */
1069
+ waitingMyTurn(queueItem, callback) {
1070
+ const origin = `${this.myid}-throttle-waitingMyTurn`;
1071
+ log.trace(origin);
1072
+
1073
+ try {
1074
+ if (queueItem === null || queueItem === undefined) {
1075
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['queue item'], null, null, null);
1076
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1077
+ return callback(null, errorObj);
1078
+ }
1079
+ if (queueItem.request_id === undefined || Number(queueItem.request_id) < 0) {
1080
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['queue item -> request id'], null, null, null);
1081
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1082
+ return callback(null, errorObj);
1083
+ }
1084
+ if (queueItem.transNum === undefined || Number(queueItem.transNum) < 0) {
1085
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['queue item -> transaction number'], null, null, null);
1086
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1087
+ return callback(null, errorObj);
1088
+ }
1089
+ if ((queueItem.active !== undefined && queueItem.active)
1090
+ || (queueItem.end !== undefined && queueItem.end !== null)) {
1091
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Item In Wrong State', ['already started'], null, null, null);
1092
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1093
+ return callback(null, errorObj);
1094
+ }
1095
+
1096
+ // check if there is an actual turn available
1097
+ return checkTurnAvailable(queueItem.request_id, queueItem.transNum, (toRun, cerror) => {
1098
+ if (cerror) {
1099
+ return callback(null, cerror);
1100
+ }
1101
+
1102
+ // is it my turn to run
1103
+ if (toRun === 0) {
1104
+ return claimTurn(this.dbUtil, queueColl, queueItem, callback);
1105
+ }
1106
+
1107
+ if (toRun <= (3 * concurrentMax)) {
1108
+ // if I have less than 3 wait cycles, OK to start running checks faster
1109
+ return gettingCloseInterval(queueItem.request_id, queueItem.transNum, (waitEnd, gerror) => {
1110
+ if (gerror) {
1111
+ return callback(null, gerror);
1112
+ }
1113
+
1114
+ log.spam(`${origin}: ${waitEnd}`);
1115
+ return claimTurn(this.dbUtil, queueColl, queueItem, callback);
1116
+ });
1117
+ }
1118
+ // set divisor to be 1 if concurrentMax is 0
1119
+ const concurrentMaxDivisor = concurrentMax !== 0 ? concurrentMax : 1;
1120
+ // calculate the wait for the outer timeout (this one needs to get us close to our run time)
1121
+ let outerInterval = (toRun / concurrentMaxDivisor) * (expireTimeout + (avgTotal / avgSize));
1122
+
1123
+ // Use the 90% of the outer intverval or the outer Interval -2 seconds which ever is greater
1124
+ if ((outerInterval * 0.1) > 2000) {
1125
+ outerInterval -= 2000;
1126
+ } else {
1127
+ outerInterval *= 0.95;
1128
+ }
1129
+
1130
+ log.debug(`${origin}: Request ${queueItem.request_id} Transaction ${queueItem.transNum} Outer Interval set to: ${outerInterval}`);
1131
+
1132
+ // outer interval to get a turn request
1133
+ // The outer interval is really used to get us close to when we should run
1134
+ const intervalObject = setTimeout(() => {
1135
+ clearTimeout(intervalObject);
1136
+
1137
+ // recurrsive call in case re-estimate is needed
1138
+ // (abort, things taking longer than approximation, etc)
1139
+ return this.waitingMyTurn(queueItem, callback);
1140
+ }, outerInterval);
1141
+ });
1142
+ } catch (e) {
1143
+ // handle any exception
1144
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue while waiting turn');
1145
+ return callback(null, errorObj);
1146
+ }
1147
+ }
1148
+
1149
+ /**
1150
+ * finishTurn is used to mark the request completed. This means setting the end time
1151
+ * and changing the running flag to false. If there is no graceful expiration of the
1152
+ * concurrent request, then this makes the call to free the turn.
1153
+ *
1154
+ * @function finishTurn
1155
+ * @param {Object} queueItem - the queue request which would have been returned from
1156
+ * waitingMyTurn (required)
1157
+ * @param {Number} reqEnd - the time it took to execute the call (optional)
1158
+ * @param {Function} callback - a callback function to return the finished Queue Object
1159
+ */
1160
+ finishTurn(queueItem, reqEnd, callback) {
1161
+ const origin = `${this.myid}-throttle-finishTurn`;
1162
+ log.trace(origin);
1163
+
1164
+ try {
1165
+ if (queueItem === null || queueItem === undefined) {
1166
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Missing Data', ['queue item'], null, null, null);
1167
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1168
+ return callback(null, errorObj);
1169
+ }
1170
+ if (queueItem.start === undefined || queueItem.start === null
1171
+ || queueItem.active === undefined || !queueItem.active) {
1172
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Item In Wrong State', ['not started'], null, null, null);
1173
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1174
+ return callback(null, errorObj);
1175
+ }
1176
+ if (queueItem.end !== undefined && queueItem.end !== null) {
1177
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Item In Wrong State', ['already ended'], null, null, null);
1178
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1179
+ return callback(null, errorObj);
1180
+ }
1181
+
1182
+ const cur = new Date();
1183
+
1184
+ // set up the update object to mark the queue item that the execution finished
1185
+ // set end and change running to false
1186
+ const data = {
1187
+ _id: queueItem._id,
1188
+ ph_instance: queueItem.ph_instance,
1189
+ request_id: queueItem.request_id,
1190
+ transNum: queueItem.transNum,
1191
+ priority: queueItem.priority,
1192
+ start: queueItem.start,
1193
+ end: cur.getTime(),
1194
+ active: queueItem.active,
1195
+ running: false,
1196
+ event: queueItem.event
1197
+ };
1198
+
1199
+ // Lock the avgQlock while update the average time
1200
+ if (reqEnd !== null && reqEnd !== '') {
1201
+ this.alock.acquire(avgQlock, (done) => {
1202
+ const reqTimeMS = reqEnd / 100000;
1203
+ avgTotal = (avgTotal - avgQueue[avgPtr]) + reqTimeMS;
1204
+ avgQueue[avgPtr] = reqTimeMS;
1205
+ avgPtr += 1;
1206
+
1207
+ if (avgPtr === avgSize) {
1208
+ avgPtr = 0;
1209
+ }
1210
+
1211
+ done(avgTotal / avgSize);
1212
+ }, (ret, error) => {
1213
+ if (error) {
1214
+ log.error(`${origin}: Error from updating average queue: ${error}`);
1215
+ }
1216
+
1217
+ log.debug(`${origin}: Average run time now set to: ${ret}`);
1218
+ });
1219
+ }
1220
+
1221
+ // if there is no timeout, delete the request and free the turn
1222
+ if (expireTimeout === 0) {
1223
+ return freeQueueItem(this.dbUtil, queueColl, queueItem, callback);
1224
+ }
1225
+
1226
+ if (numberPhs > 1) {
1227
+ // otherwise mark the request done, can not free it yet
1228
+ // If the number of Pronghorns is greater than 1, the queue is in the database
1229
+ // return MONGOQ.replaceOne({ _id: data._id }, data, (error, result) => {
1230
+ return this.dbUtil.replaceOne(queueColl, { _id: data._id }, data, {}, null, false, (error, result) => {
1231
+ if (error) {
1232
+ const errorObj = this.transUtil.formatErrorObject(origin, 'Database Error', ['could not update item'], null, null, null);
1233
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1234
+ return callback(null, errorObj);
1235
+ }
1236
+
1237
+ log.debug(`${origin}: Mongo replace one returned ${result}`);
1238
+ return callback(data);
1239
+ });
1240
+ }
1241
+
1242
+ // If the number of Pronghorns is 1, the queue is in memory
1243
+ // Lock the memQueue while finding and updating the item
1244
+ return qlock.acquire(memQlock, (done) => {
1245
+ const index = memQueue.indexOf(queueItem);
1246
+
1247
+ if (index >= 0) {
1248
+ memQueue[index] = data;
1249
+ done(memQueue[index]);
1250
+ } else {
1251
+ done(null, 'item not found in queue');
1252
+ }
1253
+ }, (ret, error) => {
1254
+ if (error) {
1255
+ const errorObj = this.transUtil.formatErrorObject(origin, 'No Queue Item', ['data'], null, null, null);
1256
+ log.error(`${origin}: ${errorObj.IAPerror.displayString}`);
1257
+ return callback(null, errorObj);
1258
+ }
1259
+
1260
+ return callback(ret);
1261
+ });
1262
+ } catch (e) {
1263
+ // handle any exception
1264
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue during finish turn');
1265
+ return callback(null, errorObj);
1266
+ }
1267
+ }
1268
+
1269
+ /**
1270
+ * getQueue is used to get information for all of the requests currently in the queue.
1271
+ *
1272
+ * @function getQueue
1273
+ * @param {Function} callback - a callback function to return the Queue
1274
+ */
1275
+ getQueue(callback) {
1276
+ const origin = `${this.myid}-throttle-getQueue`;
1277
+ log.trace(origin);
1278
+
1279
+ try {
1280
+ return getQueueItems(this.dbUtil, queueColl, null, callback);
1281
+ } catch (e) {
1282
+ // handle any exception
1283
+ const errorObj = this.transUtil.checkAndReturn(e, origin, 'Issue during get queue');
1284
+ return callback(null, errorObj);
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ module.exports = SystemXThrottle;